Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ node_modules/

reactfire/docs/reactfire-metadata.json
reactfire/firestore-debug.log
reactfire/firebase-debug.log
reactfire/pub/**
pub
yarn-error.log
reactfire/database-debug.log
reactfire/reactfire-*.tgz
3 changes: 3 additions & 0 deletions reactfire/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
cjs/index.esm-*
**/*.test.d.ts
**/*.test.js
20 changes: 13 additions & 7 deletions reactfire/auth/auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ class MockAuth {
this.subscriber = null;
}

app = {
name: '[DEFAULT]'
};

notifySubscriber() {
if (this.subscriber) {
this.subscriber.next(this.user);
Expand All @@ -39,16 +43,16 @@ const mockFirebase = {
};

const Provider = ({ children }) => (
<FirebaseAppProvider firebaseApp={mockFirebase}>
<FirebaseAppProvider firebaseApp={(mockFirebase as any) as firebase.app.App}>
{children}
</FirebaseAppProvider>
);

const Component = ({ children }) => (
const Component = (props?: { children?: any }) => (
<Provider>
<React.Suspense fallback={'loading'}>
<AuthCheck fallback={<h1 data-testid="signed-out">not signed in</h1>}>
{children || <h1 data-testid="signed-in">signed in</h1>}
{props?.children || <h1 data-testid="signed-in">signed in</h1>}
</AuthCheck>
</React.Suspense>
</Provider>
Expand All @@ -57,12 +61,14 @@ const Component = ({ children }) => (
describe('AuthCheck', () => {
beforeEach(() => {
// clear the signed in user
mockFirebase.auth().updateUser(null);
act(() => mockFirebase.auth().updateUser(null));
});

afterEach(() => {
cleanup();
jest.clearAllMocks();
act(() => {
cleanup();
jest.clearAllMocks();
});
});

it('can find firebase Auth from Context', () => {
Expand Down Expand Up @@ -95,7 +101,7 @@ describe('AuthCheck', () => {
});

it('renders children if a user is logged in', async () => {
mockFirebase.auth().updateUser({ uid: 'testuser' });
act(() => mockFirebase.auth().updateUser({ uid: 'testuser' }));
const { getByTestId } = render(<Component />);

await wait(() => expect(getByTestId('signed-in')).toBeInTheDocument());
Expand Down
23 changes: 8 additions & 15 deletions reactfire/auth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export function preloadUser(firebaseApp: firebase.app.App) {
return preloadAuth(firebaseApp).then(auth => {
const result = preloadObservable(
user(auth() as firebase.auth.Auth),
'auth: user'
`auth:user:${firebaseApp.name}`
);
return result.request.promise;
return result.toPromise();
});
}

Expand All @@ -31,18 +31,8 @@ export function useUser<T = unknown>(
options?: ReactFireOptions<T>
): User | T {
auth = auth || useAuth()();

let currentUser = undefined;

if (options && options.startWithValue !== undefined) {
currentUser = options.startWithValue;
} else if (auth.currentUser) {
// if auth.currentUser is undefined or null, we won't use it
// because null can mean "not signed in" OR "still loading"
currentUser = auth.currentUser;
}

return useObservable(user(auth), 'auth: user', currentUser);
const currentUser = auth.currentUser || options?.startWithValue;
return useObservable(user(auth), `auth:user:${auth.app.name}`, currentUser);
}

export function useIdTokenResult(user: User, forceRefresh: boolean = false) {
Expand All @@ -52,7 +42,10 @@ export function useIdTokenResult(user: User, forceRefresh: boolean = false) {

const idToken$ = from(user.getIdTokenResult(forceRefresh));

return useObservable<any>(idToken$, `${user.uid}-claims`);
return useObservable<any>(
idToken$,
`auth:idTokenResult:${user.uid}:forceRefresh=${forceRefresh}`
);
}

export interface AuthCheckProps {
Expand Down
14 changes: 8 additions & 6 deletions reactfire/database/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function useDatabaseObject<T = unknown>(
): QueryChange | T {
return useObservable(
object(ref),
`RTDB Doc: ${ref.toString()}`,
`database:object:${ref.toString()}`,
options ? options.startWithValue : undefined
);
}
Expand Down Expand Up @@ -54,9 +54,10 @@ export function useDatabaseObjectData<T>(
ref: database.Reference,
options?: ReactFireOptions<T>
): T {
const idField = checkIdField(options);
return useObservable(
objectVal(ref, checkIdField(options)),
`RTDB DocData: ${ref.toString()}`,
objectVal(ref, idField),
`database:objectVal:${ref.toString()}:idField=${idField}`,
checkStartWithValue(options)
);
}
Expand All @@ -78,7 +79,7 @@ export function useDatabaseList<T = { [key: string]: unknown }>(
ref: database.Reference | database.Query,
options?: ReactFireOptions<T[]>
): QueryChange[] | T[] {
const hash = `RTDB List: ${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`;
const hash = `database:list:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}`;

return useObservable(
list(ref),
Expand All @@ -91,9 +92,10 @@ export function useDatabaseListData<T = { [key: string]: unknown }>(
ref: database.Reference | database.Query,
options?: ReactFireOptions<T[]>
): T[] {
const idField = checkIdField(options);
return useObservable(
listVal(ref, checkIdField(options)),
`RTDB ListData: ${ref.toString()}`,
listVal(ref, idField),
`database:listVal:${ref.toString()}|${(ref as _QueryWithId).queryIdentifier()}:idField=${idField}`,
checkStartWithValue(options)
);
}
67 changes: 59 additions & 8 deletions reactfire/firebaseApp/firebaseApp.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@ import { FirebaseAppProvider } from './index';

afterEach(cleanup);

const DEFAULT_APP_CONFIG = { appId: '12345' };

describe('FirebaseAppProvider', () => {
it('calls firebase.initializeApp with the provided config', () => {
const config = { appId: '12345' };

const spy = jest.spyOn(firebase, 'initializeApp');

render(<FirebaseAppProvider firebaseConfig={config} />);
expect(spy).toBeCalledWith(config);
render(<FirebaseAppProvider firebaseConfig={DEFAULT_APP_CONFIG} />);
expect(spy).toBeCalledWith(DEFAULT_APP_CONFIG, undefined);

spy.mockRestore();
});

it('does not call firebase.initializeApp if the firebaseApp is provided', () => {
const spy = jest.spyOn(firebase, 'initializeApp');
const app = {};
const app: firebase.app.App = {} as any;
render(<FirebaseAppProvider firebaseApp={app} />);
expect(spy).not.toBeCalled();

Expand All @@ -32,7 +32,7 @@ describe('FirebaseAppProvider', () => {
it('initializes fireperf if specified', async () => {
const mockPerf = jest.fn();
firebase['performance' as any] = mockPerf;
const app = { performance: mockPerf };
const app: firebase.app.App = { performance: mockPerf } as any;

render(<FirebaseAppProvider firebaseApp={app} initPerformance />);

Expand All @@ -42,7 +42,7 @@ describe('FirebaseAppProvider', () => {
it('does not initialize fireperf if not specified', async () => {
const mockPerf = jest.fn();
firebase['performance' as any] = mockPerf;
const app = { performance: mockPerf };
const app: firebase.app.App = { performance: mockPerf } as any;

render(<FirebaseAppProvider firebaseApp={app} />);

Expand All @@ -52,7 +52,7 @@ describe('FirebaseAppProvider', () => {

describe('useFirebaseApp', () => {
it('finds firebase from Context', () => {
const firebaseApp = { a: 1 };
const firebaseApp: firebase.app.App = { a: 1 } as any;

const wrapper = ({ children }) => (
<FirebaseAppProvider firebaseApp={firebaseApp}>
Expand All @@ -65,6 +65,57 @@ describe('useFirebaseApp', () => {
expect(result.current).toBe(firebaseApp);
});

it('can initialize more than one firebase app', () => {
const config = { a: 1 };

const initializeApp = jest.spyOn(firebase, 'initializeApp');

const wrapper = ({ children }) => (
<div>
<FirebaseAppProvider firebaseConfig={DEFAULT_APP_CONFIG}>
{children}
</FirebaseAppProvider>
<FirebaseAppProvider firebaseConfig={config} appName="app-2">
appA
</FirebaseAppProvider>
</div>
);

const { result } = renderHook(() => useFirebaseApp(), { wrapper });

expect(initializeApp).toBeCalledWith(config, 'app-2');
initializeApp.mockRestore();

expect(result.error).toBeUndefined();
});

it('will throw if configs dont match, and same name', () => {
const config = { a: 1 };

const initializeApp = jest.spyOn(firebase, 'initializeApp');

const wrapper = ({ children }) => (
<div>
<FirebaseAppProvider firebaseConfig={DEFAULT_APP_CONFIG}>
{children}
</FirebaseAppProvider>
<FirebaseAppProvider firebaseConfig={config}>appA</FirebaseAppProvider>
</div>
);

try {
const { result } = renderHook(() => useFirebaseApp(), { wrapper });
fail('expected a throw');
} catch (e) {
expect(e).toEqual(
'Does not match the options already provided to the default firebase app instance, give this new instance a different appName.'
);
}

expect(initializeApp).not.toBeCalled();
initializeApp.mockRestore();
});

it('throws an error if Firebase is not in context', () => {
const { result } = renderHook(() => useFirebaseApp());

Expand Down
50 changes: 34 additions & 16 deletions reactfire/firebaseApp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,54 @@ export * from './sdk';

type FirebaseAppContextValue = firebase.app.App;

// INVESTIGATE I don't like magic strings, can we have export this in js-sdk?
const DEFAULT_APP_NAME = '[DEFAULT]';

const FirebaseAppContext = React.createContext<
FirebaseAppContextValue | undefined
>(undefined);

export function FirebaseAppProvider(props) {
const { firebaseConfig, initPerformance } = props;
let { firebaseApp } = props;
type Props = {
initPerformance?: boolean;
firebaseApp?: firebase.app.App;
firebaseConfig?: Object;
appName?: string;
};

firebaseApp =
firebaseApp ||
export function FirebaseAppProvider(props: Props & { [key: string]: unknown }) {
const { firebaseConfig, appName, initPerformance = false } = props;
const firebaseApp: firebase.app.App =
props.firebaseApp ||
React.useMemo(() => {
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
const existingApp = firebase.apps.find(
app => app.name == (appName || DEFAULT_APP_NAME)
);
if (existingApp) {
// INVESTIGATE can we do a shallow eq check rather than JSON eq?
if (
JSON.stringify(existingApp.options) != JSON.stringify(firebaseConfig)
) {
throw `Does not match the options already provided to the ${appName ||
'default'} firebase app instance, give this new instance a different appName.`;
}
return existingApp;
} else {
return firebase.initializeApp(firebaseConfig, appName);
}

return firebase;
}, [firebaseConfig]);
}, [firebaseConfig, appName]);

React.useMemo(() => {
if (initPerformance === true && !!firebase.apps.length) {
if (!firebase.performance) {
if (initPerformance === true) {
if (firebaseApp.performance) {
// initialize Performance Monitoring
firebaseApp.performance();
} else {
throw new Error(
'firebase.performance not found. Did you forget to import it?'
);
}

// initialize Performance Monitoring
firebase.performance();
}
}, [initPerformance, firebaseApp]);
}, [initPerformance, appName]);

return <FirebaseAppContext.Provider value={firebaseApp} {...props} />;
}
Expand Down
Loading