Skip to content

Adding auth state ready() ax #7384

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4c92b4e
added authStateReady function
AngelAngelXie Jun 21, 2023
f9c21bb
updated Auth interface
AngelAngelXie Jun 21, 2023
4dda61b
added unit tests on authStateReady
AngelAngelXie Jun 21, 2023
06d202d
updated demo app
AngelAngelXie Jun 21, 2023
16c022c
added authStateReady to Auth interface
AngelAngelXie Jun 21, 2023
4e81db4
fixed formatting issue
AngelAngelXie Jun 21, 2023
3bbc80b
updated authStateReady calls in demo app
AngelAngelXie Jun 22, 2023
0a06aa2
fixed formatting issues
AngelAngelXie Jun 22, 2023
bb91d24
fixed demo app to incorporate authStateReady
AngelAngelXie Jun 28, 2023
cad6d23
formatted code
AngelAngelXie Jun 28, 2023
0ed6c02
removed unnecessary async keywords
AngelAngelXie Jun 29, 2023
5477946
reverted changes in onSignOut
AngelAngelXie Jun 29, 2023
041374d
clean up code
AngelAngelXie Jun 29, 2023
5f0b3d6
fixed comments
AngelAngelXie Jun 29, 2023
330b374
resolved code review comments and updated tests
AngelAngelXie Jun 30, 2023
880e0f7
changed reference doc
AngelAngelXie Jul 5, 2023
a34c7b3
resolved comments from pr
AngelAngelXie Jul 5, 2023
4647681
added changeset
AngelAngelXie Jul 5, 2023
a6be192
Update twelve-actors-enjoy.md
AngelAngelXie Jul 5, 2023
be970ef
resolved doc change check failure
AngelAngelXie Jul 5, 2023
bde0899
resolved issues with Doc Change Check
AngelAngelXie Jul 5, 2023
b8f2545
Merge branch 'adding-authStateReady()-ax' of https://github.com/fireb…
AngelAngelXie Jul 5, 2023
38bd563
clarify comments
AngelAngelXie Jul 6, 2023
d87d7ad
fixed doc change check issue
AngelAngelXie Jul 6, 2023
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
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) | return a promise that resolves immediately when the initial auth state is settled. The current user might be a valid user, or null if there is no user signed in currently. |
| [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()

return a promise that resolves immediately when the initial auth state is settled. The current user might be a valid user, or null if there is no user signed in currently.

<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
130 changes: 77 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 (i.e. currentUser or lastUser).
* @return {!firebase.User}
*/
function activeUser() {
Expand All @@ -126,63 +126,87 @@ function activeUser() {
}
}

/**
* Returns the active user after sign in (i.e. 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 +480,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 @@ -1052,7 +1076,7 @@ function onApplyActionCode() {
* or not.
*/
function getIdToken(forceRefresh) {
if (activeUser() == null) {
if (!activeUser()) {
alertError('No user logged in.');
return;
}
Expand All @@ -1077,7 +1101,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('returns resolved promise once the user is initialized to object of type UserInternal', 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 remain 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;
/**
* return a promise that resolves immediately when the initial
* auth state is settled. The current user might be a valid user,
* or null if there is no user signed in currently.
*/
authStateReady(): Promise<void>;
/** The currently signed-in user (or null). */
readonly currentUser: User | null;
/** The current emulator configuration (or null). */
Expand Down