Skip to content

Commit

Permalink
Adding auth state ready() ax (#7384)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AngelAngelXie committed Jul 18, 2023
1 parent 3f3f536 commit 8e15973
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 53 deletions.
6 changes: 6 additions & 0 deletions .changeset/twelve-actors-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@firebase/auth': minor
'firebase': minor
---

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.
1 change: 1 addition & 0 deletions common/api-review/auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function applyActionCode(auth: Auth, oobCode: string): Promise<void>;
// @public
export interface Auth {
readonly app: FirebaseApp;
authStateReady(): Promise<void>;
beforeAuthStateChanged(callback: (user: User | null) => void | Promise<void>, onAbort?: () => void): Unsubscribe;
readonly config: Config;
readonly currentUser: User | null;
Expand Down
14 changes: 14 additions & 0 deletions docs-devsite/auth.auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export interface Auth

| Method | Description |
| --- | --- |
| [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. |
| [beforeAuthStateChanged(callback, onAbort)](./auth.auth.md#authbeforeauthstatechanged) | Adds a blocking callback that runs before an auth state change sets a new user. |
| [onAuthStateChanged(nextOrObserver, error, completed)](./auth.auth.md#authonauthstatechanged) | Adds an observer for changes to the user's sign-in state. |
| [onIdTokenChanged(nextOrObserver, error, completed)](./auth.auth.md#authonidtokenchanged) | Adds an observer for changes to the signed-in user's ID token. |
Expand Down Expand Up @@ -144,6 +145,19 @@ const result = await signInWithEmailAndPassword(auth, email, password);

```

## Auth.authStateReady()

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.

<b>Signature:</b>

```typescript
authStateReady(): Promise<void>;
```
<b>Returns:</b>

Promise&lt;void&gt;

## Auth.beforeAuthStateChanged()

Adds a blocking callback that runs before an auth state change sets a new user.
Expand Down
131 changes: 78 additions & 53 deletions packages/auth/demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const providersIcons = {
};

/**
* Returns the active user (i.e. currentUser or lastUser).
* Returns active user (currentUser or lastUser).
* @return {!firebase.User}
*/
function activeUser() {
Expand All @@ -126,63 +126,88 @@ function activeUser() {
}
}

/**
* Blocks until there is a valid user
* then returns the valid user (currentUser or lastUser).
* @return {!firebase.User}
*/
async function getActiveUserBlocking() {
const type = $('input[name=toggle-user-selection]:checked').val();
if (type === 'lastUser') {
return lastUser;
} else {
try {
await auth.authStateReady();
return auth.currentUser;
} catch (e) {
log(e);
}
}
}

/**
* Refreshes the current user data in the UI, displaying a user info box if
* a user is signed in, or removing it.
*/
function refreshUserData() {
if (activeUser()) {
const user = activeUser();
$('.profile').show();
$('body').addClass('user-info-displayed');
$('div.profile-email,span.profile-email').text(user.email || 'No Email');
$('div.profile-phone,span.profile-phone').text(
user.phoneNumber || 'No Phone'
);
$('div.profile-uid,span.profile-uid').text(user.uid);
$('div.profile-name,span.profile-name').text(user.displayName || 'No Name');
$('input.profile-name').val(user.displayName);
$('input.photo-url').val(user.photoURL);
if (user.photoURL != null) {
let photoURL = user.photoURL;
// Append size to the photo URL for Google hosted images to avoid requesting
// the image with its original resolution (using more bandwidth than needed)
// when it is going to be presented in smaller size.
if (
photoURL.indexOf('googleusercontent.com') !== -1 ||
photoURL.indexOf('ggpht.com') !== -1
) {
photoURL = photoURL + '?sz=' + $('img.profile-image').height();
async function refreshUserData() {
try {
let user = await getActiveUserBlocking();
if (user) {
$('.profile').show();
$('body').addClass('user-info-displayed');
$('div.profile-email,span.profile-email').text(user.email || 'No Email');
$('div.profile-phone,span.profile-phone').text(
user.phoneNumber || 'No Phone'
);
$('div.profile-uid,span.profile-uid').text(user.uid);
$('div.profile-name,span.profile-name').text(
user.displayName || 'No Name'
);
$('input.profile-name').val(user.displayName);
$('input.photo-url').val(user.photoURL);
if (user.photoURL != null) {
let photoURL = user.photoURL;
// Append size to the photo URL for Google hosted images to avoid requesting
// the image with its original resolution (using more bandwidth than needed)
// when it is going to be presented in smaller size.
if (
photoURL.indexOf('googleusercontent.com') !== -1 ||
photoURL.indexOf('ggpht.com') !== -1
) {
photoURL = photoURL + '?sz=' + $('img.profile-image').height();
}
$('img.profile-image').attr('src', photoURL).show();
} else {
$('img.profile-image').hide();
}
$('img.profile-image').attr('src', photoURL).show();
} else {
$('img.profile-image').hide();
}
$('.profile-email-verified').toggle(user.emailVerified);
$('.profile-email-not-verified').toggle(!user.emailVerified);
$('.profile-anonymous').toggle(user.isAnonymous);
// Display/Hide providers icons.
$('.profile-providers').empty();
if (user['providerData'] && user['providerData'].length) {
const providersCount = user['providerData'].length;
for (let i = 0; i < providersCount; i++) {
addProviderIcon(user['providerData'][i]['providerId']);
$('.profile-email-verified').toggle(user.emailVerified);
$('.profile-email-not-verified').toggle(!user.emailVerified);
$('.profile-anonymous').toggle(user.isAnonymous);
// Display/Hide providers icons.
$('.profile-providers').empty();
if (user['providerData'] && user['providerData'].length) {
const providersCount = user['providerData'].length;
for (let i = 0; i < providersCount; i++) {
addProviderIcon(user['providerData'][i]['providerId']);
}
}
// Show enrolled second factors if available for the active user.
showMultiFactorStatus(user);
// Change color.
if (user === auth.currentUser) {
$('#user-info').removeClass('last-user');
$('#user-info').addClass('current-user');
} else {
$('#user-info').removeClass('current-user');
$('#user-info').addClass('last-user');
}
}
// Show enrolled second factors if available for the active user.
showMultiFactorStatus(user);
// Change color.
if (user === auth.currentUser) {
$('#user-info').removeClass('last-user');
$('#user-info').addClass('current-user');
} else {
$('#user-info').removeClass('current-user');
$('#user-info').addClass('last-user');
$('.profile').slideUp();
$('body').removeClass('user-info-displayed');
$('input.profile-data').val('');
}
} else {
$('.profile').slideUp();
$('body').removeClass('user-info-displayed');
$('input.profile-data').val('');
} catch (error) {
log(error);
}
}

Expand Down Expand Up @@ -456,7 +481,7 @@ function onReauthenticateWithEmailAndPassword() {
reauthenticateWithCredential(activeUser(), credential).then(result => {
logAdditionalUserInfo(result);
refreshUserData();
alertSuccess('User reauthenticated with email/password!');
alertSuccess('User reauthenticated with email/password');
}, onAuthError);
}

Expand Down Expand Up @@ -1050,7 +1075,7 @@ function onApplyActionCode() {
* or not.
*/
function getIdToken(forceRefresh) {
if (activeUser() == null) {
if (!activeUser()) {
alertError('No user logged in.');
return;
}
Expand All @@ -1075,7 +1100,7 @@ function getIdToken(forceRefresh) {
* or not
*/
function getIdTokenResult(forceRefresh) {
if (activeUser() == null) {
if (!activeUser()) {
alertError('No user logged in.');
return;
}
Expand Down
107 changes: 107 additions & 0 deletions packages/auth/src/core/auth/auth_impl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,4 +786,111 @@ describe('core/auth/auth_impl', () => {
expect(auth._getRecaptchaConfig()).to.eql(cachedRecaptchaConfigOFF);
});
});

describe('AuthStateReady', () => {
let user: UserInternal;
let authStateChangedSpy: sinon.SinonSpy;

beforeEach(async () => {
user = testUser(auth, 'uid');

authStateChangedSpy = sinon.spy(auth, 'onAuthStateChanged');

await auth._updateCurrentUser(null);
});

it('immediately returns resolved promise if the user is previously logged in', async () => {
await auth._updateCurrentUser(user);

await auth
.authStateReady()
.then(() => {
expect(authStateChangedSpy).to.not.have.been.called;
expect(auth.currentUser).to.eq(user);
})
.catch(error => {
throw new Error(error);
});
});

it('calls onAuthStateChanged if there is no currentUser available, and returns resolved promise once the user is updated', async () => {
expect(authStateChangedSpy).to.not.have.been.called;
const promiseVar = auth.authStateReady();
expect(authStateChangedSpy).to.be.calledOnce;

await auth._updateCurrentUser(user);

await promiseVar
.then(() => {
expect(auth.currentUser).to.eq(user);
})
.catch(error => {
throw new Error(error);
});

expect(authStateChangedSpy).to.be.calledOnce;
});

it('resolves the promise during repeated logout', async () => {
expect(authStateChangedSpy).to.not.have.been.called;
const promiseVar = auth.authStateReady();
expect(authStateChangedSpy).to.be.calledOnce;

await auth._updateCurrentUser(null);

await promiseVar
.then(() => {
expect(auth.currentUser).to.eq(null);
})
.catch(error => {
throw new Error(error);
});

expect(authStateChangedSpy).to.be.calledOnce;
});

it('resolves the promise with currentUser being null during log in failure', async () => {
expect(authStateChangedSpy).to.not.have.been.called;
const promiseVar = auth.authStateReady();
expect(authStateChangedSpy).to.be.calledOnce;

const auth2 = await testAuth();
Object.assign(auth2.config, { apiKey: 'not-the-right-auth' });
const user = testUser(auth2, 'uid');
await expect(auth.updateCurrentUser(user)).to.be.rejectedWith(
FirebaseError,
'auth/invalid-user-token'
);

await promiseVar
.then(() => {
expect(auth.currentUser).to.eq(null);
})
.catch(error => {
throw new Error(error);
});

expect(authStateChangedSpy).to.be.calledOnce;
});

it('resolves the promise in a delayed user log in process', async () => {
setTimeout(async () => {
await auth._updateCurrentUser(user);
}, 5000);

const promiseVar = auth.authStateReady();
expect(auth.currentUser).to.eq(null);
expect(authStateChangedSpy).to.be.calledOnce;

await setTimeout(() => {
promiseVar
.then(async () => {
await expect(auth.currentUser).to.eq(user);
})
.catch(error => {
throw new Error(error);
});
}, 10000);
});
});
});
13 changes: 13 additions & 0 deletions packages/auth/src/core/auth/auth_impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,19 @@ export class AuthImpl implements AuthInternal, _FirebaseService {
);
}

authStateReady(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.currentUser) {
resolve();
} else {
const unsubscribe = this.onAuthStateChanged(() => {
unsubscribe();
resolve();
}, reject);
}
});
}

toJSON(): object {
return {
apiKey: this.config.apiKey,
Expand Down
6 changes: 6 additions & 0 deletions packages/auth/src/model/public_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,12 @@ export interface Auth {
error?: ErrorFn,
completed?: CompleteFn
): Unsubscribe;
/**
* 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.
*/
authStateReady(): Promise<void>;
/** The currently signed-in user (or null). */
readonly currentUser: User | null;
/** The current emulator configuration (or null). */
Expand Down

0 comments on commit 8e15973

Please sign in to comment.