Skip to content

Commit 4a81c74

Browse files
authored
Merge a2d6336 into 37e930c
2 parents 37e930c + a2d6336 commit 4a81c74

File tree

11 files changed

+299
-80
lines changed

11 files changed

+299
-80
lines changed

packages-exp/auth-exp/demo/src/index.js

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,17 @@ import {
5050
signInWithCustomToken,
5151
signInWithEmailAndPassword,
5252
unlink,
53-
<<<<<<< HEAD
5453
updateEmail,
5554
updatePassword,
5655
updateProfile,
57-
verifyPasswordResetCode
58-
} from '@firebase/auth-exp';
59-
=======
60-
getMultiFactorResolver,
61-
multiFactor,
62-
PhoneMultiFactorGenerator,
56+
verifyPasswordResetCode,
6357
OAuthProvider,
6458
signInWithPopup,
6559
linkWithPopup,
6660
reauthenticateWithPopup,
67-
BrowserPopupRedirectResolver
61+
browserPopupRedirectResolver
6862
} from '@firebase/auth-exp';
6963

70-
const browserPopupRedirectResolver = new BrowserPopupRedirectResolver();
71-
72-
>>>>>>> ad6dfaac7... Popup strategy implementation
7364
import { config } from './config';
7465
import {
7566
alertError,
@@ -1248,8 +1239,8 @@ function signInWithPopupRedirect(provider) {
12481239
const glob = {
12491240
signInWithPopup,
12501241
linkWithPopup,
1251-
reauthenticateWithPopup,
1252-
}
1242+
reauthenticateWithPopup
1243+
};
12531244
let action = $('input[name=popup-redirect-action]:checked').val();
12541245
let type = $('input[name=popup-redirect-type]:checked').val();
12551246
let method = null;
@@ -1305,14 +1296,18 @@ function signInWithPopupRedirect(provider) {
13051296
console.log('Provider:');
13061297
console.log(provider);
13071298
if (type == 'popup') {
1308-
glob[method](inst, provider, browserPopupRedirectResolver).then(response => {
1309-
console.log('Popup response:');
1310-
console.log(response);
1311-
alertSuccess(action + ' with ' + provider['providerId'] + ' successful!');
1312-
logAdditionalUserInfo(response);
1313-
onAuthSuccess(activeUser());
1314-
},
1315-
onAuthError);
1299+
glob[method](inst, provider, browserPopupRedirectResolver).then(
1300+
response => {
1301+
console.log('Popup response:');
1302+
console.log(response);
1303+
alertSuccess(
1304+
action + ' with ' + provider['providerId'] + ' successful!'
1305+
);
1306+
logAdditionalUserInfo(response);
1307+
onAuthSuccess(activeUser());
1308+
},
1309+
onAuthError
1310+
);
13161311
} else {
13171312
alertNotImplemented();
13181313
// try {

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

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,15 @@ describe('src/core/auth/auth_event_manager', () => {
3434
let manager: AuthEventManager;
3535

3636
function makeConsumer(
37-
filter: AuthEventType
37+
filter: AuthEventType | AuthEventType[]
3838
): sinon.SinonStubbedInstance<AuthEventConsumer> {
3939
const stub = sinon.stub({
40-
filter,
41-
isMatchingEvent: () => true,
40+
filter: Array.isArray(filter) ? filter : [filter],
4241
onAuthEvent: () => {},
43-
onError: () => {}
42+
onError: () => {},
43+
eventId: null
4444
});
4545

46-
// Make isMatchingEvent call through by default
47-
stub.isMatchingEvent.returns(true);
48-
4946
return stub;
5047
}
5148

@@ -94,21 +91,22 @@ describe('src/core/auth/auth_event_manager', () => {
9491
expect(consumer.onAuthEvent).not.to.have.been.called;
9592
});
9693

97-
it('calls isMatchingEvent with the event id', () => {
94+
it('does not call through if eventId does not match', () => {
9895
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
96+
consumer.eventId = 'not-event-id';
9997
manager.registerConsumer(consumer);
98+
10099
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP, 'event-id'));
101-
expect(consumer.isMatchingEvent).to.have.been.calledWith('event-id');
100+
expect(consumer.onAuthEvent).not.to.have.been.called;
102101
});
103102

104-
it('does not call through if isMatchingEvent is false', () => {
103+
it('does call through if eventId is null', () => {
105104
const consumer = makeConsumer(AuthEventType.REAUTH_VIA_POPUP);
105+
consumer.eventId = null;
106106
manager.registerConsumer(consumer);
107-
consumer.isMatchingEvent.returns(false);
108107

109-
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP));
110-
expect(consumer.onAuthEvent).not.to.have.been.called;
111-
expect(consumer.isMatchingEvent).to.have.been.called;
108+
manager.onEvent(makeEvent(AuthEventType.REAUTH_VIA_POPUP, 'event-id'));
109+
expect(consumer.onAuthEvent).to.have.been.called;
112110
});
113111

114112
it('converts errors into FirebaseError if the type matches', () => {
@@ -139,4 +137,59 @@ describe('src/core/auth/auth_event_manager', () => {
139137
const error = consumer.onError.getCall(0).args[0];
140138
expect(error.code).to.eq(`auth/${AuthErrorCode.INTERNAL_ERROR}`);
141139
});
140+
141+
context('redirect consumers', () => {
142+
let consumer: AuthEventConsumer;
143+
144+
beforeEach(() => {
145+
consumer = makeConsumer([
146+
AuthEventType.SIGN_IN_VIA_REDIRECT,
147+
AuthEventType.LINK_VIA_REDIRECT,
148+
AuthEventType.REAUTH_VIA_REDIRECT
149+
]);
150+
});
151+
152+
it('redirect events are queued until the future', () => {
153+
const event = makeEvent(AuthEventType.REAUTH_VIA_REDIRECT);
154+
expect(manager.onEvent(event)).to.be.true;
155+
156+
manager.registerConsumer(consumer);
157+
expect(consumer.onAuthEvent).to.have.been.calledWith(event);
158+
});
159+
160+
it('queued redirects only work for the first new consumer', () => {
161+
const event = makeEvent(AuthEventType.REAUTH_VIA_REDIRECT);
162+
expect(manager.onEvent(event)).to.be.true;
163+
164+
manager.registerConsumer(consumer);
165+
expect(consumer.onAuthEvent).to.have.been.calledWith(event);
166+
167+
const consumerB = makeConsumer(AuthEventType.REAUTH_VIA_REDIRECT);
168+
manager.registerConsumer(consumerB);
169+
expect(consumerB.onAuthEvent).not.to.have.been.called;
170+
});
171+
172+
it('does not queue a redirect event if it was handled immediately', () => {
173+
const event = makeEvent(AuthEventType.REAUTH_VIA_REDIRECT);
174+
manager.registerConsumer(consumer);
175+
176+
expect(manager.onEvent(event)).to.be.true;
177+
expect(consumer.onAuthEvent).to.have.been.calledWith(event);
178+
179+
const consumerB = makeConsumer(AuthEventType.REAUTH_VIA_REDIRECT);
180+
manager.registerConsumer(consumerB);
181+
expect(consumerB.onAuthEvent).not.to.have.been.called;
182+
});
183+
184+
it('unknown auth error prevents consumption of future redirect events', () => {
185+
const event = makeEvent(AuthEventType.UNKNOWN);
186+
event.error = { code: 'auth/no-auth-event' } as AuthEventError;
187+
expect(manager.onEvent(event)).to.be.true;
188+
expect(manager.onEvent(makeEvent(AuthEventType.SIGN_IN_VIA_REDIRECT))).to
189+
.be.false;
190+
191+
manager.registerConsumer(consumer);
192+
expect(consumer.onAuthEvent).not.to.have.been.called;
193+
});
194+
});
142195
});

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

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,43 +18,107 @@
1818
import {
1919
AuthEvent,
2020
AuthEventConsumer,
21+
AuthEventType,
2122
EventManager
2223
} from '../../model/popup_redirect';
2324
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
2425

2526
export class AuthEventManager implements EventManager {
2627
private readonly consumers: Set<AuthEventConsumer> = new Set();
28+
private queuedRedirectEvent: AuthEvent | null = null;
29+
private hasHandledPotentialRedirect = false;
2730

2831
constructor(private readonly appName: string) {}
2932

3033
registerConsumer(authEventConsumer: AuthEventConsumer): void {
3134
this.consumers.add(authEventConsumer);
35+
36+
if (
37+
this.queuedRedirectEvent &&
38+
this.isEventForConsumer(this.queuedRedirectEvent, authEventConsumer)
39+
) {
40+
this.sendToConsumer(this.queuedRedirectEvent, authEventConsumer);
41+
this.queuedRedirectEvent = null;
42+
}
3243
}
3344

3445
unregisterConsumer(authEventConsumer: AuthEventConsumer): void {
3546
this.consumers.delete(authEventConsumer);
3647
}
3748

38-
onEvent(event: AuthEvent): void {
49+
onEvent(event: AuthEvent): boolean {
50+
if (isNullRedirectEvent(event)) {
51+
// A null redirect event comes through as the first event when no pending
52+
// redirect actions are in the auth domain
53+
this.hasHandledPotentialRedirect = true;
54+
return true;
55+
}
56+
57+
let handled = false;
3958
this.consumers.forEach(consumer => {
40-
if (
41-
consumer.filter === event.type &&
42-
consumer.isMatchingEvent(event.eventId)
43-
) {
44-
if (event.error) {
45-
console.error('ERROR');
46-
const code =
47-
(event.error.code?.split('auth/')[1] as AuthErrorCode) ||
48-
AuthErrorCode.INTERNAL_ERROR;
49-
consumer.onError(
50-
AUTH_ERROR_FACTORY.create(code, {
51-
appName: this.appName
52-
})
53-
);
54-
} else {
55-
consumer.onAuthEvent(event);
56-
}
59+
if (this.isEventForConsumer(event, consumer)) {
60+
handled = true;
61+
this.sendToConsumer(event, consumer);
5762
}
5863
});
64+
65+
if (this.hasHandledPotentialRedirect || !isRedirectEvent(event.type)) {
66+
// If we've already seen a redirect before, or this is a popup event,
67+
// bail now
68+
return handled;
69+
}
70+
71+
this.hasHandledPotentialRedirect = true;
72+
73+
// If the redirect wasn't handled, hang on to it
74+
if (!handled) {
75+
this.queuedRedirectEvent = event;
76+
handled = true;
77+
}
78+
79+
return handled;
80+
}
81+
82+
private sendToConsumer(event: AuthEvent, consumer: AuthEventConsumer): void {
83+
if (event.error) {
84+
const code =
85+
(event.error.code?.split('auth/')[1] as AuthErrorCode) ||
86+
AuthErrorCode.INTERNAL_ERROR;
87+
consumer.onError(
88+
AUTH_ERROR_FACTORY.create(code, {
89+
appName: this.appName
90+
})
91+
);
92+
} else {
93+
consumer.onAuthEvent(event);
94+
}
95+
}
96+
97+
private isEventForConsumer(
98+
event: AuthEvent,
99+
consumer: AuthEventConsumer
100+
): boolean {
101+
const eventIdMatches =
102+
consumer.eventId === null ||
103+
(!!event.eventId && event.eventId === consumer.eventId);
104+
return consumer.filter.includes(event.type) && eventIdMatches;
105+
}
106+
}
107+
108+
function isNullRedirectEvent({ type, error }: AuthEvent): boolean {
109+
return (
110+
type === AuthEventType.UNKNOWN &&
111+
error?.code === `auth/${AuthErrorCode.NO_AUTH_EVENT}`
112+
);
113+
}
114+
115+
function isRedirectEvent(type: AuthEventType): boolean {
116+
switch (type) {
117+
case AuthEventType.SIGN_IN_VIA_REDIRECT:
118+
case AuthEventType.LINK_VIA_REDIRECT:
119+
case AuthEventType.REAUTH_VIA_REDIRECT:
120+
return true;
121+
default:
122+
return false;
59123
}
60124
}

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,18 @@ export abstract class AbstractPopupRedirectOperation
4343
implements AuthEventConsumer {
4444
private pendingPromise: PendingPromise | null = null;
4545
private eventManager: EventManager | null = null;
46+
readonly filter: AuthEventType[];
4647

4748
abstract eventId: string | null;
4849

4950
constructor(
5051
protected readonly auth: Auth,
51-
readonly filter: AuthEventType,
52+
filter: AuthEventType | AuthEventType[],
5253
protected readonly resolver: PopupRedirectResolver,
5354
protected user?: User
54-
) {}
55+
) {
56+
this.filter = Array.isArray(filter) ? filter : [filter];
57+
}
5558

5659
abstract onExecution(): Promise<void>;
5760

@@ -96,10 +99,6 @@ export abstract class AbstractPopupRedirectOperation
9699
this.reject(error);
97100
}
98101

99-
isMatchingEvent(eventId: string | null): boolean {
100-
return !!eventId && this.eventId === eventId;
101-
}
102-
103102
private getIdpTask(type: AuthEventType): IdpTask {
104103
switch (type) {
105104
case AuthEventType.SIGN_IN_VIA_POPUP:

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ describe('core/strategies/phone', () => {
414414
});
415415
});
416416

417-
describe("updatePhoneNumber", () => {
417+
describe('updatePhoneNumber', () => {
418418
let user: User;
419419
let reloadMock: fetch.Route;
420420
let signInMock: fetch.Route;

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '../../model/popup_redirect';
2525
import { User } from '../../model/user';
2626
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
27+
import { debugAssert } from '../util/assert';
2728
import { Delay } from '../util/delay';
2829
import { _generateEventId } from '../util/event_id';
2930
import { _getInstance } from '../util/instantiator';
@@ -100,7 +101,7 @@ class PopupOperation extends AbstractPopupRedirectOperation {
100101

101102
constructor(
102103
auth: Auth,
103-
readonly filter: AuthEventType,
104+
filter: AuthEventType,
104105
private readonly provider: externs.AuthProvider,
105106
resolver: PopupRedirectResolver,
106107
user?: User
@@ -114,11 +115,15 @@ class PopupOperation extends AbstractPopupRedirectOperation {
114115
}
115116

116117
async onExecution(): Promise<void> {
118+
debugAssert(
119+
this.filter.length === 1,
120+
'Popup operations only handle one event'
121+
);
117122
const eventId = _generateEventId();
118123
this.authWindow = await this.resolver._openPopup(
119124
this.auth,
120125
this.provider,
121-
this.filter,
126+
this.filter[0], // There's always one, see constructor
122127
eventId
123128
);
124129
this.authWindow.associatedEvent = eventId;

packages-exp/auth-exp/src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,4 @@ export { PhoneMultiFactorGenerator } from './mfa/assertions/phone';
128128
export { getMultiFactorResolver } from './mfa/mfa_resolver';
129129
export { multiFactor } from './mfa/mfa_user';
130130

131-
// TODO(samhorlbeck): This should be exported as a single const
132-
export { BrowserPopupRedirectResolver } from './platform_browser/popup_redirect';
131+
export { browserPopupRedirectResolver } from './platform_browser/popup_redirect';

0 commit comments

Comments
 (0)