Skip to content

Commit 8e15973

Browse files
Adding auth state ready() ax (#7384)
* added authStateReady function * updated Auth interface * added unit tests on authStateReady * updated demo app * added authStateReady to Auth interface * fixed formatting issue * updated authStateReady calls in demo app * fixed formatting issues * fixed demo app to incorporate authStateReady * formatted code * removed unnecessary async keywords * reverted changes in onSignOut * clean up code * fixed comments * resolved code review comments and updated tests * changed reference doc * resolved comments from pr * added changeset * Update twelve-actors-enjoy.md * resolved doc change check failure * resolved issues with Doc Change Check * clarify comments * fixed doc change check issue
1 parent 3f3f536 commit 8e15973

File tree

7 files changed

+225
-53
lines changed

7 files changed

+225
-53
lines changed

.changeset/twelve-actors-enjoy.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@firebase/auth': minor
3+
'firebase': minor
4+
---
5+
6+
Implemented `authStateReady()`, which returns a promise that resolves immediately when the initial auth state is settled and currentUser is available. When the promise is resolved, the current user might be a valid user or null if there is no user signed in currently.

common/api-review/auth.api.md

+1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export function applyActionCode(auth: Auth, oobCode: string): Promise<void>;
8181
// @public
8282
export interface Auth {
8383
readonly app: FirebaseApp;
84+
authStateReady(): Promise<void>;
8485
beforeAuthStateChanged(callback: (user: User | null) => void | Promise<void>, onAbort?: () => void): Unsubscribe;
8586
readonly config: Config;
8687
readonly currentUser: User | null;

docs-devsite/auth.auth.md

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface Auth
3737

3838
| Method | Description |
3939
| --- | --- |
40+
| [authStateReady()](./auth.auth.md#authauthstateready) | returns a promise that resolves immediately when the initial auth state is settled. When the promise resolves, the current user might be a valid user or <code>null</code> if the user signed out. |
4041
| [beforeAuthStateChanged(callback, onAbort)](./auth.auth.md#authbeforeauthstatechanged) | Adds a blocking callback that runs before an auth state change sets a new user. |
4142
| [onAuthStateChanged(nextOrObserver, error, completed)](./auth.auth.md#authonauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
4243
| [onIdTokenChanged(nextOrObserver, error, completed)](./auth.auth.md#authonidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
@@ -144,6 +145,19 @@ const result = await signInWithEmailAndPassword(auth, email, password);
144145

145146
```
146147

148+
## Auth.authStateReady()
149+
150+
returns a promise that resolves immediately when the initial auth state is settled. When the promise resolves, the current user might be a valid user or `null` if the user signed out.
151+
152+
<b>Signature:</b>
153+
154+
```typescript
155+
authStateReady(): Promise<void>;
156+
```
157+
<b>Returns:</b>
158+
159+
Promise&lt;void&gt;
160+
147161
## Auth.beforeAuthStateChanged()
148162

149163
Adds a blocking callback that runs before an auth state change sets a new user.

packages/auth/demo/src/index.js

+78-53
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const providersIcons = {
114114
};
115115

116116
/**
117-
* Returns the active user (i.e. currentUser or lastUser).
117+
* Returns active user (currentUser or lastUser).
118118
* @return {!firebase.User}
119119
*/
120120
function activeUser() {
@@ -126,63 +126,88 @@ function activeUser() {
126126
}
127127
}
128128

129+
/**
130+
* Blocks until there is a valid user
131+
* then returns the valid user (currentUser or lastUser).
132+
* @return {!firebase.User}
133+
*/
134+
async function getActiveUserBlocking() {
135+
const type = $('input[name=toggle-user-selection]:checked').val();
136+
if (type === 'lastUser') {
137+
return lastUser;
138+
} else {
139+
try {
140+
await auth.authStateReady();
141+
return auth.currentUser;
142+
} catch (e) {
143+
log(e);
144+
}
145+
}
146+
}
147+
129148
/**
130149
* Refreshes the current user data in the UI, displaying a user info box if
131150
* a user is signed in, or removing it.
132151
*/
133-
function refreshUserData() {
134-
if (activeUser()) {
135-
const user = activeUser();
136-
$('.profile').show();
137-
$('body').addClass('user-info-displayed');
138-
$('div.profile-email,span.profile-email').text(user.email || 'No Email');
139-
$('div.profile-phone,span.profile-phone').text(
140-
user.phoneNumber || 'No Phone'
141-
);
142-
$('div.profile-uid,span.profile-uid').text(user.uid);
143-
$('div.profile-name,span.profile-name').text(user.displayName || 'No Name');
144-
$('input.profile-name').val(user.displayName);
145-
$('input.photo-url').val(user.photoURL);
146-
if (user.photoURL != null) {
147-
let photoURL = user.photoURL;
148-
// Append size to the photo URL for Google hosted images to avoid requesting
149-
// the image with its original resolution (using more bandwidth than needed)
150-
// when it is going to be presented in smaller size.
151-
if (
152-
photoURL.indexOf('googleusercontent.com') !== -1 ||
153-
photoURL.indexOf('ggpht.com') !== -1
154-
) {
155-
photoURL = photoURL + '?sz=' + $('img.profile-image').height();
152+
async function refreshUserData() {
153+
try {
154+
let user = await getActiveUserBlocking();
155+
if (user) {
156+
$('.profile').show();
157+
$('body').addClass('user-info-displayed');
158+
$('div.profile-email,span.profile-email').text(user.email || 'No Email');
159+
$('div.profile-phone,span.profile-phone').text(
160+
user.phoneNumber || 'No Phone'
161+
);
162+
$('div.profile-uid,span.profile-uid').text(user.uid);
163+
$('div.profile-name,span.profile-name').text(
164+
user.displayName || 'No Name'
165+
);
166+
$('input.profile-name').val(user.displayName);
167+
$('input.photo-url').val(user.photoURL);
168+
if (user.photoURL != null) {
169+
let photoURL = user.photoURL;
170+
// Append size to the photo URL for Google hosted images to avoid requesting
171+
// the image with its original resolution (using more bandwidth than needed)
172+
// when it is going to be presented in smaller size.
173+
if (
174+
photoURL.indexOf('googleusercontent.com') !== -1 ||
175+
photoURL.indexOf('ggpht.com') !== -1
176+
) {
177+
photoURL = photoURL + '?sz=' + $('img.profile-image').height();
178+
}
179+
$('img.profile-image').attr('src', photoURL).show();
180+
} else {
181+
$('img.profile-image').hide();
156182
}
157-
$('img.profile-image').attr('src', photoURL).show();
158-
} else {
159-
$('img.profile-image').hide();
160-
}
161-
$('.profile-email-verified').toggle(user.emailVerified);
162-
$('.profile-email-not-verified').toggle(!user.emailVerified);
163-
$('.profile-anonymous').toggle(user.isAnonymous);
164-
// Display/Hide providers icons.
165-
$('.profile-providers').empty();
166-
if (user['providerData'] && user['providerData'].length) {
167-
const providersCount = user['providerData'].length;
168-
for (let i = 0; i < providersCount; i++) {
169-
addProviderIcon(user['providerData'][i]['providerId']);
183+
$('.profile-email-verified').toggle(user.emailVerified);
184+
$('.profile-email-not-verified').toggle(!user.emailVerified);
185+
$('.profile-anonymous').toggle(user.isAnonymous);
186+
// Display/Hide providers icons.
187+
$('.profile-providers').empty();
188+
if (user['providerData'] && user['providerData'].length) {
189+
const providersCount = user['providerData'].length;
190+
for (let i = 0; i < providersCount; i++) {
191+
addProviderIcon(user['providerData'][i]['providerId']);
192+
}
193+
}
194+
// Show enrolled second factors if available for the active user.
195+
showMultiFactorStatus(user);
196+
// Change color.
197+
if (user === auth.currentUser) {
198+
$('#user-info').removeClass('last-user');
199+
$('#user-info').addClass('current-user');
200+
} else {
201+
$('#user-info').removeClass('current-user');
202+
$('#user-info').addClass('last-user');
170203
}
171-
}
172-
// Show enrolled second factors if available for the active user.
173-
showMultiFactorStatus(user);
174-
// Change color.
175-
if (user === auth.currentUser) {
176-
$('#user-info').removeClass('last-user');
177-
$('#user-info').addClass('current-user');
178204
} else {
179-
$('#user-info').removeClass('current-user');
180-
$('#user-info').addClass('last-user');
205+
$('.profile').slideUp();
206+
$('body').removeClass('user-info-displayed');
207+
$('input.profile-data').val('');
181208
}
182-
} else {
183-
$('.profile').slideUp();
184-
$('body').removeClass('user-info-displayed');
185-
$('input.profile-data').val('');
209+
} catch (error) {
210+
log(error);
186211
}
187212
}
188213

@@ -456,7 +481,7 @@ function onReauthenticateWithEmailAndPassword() {
456481
reauthenticateWithCredential(activeUser(), credential).then(result => {
457482
logAdditionalUserInfo(result);
458483
refreshUserData();
459-
alertSuccess('User reauthenticated with email/password!');
484+
alertSuccess('User reauthenticated with email/password');
460485
}, onAuthError);
461486
}
462487

@@ -1050,7 +1075,7 @@ function onApplyActionCode() {
10501075
* or not.
10511076
*/
10521077
function getIdToken(forceRefresh) {
1053-
if (activeUser() == null) {
1078+
if (!activeUser()) {
10541079
alertError('No user logged in.');
10551080
return;
10561081
}
@@ -1075,7 +1100,7 @@ function getIdToken(forceRefresh) {
10751100
* or not
10761101
*/
10771102
function getIdTokenResult(forceRefresh) {
1078-
if (activeUser() == null) {
1103+
if (!activeUser()) {
10791104
alertError('No user logged in.');
10801105
return;
10811106
}

packages/auth/src/core/auth/auth_impl.test.ts

+107
Original file line numberDiff line numberDiff line change
@@ -786,4 +786,111 @@ describe('core/auth/auth_impl', () => {
786786
expect(auth._getRecaptchaConfig()).to.eql(cachedRecaptchaConfigOFF);
787787
});
788788
});
789+
790+
describe('AuthStateReady', () => {
791+
let user: UserInternal;
792+
let authStateChangedSpy: sinon.SinonSpy;
793+
794+
beforeEach(async () => {
795+
user = testUser(auth, 'uid');
796+
797+
authStateChangedSpy = sinon.spy(auth, 'onAuthStateChanged');
798+
799+
await auth._updateCurrentUser(null);
800+
});
801+
802+
it('immediately returns resolved promise if the user is previously logged in', async () => {
803+
await auth._updateCurrentUser(user);
804+
805+
await auth
806+
.authStateReady()
807+
.then(() => {
808+
expect(authStateChangedSpy).to.not.have.been.called;
809+
expect(auth.currentUser).to.eq(user);
810+
})
811+
.catch(error => {
812+
throw new Error(error);
813+
});
814+
});
815+
816+
it('calls onAuthStateChanged if there is no currentUser available, and returns resolved promise once the user is updated', async () => {
817+
expect(authStateChangedSpy).to.not.have.been.called;
818+
const promiseVar = auth.authStateReady();
819+
expect(authStateChangedSpy).to.be.calledOnce;
820+
821+
await auth._updateCurrentUser(user);
822+
823+
await promiseVar
824+
.then(() => {
825+
expect(auth.currentUser).to.eq(user);
826+
})
827+
.catch(error => {
828+
throw new Error(error);
829+
});
830+
831+
expect(authStateChangedSpy).to.be.calledOnce;
832+
});
833+
834+
it('resolves the promise during repeated logout', async () => {
835+
expect(authStateChangedSpy).to.not.have.been.called;
836+
const promiseVar = auth.authStateReady();
837+
expect(authStateChangedSpy).to.be.calledOnce;
838+
839+
await auth._updateCurrentUser(null);
840+
841+
await promiseVar
842+
.then(() => {
843+
expect(auth.currentUser).to.eq(null);
844+
})
845+
.catch(error => {
846+
throw new Error(error);
847+
});
848+
849+
expect(authStateChangedSpy).to.be.calledOnce;
850+
});
851+
852+
it('resolves the promise with currentUser being null during log in failure', async () => {
853+
expect(authStateChangedSpy).to.not.have.been.called;
854+
const promiseVar = auth.authStateReady();
855+
expect(authStateChangedSpy).to.be.calledOnce;
856+
857+
const auth2 = await testAuth();
858+
Object.assign(auth2.config, { apiKey: 'not-the-right-auth' });
859+
const user = testUser(auth2, 'uid');
860+
await expect(auth.updateCurrentUser(user)).to.be.rejectedWith(
861+
FirebaseError,
862+
'auth/invalid-user-token'
863+
);
864+
865+
await promiseVar
866+
.then(() => {
867+
expect(auth.currentUser).to.eq(null);
868+
})
869+
.catch(error => {
870+
throw new Error(error);
871+
});
872+
873+
expect(authStateChangedSpy).to.be.calledOnce;
874+
});
875+
876+
it('resolves the promise in a delayed user log in process', async () => {
877+
setTimeout(async () => {
878+
await auth._updateCurrentUser(user);
879+
}, 5000);
880+
881+
const promiseVar = auth.authStateReady();
882+
expect(auth.currentUser).to.eq(null);
883+
expect(authStateChangedSpy).to.be.calledOnce;
884+
885+
await setTimeout(() => {
886+
promiseVar
887+
.then(async () => {
888+
await expect(auth.currentUser).to.eq(user);
889+
})
890+
.catch(error => {
891+
throw new Error(error);
892+
});
893+
}, 10000);
894+
});
895+
});
789896
});

packages/auth/src/core/auth/auth_impl.ts

+13
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,19 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
467467
);
468468
}
469469

470+
authStateReady(): Promise<void> {
471+
return new Promise((resolve, reject) => {
472+
if (this.currentUser) {
473+
resolve();
474+
} else {
475+
const unsubscribe = this.onAuthStateChanged(() => {
476+
unsubscribe();
477+
resolve();
478+
}, reject);
479+
}
480+
});
481+
}
482+
470483
toJSON(): object {
471484
return {
472485
apiKey: this.config.apiKey,

packages/auth/src/model/public_types.ts

+6
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,12 @@ export interface Auth {
291291
error?: ErrorFn,
292292
completed?: CompleteFn
293293
): Unsubscribe;
294+
/**
295+
* returns a promise that resolves immediately when the initial
296+
* auth state is settled. When the promise resolves, the current user might be a valid user
297+
* or `null` if the user signed out.
298+
*/
299+
authStateReady(): Promise<void>;
294300
/** The currently signed-in user (or null). */
295301
readonly currentUser: User | null;
296302
/** The current emulator configuration (or null). */

0 commit comments

Comments
 (0)