Skip to content

Commit 724af8a

Browse files
committed
Profile management
1 parent e71513a commit 724af8a

File tree

9 files changed

+231
-14
lines changed

9 files changed

+231
-14
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,13 +203,13 @@ describe('core/auth/auth_impl', () => {
203203
});
204204

205205
it('onAuthStateChange does not trigger for user props change', async () => {
206-
user.refreshToken = 'hey look I changed';
206+
user.photoURL = 'blah';
207207
await auth.updateCurrentUser(user);
208208
expect(authStateCallback).not.to.have.been.called;
209209
});
210210

211211
it('onIdTokenChange triggers for user props change', async () => {
212-
user.refreshToken = 'hey look I changed';
212+
user.photoURL = 'hey look I changed';
213213
await auth.updateCurrentUser(user);
214214
expect(idTokenCallback).to.have.been.calledWith(user);
215215
});

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ export class AuthImpl implements Auth {
122122
);
123123
}
124124

125+
async _persistAndNotifyIfCurrent(user: User): Promise<void> {
126+
if (user === this.currentUser) {
127+
return this.updateCurrentUser(user);
128+
}
129+
}
130+
125131
_notifyStateListeners(): void {
126132
if (!this._isInitialized) {
127133
return;
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import * as chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
import * as sinonChai from 'sinon-chai';
22+
23+
import { UserInfo } from '@firebase/auth-types-exp';
24+
25+
// import { UserInfo } from '@firebase/auth-types-exp';
26+
import { mockEndpoint } from '../../../test/api/helper';
27+
import { testUser } from '../../../test/mock_auth';
28+
import * as fetch from '../../../test/mock_fetch';
29+
import { Endpoint } from '../../api';
30+
import { User } from '../../model/user';
31+
import { ProviderId } from '../providers';
32+
// import { ProviderId } from '../providers';
33+
import { updateProfile } from './account_info';
34+
35+
use(chaiAsPromised);
36+
use(sinonChai);
37+
38+
const PASSWORD_PROVIDER: UserInfo = {
39+
providerId: ProviderId.PASSWORD,
40+
uid: 'uid',
41+
email: 'email',
42+
displayName: 'old-name',
43+
phoneNumber: 'phone-number',
44+
photoURL: 'old-url'
45+
};
46+
47+
describe('core/user/profile', () => {
48+
let user: User;
49+
50+
beforeEach(() => {
51+
user = testUser('uid', '', true);
52+
});
53+
54+
afterEach(() => {
55+
sinon.restore();
56+
});
57+
58+
beforeEach(fetch.setUp);
59+
afterEach(fetch.tearDown);
60+
61+
it('returns immediately if profile object is empty', async () => {
62+
const ep = mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
63+
await updateProfile(user, {});
64+
expect(ep.calls).to.be.empty;
65+
});
66+
67+
it('calls the setAccountInfo endpoint', async () => {
68+
const ep =mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {});
69+
70+
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
71+
expect(ep.calls[0].request).to.eql({
72+
idToken: 'access-token',
73+
displayName: 'displayname',
74+
photoUrl: 'photo',
75+
});
76+
});
77+
78+
it('sets the fields on the user based on the response', async () => {
79+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
80+
displayName: 'response-name',
81+
photoUrl: 'response-photo',
82+
});
83+
84+
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
85+
expect(user.displayName).to.eq('response-name');
86+
expect(user.photoURL).to.eq('response-photo');
87+
});
88+
89+
it('sets the fields on the passwd provider', async () => {
90+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
91+
displayName: 'response-name',
92+
photoUrl: 'response-photo',
93+
});
94+
user.providerData = [{...PASSWORD_PROVIDER}];
95+
96+
await updateProfile(user, {displayName: 'displayname', photoURL: 'photo'});
97+
const provider = user.providerData[0];
98+
expect(provider.displayName).to.eq('response-name');
99+
expect(provider.photoURL).to.eq('response-photo');
100+
});
101+
102+
describe('notifications', () => {
103+
beforeEach(() => {
104+
user.auth.currentUser = user;
105+
});
106+
107+
it('triggers a token update if necessary', async () => {
108+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
109+
idToken: 'new-id-token',
110+
refreshToken: 'new-refresh-token',
111+
expiresIn: 300,
112+
});
113+
114+
const notifySpy = sinon.stub(user.auth, '_notifyStateListeners');
115+
await updateProfile(user, {displayName: 'd'});
116+
expect(notifySpy).to.have.been.called;
117+
});
118+
119+
it('does NOT trigger a token update if unnecessary', async () => {
120+
mockEndpoint(Endpoint.SET_ACCOUNT_INFO, {
121+
idToken: 'access-token',
122+
refreshToken: 'refresh-token',
123+
expiresIn: 300,
124+
});
125+
126+
const notifySpy = sinon.stub(user.auth, '_notifyStateListeners');
127+
await updateProfile(user, {displayName: 'd'});
128+
expect(notifySpy).not.to.have.been.called;
129+
});
130+
});
131+
});
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import * as externs from '@firebase/auth-types-exp';
19+
20+
import {
21+
updateEmailPassword as apiUpdateEmailPassword
22+
} from '../../api/account_management/email_and_password';
23+
import { updateProfile as apiUpdateProfile } from '../../api/account_management/profile';
24+
import { User } from '../../model/user';
25+
import { ProviderId } from '../providers';
26+
import { _reloadWithoutSaving } from './reload';
27+
28+
interface Profile {
29+
displayName?: string|null;
30+
photoURL?: string|null;
31+
}
32+
33+
export async function updateProfile(externUser: externs.User, {displayName, photoURL: photoUrl}: Profile): Promise<void> {
34+
if (displayName === undefined && photoUrl === undefined) {
35+
return;
36+
}
37+
38+
const user = externUser as User;
39+
const idToken = await user.getIdToken();
40+
const profileRequest = {idToken, displayName, photoUrl};
41+
const response = await apiUpdateProfile(user.auth, profileRequest);
42+
43+
user.displayName = response.displayName || null;
44+
user.photoURL = response.photoUrl || null;
45+
46+
// Update the password provider as well
47+
const passwordProvider = user.providerData.find(p => p.providerId === ProviderId.PASSWORD);
48+
if (passwordProvider) {
49+
passwordProvider.displayName = user.displayName;
50+
passwordProvider.photoURL = user.photoURL;
51+
}
52+
53+
user._updateTokensIfNecessary(response);
54+
return user.auth._persistAndNotifyIfCurrent(user);
55+
}
56+
57+
export async function updateEmail(externUser: externs.User, newEmail: string): Promise<void> {
58+
const user = externUser as User;
59+
const {auth} = user;
60+
const idToken = await user.getIdToken();
61+
const response = await apiUpdateEmailPassword(auth, {idToken, email: newEmail});
62+
user._updateTokensIfNecessary(response);
63+
64+
// To update
65+
await _reloadWithoutSaving(user);
66+
return auth._persistAndNotifyIfCurrent(user);
67+
}

packages-exp/auth-exp/src/core/user/reload.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717

1818
import * as externs from '@firebase/auth-types-exp';
1919

20-
import {
21-
getAccountInfo,
22-
ProviderUserInfo
23-
} from '../../api/account_management/account';
20+
import { getAccountInfo, ProviderUserInfo } from '../../api/account_management/account';
2421
import { User } from '../../model/user';
2522
import { ProviderId } from '../providers';
2623
import { assert } from '../util/assert';
@@ -61,7 +58,7 @@ export async function reload(externUser: externs.User): Promise<void> {
6158
// Even though the current user hasn't changed, update
6259
// current user will trigger a persistence update w/ the
6360
// new info.
64-
return user.auth.updateCurrentUser(user);
61+
return user.auth._persistAndNotifyIfCurrent(user);
6562
}
6663

6764
function mergeProviderData(

packages-exp/auth-exp/src/core/user/user_impl.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { IdTokenResult } from '@firebase/auth-types-exp';
1919

2020
import { deleteAccount } from '../../api/account_management/account';
2121
import { Auth } from '../../model/auth';
22+
import { IdTokenResponse } from '../../model/id_token';
2223
import { User } from '../../model/user';
2324
import { PersistedBlob } from '../persistence';
2425
import { ProviderId } from '../providers';
@@ -52,7 +53,6 @@ export class UserImpl implements User {
5253
// For the user object, provider is always Firebase.
5354
readonly providerId = ProviderId.FIREBASE;
5455
stsTokenManager: StsTokenManager;
55-
refreshToken = '';
5656

5757
uid: string;
5858
auth: Auth;
@@ -82,11 +82,10 @@ export class UserImpl implements User {
8282
const tokens = await this.stsTokenManager.getToken(this.auth, forceRefresh);
8383
assert(tokens, this.auth.name);
8484

85-
const { refreshToken, accessToken, wasRefreshed } = tokens;
86-
this.refreshToken = refreshToken || '';
85+
const { accessToken, wasRefreshed } = tokens;
8786

88-
if (wasRefreshed && this.auth.currentUser === this) {
89-
this.auth._notifyStateListeners();
87+
if (wasRefreshed) {
88+
await this.auth._persistAndNotifyIfCurrent(this);
9089
}
9190

9291
return accessToken;
@@ -100,6 +99,12 @@ export class UserImpl implements User {
10099
return reload(this);
101100
}
102101

102+
_updateTokensIfNecessary(response: IdTokenResponse): void {
103+
if (response.idToken && response.idToken !== this.stsTokenManager.accessToken) {
104+
this.stsTokenManager.updateFromServerResponse(response);
105+
}
106+
}
107+
103108
async delete(): Promise<void> {
104109
const idToken = await this.getIdToken();
105110
await deleteAccount(this.auth, { idToken });
@@ -122,6 +127,10 @@ export class UserImpl implements User {
122127
};
123128
}
124129

130+
get refreshToken(): string {
131+
return this.stsTokenManager.refreshToken || '';
132+
}
133+
125134
static fromPlainObject(auth: Auth, object: PersistedBlob): User {
126135
const {
127136
uid,

packages-exp/auth-exp/src/model/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface Auth extends externs.Auth {
4646
completed?: CompleteFn
4747
): Unsubscribe;
4848
_notifyStateListeners(): void;
49+
_persistAndNotifyIfCurrent(user: User): Promise<void>;
4950
}
5051

5152
export interface Dependencies {

packages-exp/auth-exp/src/model/user.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ import * as externs from '@firebase/auth-types-exp';
2020
import { PersistedBlob } from '../core/persistence';
2121
import { ProviderId } from '../core/providers';
2222
import { Auth } from './auth';
23+
import { IdTokenResponse } from './id_token';
24+
25+
type ModifiableUserInfo = {
26+
-readonly [K in keyof externs.UserInfo]: externs.UserInfo[K];
27+
};
2328

2429
export interface User extends externs.User {
2530
uid: string;
@@ -33,8 +38,9 @@ export interface User extends externs.User {
3338
refreshToken: string;
3439
emailVerified: boolean;
3540
tenantId: string | null;
36-
providerData: externs.UserInfo[];
41+
providerData: ModifiableUserInfo[];
3742
metadata: externs.UserMetadata;
43+
_updateTokensIfNecessary(response: IdTokenResponse): void;
3844

3945
getIdToken(forceRefresh?: boolean): Promise<string>;
4046
getIdTokenResult(forceRefresh?: boolean): Promise<externs.IdTokenResult>;

packages-exp/auth-types-exp/index.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,4 @@ type ActionCodeOperationType =
153153
| 'EMAIL_SIGNIN'
154154
| 'VERIFY_EMAIL'
155155
| 'VERIFY_AND_CHANGE_EMAIL'
156-
| 'REVERT_SECOND_FACTOR_ADDITION'
156+
| 'REVERT_SECOND_FACTOR_ADDITION';

0 commit comments

Comments
 (0)