Skip to content

Commit e7ada09

Browse files
committed
Refactor popup code to have abstract base
1 parent 5ee8a36 commit e7ada09

File tree

3 files changed

+164
-117
lines changed

3 files changed

+164
-117
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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+
import { FirebaseError } from '@firebase/util';
20+
21+
import { Auth } from '../../model/auth';
22+
import {
23+
AuthEvent, AuthEventConsumer, AuthEventType, EventManager, PopupRedirectResolver
24+
} from '../../model/popup_redirect';
25+
import { User, UserCredential } from '../../model/user';
26+
import { AuthErrorCode } from '../errors';
27+
import { fail } from '../util/assert';
28+
import { _link, _reauth, _signIn, IdpTask, IdpTaskParams } from './idp';
29+
30+
interface PendingPromise {
31+
resolve: (cred: UserCredential) => void;
32+
reject: (error: Error) => void;
33+
}
34+
35+
/**
36+
* Popup event manager. Handles the popup's entire lifecycle; listens to auth
37+
* events
38+
*/
39+
export abstract class AbstractPopupRedirectAction implements AuthEventConsumer {
40+
private pendingPromise: PendingPromise | null = null;
41+
private eventManager: EventManager | null = null;
42+
43+
abstract eventId: string|null;
44+
45+
constructor(
46+
protected readonly auth: Auth,
47+
readonly filter: AuthEventType|'redirect',
48+
protected readonly resolver: PopupRedirectResolver,
49+
protected user?: User
50+
) {
51+
}
52+
53+
protected abstract onExecution(): Promise<void>;
54+
55+
execute(): Promise<UserCredential> {
56+
return new Promise<UserCredential>(async (resolve, reject) => {
57+
this.pendingPromise = { resolve, reject };
58+
59+
this.eventManager = await this.resolver._initialize(this.auth);
60+
await this.onExecution();
61+
this.eventManager.registerConsumer(this);
62+
});
63+
}
64+
65+
async onAuthEvent(event: AuthEvent): Promise<void> {
66+
const { urlResponse, sessionId, postBody, tenantId, error, type } = event;
67+
if (error) {
68+
this.broadcastResult(null, error);
69+
return;
70+
}
71+
72+
const params: IdpTaskParams = {
73+
auth: this.auth,
74+
requestUri: urlResponse!,
75+
sessionId: sessionId!,
76+
tenantId: tenantId || undefined,
77+
postBody: postBody || undefined,
78+
user: this.user
79+
};
80+
81+
try {
82+
this.broadcastResult(await this.getIdpTask(type)(params));
83+
} catch (e) {
84+
this.broadcastResult(null, e);
85+
}
86+
}
87+
88+
onError(error: FirebaseError): void {
89+
this.broadcastResult(null, error);
90+
}
91+
92+
private getIdpTask(type: AuthEventType): IdpTask {
93+
switch(type) {
94+
case AuthEventType.SIGN_IN_VIA_POPUP:
95+
case AuthEventType.SIGN_IN_VIA_REDIRECT:
96+
return _signIn;
97+
case AuthEventType.LINK_VIA_POPUP:
98+
case AuthEventType.LINK_VIA_REDIRECT:
99+
return _link;
100+
case AuthEventType.REAUTH_VIA_POPUP:
101+
case AuthEventType.REAUTH_VIA_REDIRECT:
102+
return _reauth;
103+
default:
104+
fail(this.auth.name, AuthErrorCode.INTERNAL_ERROR);
105+
}
106+
}
107+
108+
protected broadcastResult(cred: UserCredential | null, error?: Error): void {
109+
if (this.pendingPromise) {
110+
if (error) {
111+
this.pendingPromise.reject(error);
112+
} else {
113+
this.pendingPromise.resolve(cred!);
114+
}
115+
}
116+
117+
this.pendingPromise = null;
118+
if (this.eventManager) {
119+
this.eventManager.unregisterConsumer(this);
120+
}
121+
this.cleanUp();
122+
}
123+
124+
protected abstract cleanUp(): void;
125+
}

packages-exp/auth-exp/src/core/strategies/popup.test.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ import { delay } from '../../../test/delay';
2727
import { testAuth, testUser } from '../../../test/mock_auth';
2828
import { stubTimeouts, TimerMap } from '../../../test/timeout_stub';
2929
import { Auth } from '../../model/auth';
30-
import {
31-
AuthEvent,
32-
AuthEventType,
33-
PopupRedirectResolver
34-
} from '../../model/popup_redirect';
30+
import { AuthEvent, AuthEventType, PopupRedirectResolver } from '../../model/popup_redirect';
3531
import { User } from '../../model/user';
3632
import { AuthEventManager } from '../auth/auth_event_manager';
3733
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
@@ -41,11 +37,8 @@ import * as eid from '../util/event_id';
4137
import { AuthPopup } from '../util/popup';
4238
import * as idpTasks from './idp';
4339
import {
44-
_AUTH_EVENT_TIMEOUT,
45-
_POLL_WINDOW_CLOSE_TIMEOUT,
46-
linkWithPopup,
47-
reauthenticateWithPopup,
48-
signInWithPopup
40+
_AUTH_EVENT_TIMEOUT, _POLL_WINDOW_CLOSE_TIMEOUT, linkWithPopup, reauthenticateWithPopup,
41+
signInWithPopup
4942
} from './popup';
5043

5144
use(sinonChai);
@@ -81,7 +74,8 @@ describe('src/core/strategies/popup', () => {
8174
provider = new OAuthProvider(ProviderId.GOOGLE);
8275
resolver = {
8376
_initialize: async () => eventManager,
84-
_openPopup: async () => authPopup
77+
_openPopup: async () => authPopup,
78+
_openRedirect: () => new Promise(() => {}),
8579
};
8680
idpStubs = sinon.stub(idpTasks);
8781
sinon.stub(eid, '_generateEventId').returns(MATCHING_EVENT_ID);

packages-exp/auth-exp/src/core/strategies/popup.ts

Lines changed: 34 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -16,51 +16,36 @@
1616
*/
1717

1818
import * as externs from '@firebase/auth-types-exp';
19-
import { FirebaseError } from '@firebase/util';
2019

2120
import { Auth } from '../../model/auth';
22-
import {
23-
AuthEvent,
24-
AuthEventConsumer,
25-
AuthEventType,
26-
EventManager,
27-
PopupRedirectResolver
28-
} from '../../model/popup_redirect';
21+
import { AuthEventType, PopupRedirectResolver } from '../../model/popup_redirect';
2922
import { User, UserCredential } from '../../model/user';
3023
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
3124
import { Delay } from '../util/delay';
3225
import { _generateEventId } from '../util/event_id';
26+
import { _getInstance } from '../util/instantiator';
3327
import { AuthPopup } from '../util/popup';
34-
import { _link, _reauth, _signIn, IdpTask, IdpTaskParams } from './idp';
28+
import { AbstractPopupRedirectAction } from './abstract_popup_redirect_action';
3529

3630
// The event timeout is the same on mobile and desktop, no need for Delay.
3731
export const _AUTH_EVENT_TIMEOUT = 2020;
3832
export const _POLL_WINDOW_CLOSE_TIMEOUT = new Delay(2000, 10000);
3933

40-
interface PendingPromise {
41-
resolve: (cred: UserCredential) => void;
42-
reject: (error: Error) => void;
43-
}
44-
4534
export async function signInWithPopup(
4635
authExtern: externs.Auth,
4736
provider: externs.AuthProvider,
4837
resolverExtern: externs.PopupRedirectResolver
4938
): Promise<UserCredential> {
5039
const auth = authExtern as Auth;
51-
const resolver = resolverExtern as PopupRedirectResolver;
40+
const resolver: PopupRedirectResolver = _getInstance(resolverExtern);
5241

5342
const action = new PopupAction(
5443
auth,
5544
AuthEventType.SIGN_IN_VIA_POPUP,
56-
_signIn,
5745
provider,
5846
resolver
5947
);
60-
const cred = await action.execute();
61-
62-
await auth.updateCurrentUser(cred.user);
63-
return cred;
48+
return action.execute();
6449
}
6550

6651
export async function reauthenticateWithPopup(
@@ -69,12 +54,11 @@ export async function reauthenticateWithPopup(
6954
resolverExtern: externs.PopupRedirectResolver
7055
): Promise<UserCredential> {
7156
const user = userExtern as User;
72-
const resolver = resolverExtern as PopupRedirectResolver;
57+
const resolver: PopupRedirectResolver = _getInstance(resolverExtern);
7358

7459
const action = new PopupAction(
7560
user.auth,
7661
AuthEventType.REAUTH_VIA_POPUP,
77-
_reauth,
7862
provider,
7963
resolver,
8064
user
@@ -88,12 +72,11 @@ export async function linkWithPopup(
8872
resolverExtern: externs.PopupRedirectResolver
8973
): Promise<UserCredential> {
9074
const user = userExtern as User;
91-
const resolver = resolverExtern as PopupRedirectResolver;
75+
const resolver: PopupRedirectResolver = _getInstance(resolverExtern);
9276

9377
const action = new PopupAction(
9478
user.auth,
9579
AuthEventType.LINK_VIA_POPUP,
96-
_link,
9780
provider,
9881
resolver,
9982
user
@@ -105,121 +88,66 @@ export async function linkWithPopup(
10588
* Popup event manager. Handles the popup's entire lifecycle; listens to auth
10689
* events
10790
*/
108-
class PopupAction implements AuthEventConsumer {
91+
class PopupAction extends AbstractPopupRedirectAction {
10992
// Only one popup is ever shown at once. The lifecycle of the current popup
11093
// can be managed / cancelled by the constructor.
11194
private static currentPopupAction: PopupAction | null = null;
112-
private pendingPromise: PendingPromise | null = null;
11395
private authWindow: AuthPopup | null = null;
11496
private pollId: number | null = null;
115-
private eventManager: EventManager | null = null;
11697

11798
constructor(
118-
private readonly auth: Auth,
99+
auth: Auth,
119100
readonly filter: AuthEventType,
120-
private readonly idpTask: IdpTask,
121101
private readonly provider: externs.AuthProvider,
122-
private readonly resolver: PopupRedirectResolver,
123-
private readonly user?: User
102+
resolver: PopupRedirectResolver,
103+
user?: User
124104
) {
105+
super(auth, filter, resolver, user)
125106
if (PopupAction.currentPopupAction) {
126107
PopupAction.currentPopupAction.cancel();
127108
}
128109

129110
PopupAction.currentPopupAction = this;
130111
}
131112

132-
execute(): Promise<UserCredential> {
133-
return new Promise<UserCredential>(async (resolve, reject) => {
134-
this.pendingPromise = { resolve, reject };
135-
136-
this.eventManager = await this.resolver._initialize(this.auth);
137-
const eventId = _generateEventId();
138-
this.authWindow = await this.resolver._openPopup(
139-
this.auth,
140-
this.provider,
141-
this.filter,
142-
eventId
143-
);
144-
this.authWindow.associatedEvent = eventId;
145-
146-
this.eventManager.registerConsumer(this);
147-
148-
// Handle user closure. Notice this does *not* use await
149-
this.pollUserCancellation(this.auth.name);
150-
});
151-
}
152-
153-
isMatchingEvent(eventId: string | null): boolean {
154-
return !!eventId && this.authWindow?.associatedEvent === eventId;
113+
protected async onExecution(): Promise<void> {
114+
const eventId = _generateEventId();
115+
this.authWindow = await this.resolver._openPopup(
116+
this.auth,
117+
this.provider,
118+
this.filter,
119+
eventId
120+
);
121+
this.authWindow.associatedEvent = eventId;
122+
123+
// Handle user closure. Notice this does *not* use await
124+
this.pollUserCancellation(this.auth.name);
155125
}
156126

157-
async onAuthEvent(event: AuthEvent): Promise<void> {
158-
const { urlResponse, sessionId, postBody, tenantId, error } = event;
159-
if (error) {
160-
this.broadcastResult(null, error);
161-
return;
162-
}
163-
164-
const params: IdpTaskParams = {
165-
auth: this.auth,
166-
requestUri: urlResponse!,
167-
sessionId: sessionId!,
168-
tenantId: tenantId || undefined,
169-
postBody: postBody || undefined,
170-
user: this.user
171-
};
172-
173-
try {
174-
this.broadcastResult(await this.idpTask(params));
175-
} catch (e) {
176-
this.broadcastResult(null, e);
177-
}
178-
}
179-
180-
onError(error: FirebaseError): void {
181-
this.broadcastResult(null, error);
127+
get eventId(): string | null {
128+
return this.authWindow?.associatedEvent || null;
182129
}
183130

184131
cancel(): void {
185-
if (this.pendingPromise) {
186-
// There was already a pending promise. Expire it.
187-
this.broadcastResult(
188-
null,
189-
AUTH_ERROR_FACTORY.create(AuthErrorCode.EXPIRED_POPUP_REQUEST, {
190-
appName: this.auth.name
191-
})
192-
);
193-
}
132+
this.broadcastResult(
133+
null,
134+
AUTH_ERROR_FACTORY.create(AuthErrorCode.EXPIRED_POPUP_REQUEST, {
135+
appName: this.auth.name
136+
})
137+
);
194138
}
195139

196-
private broadcastResult(cred: UserCredential | null, error?: Error): void {
140+
protected cleanUp(): void {
197141
if (this.authWindow) {
198142
this.authWindow.close();
199143
}
200144

201145
if (this.pollId) {
202146
window.clearTimeout(this.pollId);
203147
}
204-
205-
if (this.pendingPromise) {
206-
if (error) {
207-
this.pendingPromise.reject(error);
208-
} else {
209-
this.pendingPromise.resolve(cred!);
210-
}
211-
}
212-
213-
this.cleanUp();
214-
}
215-
216-
private cleanUp(): void {
148+
217149
this.authWindow = null;
218-
this.pendingPromise = null;
219150
this.pollId = null;
220-
if (this.eventManager) {
221-
this.eventManager.unregisterConsumer(this);
222-
}
223151
PopupAction.currentPopupAction = null;
224152
}
225153

0 commit comments

Comments
 (0)