diff --git a/packages/auth/src/core/auth/auth_impl.test.ts b/packages/auth/src/core/auth/auth_impl.test.ts index 79c459ce9e1..d8a30fc88a4 100644 --- a/packages/auth/src/core/auth/auth_impl.test.ts +++ b/packages/auth/src/core/auth/auth_impl.test.ts @@ -553,17 +553,49 @@ describe('core/auth/auth_impl', () => { expect(callbackCalled).to.be.false; }); + it('immediately calls firebaseTokenChange if initialization finished', done => { + const token: FirebaseToken = { + token: 'test-token', + expirationTime: 123456 + }; + (auth as any).firebaseToken = token; + auth._isInitialized = true; + auth.onFirebaseTokenChanged(t => { + expect(t).to.eq(token); + done(); + }); + }); + + it('waits for initialization for onFirebaseTokenChanged', done => { + const token: FirebaseToken = { + token: 'test-token', + expirationTime: 123456 + }; + (auth as any).firebaseToken = token; + auth._isInitialized = false; + auth.onFirebaseTokenChanged(t => { + expect(t).to.eq(token); + done(); + }); + }); + describe('user logs in/out, tokens refresh', () => { let user: UserInternal; let authStateCallback: sinon.SinonSpy; let idTokenCallback: sinon.SinonSpy; let beforeAuthCallback: sinon.SinonSpy; + let firebaseTokenCallback: sinon.SinonSpy; + const testFirebaseToken: FirebaseToken = { + token: 'test-fb-token', + expirationTime: 123456789 + }; beforeEach(() => { user = testUser(auth, 'uid'); authStateCallback = sinon.spy(); idTokenCallback = sinon.spy(); beforeAuthCallback = sinon.spy(); + firebaseTokenCallback = sinon.spy(); }); context('initially currentUser is null', () => { @@ -571,10 +603,12 @@ describe('core/auth/auth_impl', () => { auth.onAuthStateChanged(authStateCallback); auth.onIdTokenChanged(idTokenCallback); auth.beforeAuthStateChanged(beforeAuthCallback); + auth.onFirebaseTokenChanged(firebaseTokenCallback); await auth._updateCurrentUser(null); authStateCallback.resetHistory(); idTokenCallback.resetHistory(); beforeAuthCallback.resetHistory(); + firebaseTokenCallback.resetHistory(); }); it('onAuthStateChange triggers on log in', async () => { @@ -591,6 +625,13 @@ describe('core/auth/auth_impl', () => { await auth._updateCurrentUser(user); expect(beforeAuthCallback).to.have.been.calledWith(user); }); + + it('onFirebaseTokenChanged triggers on token set', async () => { + await auth._updateFirebaseToken(testFirebaseToken); + expect(firebaseTokenCallback).to.have.been.calledWith( + testFirebaseToken + ); + }); }); context('initially currentUser is user', () => { @@ -598,10 +639,12 @@ describe('core/auth/auth_impl', () => { auth.onAuthStateChanged(authStateCallback); auth.onIdTokenChanged(idTokenCallback); auth.beforeAuthStateChanged(beforeAuthCallback); + auth.onFirebaseTokenChanged(firebaseTokenCallback); await auth._updateCurrentUser(user); authStateCallback.resetHistory(); idTokenCallback.resetHistory(); beforeAuthCallback.resetHistory(); + firebaseTokenCallback.resetHistory(); }); it('onAuthStateChange triggers on log out', async () => { @@ -638,6 +681,43 @@ describe('core/auth/auth_impl', () => { }); }); + context('initially firebaseToken is null', () => { + beforeEach(async () => { + auth.onFirebaseTokenChanged(firebaseTokenCallback); + await auth._updateFirebaseToken(null); + firebaseTokenCallback.resetHistory(); + }); + + it('onFirebaseTokenChanged triggers on token set', async () => { + await auth._updateFirebaseToken(testFirebaseToken); + expect(firebaseTokenCallback).to.have.been.calledWith( + testFirebaseToken + ); + }); + }); + + context('initially firebaseToken is token', () => { + beforeEach(async () => { + auth.onFirebaseTokenChanged(firebaseTokenCallback); + await auth._updateFirebaseToken(testFirebaseToken); + firebaseTokenCallback.resetHistory(); + }); + + it('onFirebaseTokenChanged triggers on token set to null', async () => { + await auth._updateFirebaseToken(null); + expect(firebaseTokenCallback).to.have.been.calledWith(null); + }); + + it('onFirebaseTokenChanged triggers for token props change', async () => { + const newToken: FirebaseToken = { + ...testFirebaseToken, + token: 'new-fb-token' + }; + await auth._updateFirebaseToken(newToken); + expect(firebaseTokenCallback).to.have.been.calledWith(newToken); + }); + }); + context('with Proactive Refresh', () => { let oldUser: UserInternal; @@ -739,6 +819,20 @@ describe('core/auth/auth_impl', () => { expect(cb2).to.have.been.calledWith(user); }); + it('onFirebaseTokenChanged works for multiple listeners', async () => { + const cb1 = sinon.spy(); + const cb2 = sinon.spy(); + auth.onFirebaseTokenChanged(cb1); + auth.onFirebaseTokenChanged(cb2); + await auth._updateFirebaseToken(null); + cb1.resetHistory(); + cb2.resetHistory(); + + await auth._updateFirebaseToken(testFirebaseToken); + expect(cb1).to.have.been.calledWith(testFirebaseToken); + expect(cb2).to.have.been.calledWith(testFirebaseToken); + }); + it('_updateCurrentUser throws if a beforeAuthStateChange callback throws', async () => { await auth._updateCurrentUser(null); const cb1 = sinon.stub().throws(); diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index 258e2052186..8c2babdd67c 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -647,6 +647,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { return this.registerStateListener( this.authStateSubscription, nextOrObserver, + this.currentUser, error, completed ); @@ -667,6 +668,21 @@ export class AuthImpl implements AuthInternal, _FirebaseService { return this.registerStateListener( this.idTokenSubscription, nextOrObserver, + this.currentUser, + error, + completed + ); + } + + onFirebaseTokenChanged( + nextOrObserver: NextOrObserver, + error?: ErrorFn, + completed?: CompleteFn + ): Unsubscribe | undefined { + return this.registerStateListener( + this.firebaseTokenSubscription, + nextOrObserver, + this.firebaseToken, error, completed ); @@ -814,9 +830,10 @@ export class AuthImpl implements AuthInternal, _FirebaseService { } } - private registerStateListener( - subscription: Subscription, - nextOrObserver: NextOrObserver, + private registerStateListener( + subscription: Subscription, + nextOrObserver: NextOrObserver, + currentValue: T | null, error?: ErrorFn, completed?: CompleteFn ): Unsubscribe { @@ -841,7 +858,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { if (isUnsubscribed) { return; } - cb(this.currentUser); + cb(currentValue); }); if (typeof nextOrObserver === 'function') { diff --git a/packages/auth/src/model/auth.ts b/packages/auth/src/model/auth.ts index 5d77f0f1fa6..308231573b0 100644 --- a/packages/auth/src/model/auth.ts +++ b/packages/auth/src/model/auth.ts @@ -18,12 +18,16 @@ import { Auth, AuthSettings, + CompleteFn, Config, EmulatorConfig, + ErrorFn, + NextOrObserver, PasswordPolicy, PasswordValidationStatus, PopupRedirectResolver, TenantConfig, + Unsubscribe, User } from './public_types'; import { ErrorFactory } from '@firebase/util'; @@ -77,6 +81,11 @@ export interface AuthInternal extends Auth { currentUser: User | null; emulatorConfig: EmulatorConfig | null; getFirebaseAccessToken(forceRefresh?: boolean): Promise; + onFirebaseTokenChanged( + nextOrObserver: NextOrObserver, + error?: ErrorFn, + completed?: CompleteFn + ): Unsubscribe | undefined; _agentRecaptchaConfig: RecaptchaConfig | null; _tenantRecaptchaConfigs: Record; _projectPasswordPolicy: PasswordPolicy | null;