Skip to content

Commit 40afc0c

Browse files
sam-gcyuchenshi
andauthored
Add an app activity listener to more-or-less complete the redirect sign in flow. (#4472)
* Initial event logic * Fixes to the partial event state * tests and non-class-based app activity listener * ... * Reshuffling, added tests * Formatting * Update packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.test.ts Co-authored-by: Yuchen Shi <[email protected]> Co-authored-by: Yuchen Shi <[email protected]>
1 parent 536b47e commit 40afc0c

File tree

7 files changed

+384
-88
lines changed

7 files changed

+384
-88
lines changed

packages-exp/auth-exp/src/platform_cordova/plugins.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
declare namespace cordova.plugins.browsertab {
2323
function isAvailable(cb: (available: boolean) => void): void;
2424
function openUrl(url: string): void;
25+
function close(): void;
2526
}
2627

2728
declare namespace cordova.InAppBrowser {

packages-exp/auth-exp/src/platform_cordova/popup_redirect/events.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { expect, use } from 'chai';
2222
import { testAuth, TestAuth } from '../../../test/helpers/mock_auth';
2323
import { AuthEvent, AuthEventType } from '../../model/popup_redirect';
2424
import {
25+
CordovaAuthEventManager,
2526
_eventFromPartialAndUrl,
2627
_generateNewEvent,
2728
_getAndRemoveEvent,
@@ -192,4 +193,40 @@ describe('platform_cordova/popup_redirect/events', () => {
192193
);
193194
});
194195
});
196+
197+
describe('_CordovaAuthEventManager', () => {
198+
let eventManager: CordovaAuthEventManager;
199+
let event: AuthEvent;
200+
201+
beforeEach(() => {
202+
eventManager = new CordovaAuthEventManager(auth);
203+
event = _generateNewEvent(auth, AuthEventType.REAUTH_VIA_REDIRECT);
204+
});
205+
206+
it('triggers passive listeners on events', done => {
207+
eventManager.addPassiveListener(actual => {
208+
expect(actual).to.eq(event);
209+
done();
210+
});
211+
212+
eventManager.onEvent(event);
213+
});
214+
215+
it('removes passive listeners properly', () => {
216+
const stub = sinon.stub();
217+
eventManager.addPassiveListener(stub);
218+
eventManager.onEvent(event);
219+
eventManager.removePassiveListener(stub);
220+
eventManager.onEvent(event);
221+
expect(stub).to.have.been.calledOnce;
222+
});
223+
224+
it('initialization resolves after first event', async () => {
225+
const promise = eventManager.initialized();
226+
eventManager.onEvent(event);
227+
await promise;
228+
229+
// If this test doesn't time out, it passed.
230+
});
231+
});
195232
});

packages-exp/auth-exp/src/platform_cordova/popup_redirect/events.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import { querystringDecode } from '@firebase/util';
19+
import { AuthEventManager } from '../../core/auth/auth_event_manager';
1920
import { AuthErrorCode } from '../../core/errors';
2021
import { PersistedBlob, Persistence } from '../../core/persistence';
2122
import {
@@ -30,6 +31,41 @@ import { browserLocalPersistence } from '../../platform_browser/persistence/loca
3031

3132
const SESSION_ID_LENGTH = 20;
3233

34+
/** Custom AuthEventManager that adds passive listeners to events */
35+
export class CordovaAuthEventManager extends AuthEventManager {
36+
private readonly passiveListeners = new Set<(e: AuthEvent) => void>();
37+
private resolveInialized!: () => void;
38+
private initPromise = new Promise<void>(resolve => {
39+
this.resolveInialized = resolve;
40+
});
41+
42+
addPassiveListener(cb: (e: AuthEvent) => void): void {
43+
this.passiveListeners.add(cb);
44+
}
45+
46+
removePassiveListener(cb: (e: AuthEvent) => void): void {
47+
this.passiveListeners.delete(cb);
48+
}
49+
50+
// In a Cordova environment, this manager can live through multiple redirect
51+
// operations
52+
resetRedirect(): void {
53+
this.queuedRedirectEvent = null;
54+
this.hasHandledPotentialRedirect = false;
55+
}
56+
57+
/** Override the onEvent method */
58+
onEvent(event: AuthEvent): boolean {
59+
this.resolveInialized();
60+
this.passiveListeners.forEach(cb => cb(event));
61+
return super.onEvent(event);
62+
}
63+
64+
async initialized(): Promise<void> {
65+
await this.initPromise;
66+
}
67+
}
68+
3369
/**
3470
* Generates a (partial) {@link AuthEvent}.
3571
*/

packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.test.ts

Lines changed: 49 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,7 @@ import {
2828
EventManager,
2929
PopupRedirectResolver
3030
} from '../../model/popup_redirect';
31-
import {
32-
CordovaAuthEventManager,
33-
cordovaPopupRedirectResolver
34-
} from './popup_redirect';
31+
import { cordovaPopupRedirectResolver } from './popup_redirect';
3532
import { GoogleAuthProvider } from '../../core/providers/google';
3633
import * as utils from './utils';
3734
import * as events from './events';
@@ -45,22 +42,52 @@ use(chaiAsPromised);
4542
use(sinonChai);
4643

4744
describe('platform_cordova/popup_redirect/popup_redirect', () => {
45+
const PACKAGE_NAME = 'my.package';
46+
const NOT_PACKAGE_NAME = 'not.my.package';
47+
const NO_EVENT_TIMER_ID = 10001;
48+
4849
let auth: TestAuth;
4950
let resolver: PopupRedirectResolver;
5051
let provider: externs.AuthProvider;
5152
let utilsStubs: sinon.SinonStubbedInstance<typeof utils>;
52-
let eventsStubs: sinon.SinonStubbedInstance<typeof events>;
53+
let eventsStubs: sinon.SinonStubbedInstance<Partial<typeof events>>;
54+
let universalLinksCb:
55+
| ((eventData: Record<string, string> | null) => unknown)
56+
| null;
57+
let tripNoEventTimer: TimerTripFn;
5358

5459
beforeEach(async () => {
5560
auth = await testAuth();
5661
resolver = new (cordovaPopupRedirectResolver as SingletonInstantiator<PopupRedirectResolver>)();
5762
provider = new GoogleAuthProvider();
5863
utilsStubs = sinon.stub(utils);
59-
eventsStubs = sinon.stub(events);
64+
eventsStubs = {
65+
_generateNewEvent: sinon.stub(events, '_generateNewEvent'),
66+
_savePartialEvent: sinon.stub(events, '_savePartialEvent'),
67+
_getAndRemoveEvent: sinon.stub(events, '_getAndRemoveEvent'),
68+
_eventFromPartialAndUrl: sinon.stub(events, '_eventFromPartialAndUrl'),
69+
_getDeepLinkFromCallback: sinon.stub(events, '_getDeepLinkFromCallback')
70+
};
71+
72+
window.universalLinks = {
73+
subscribe(_unused, cb) {
74+
universalLinksCb = cb;
75+
}
76+
};
77+
window.BuildInfo = {
78+
packageName: PACKAGE_NAME,
79+
displayName: ''
80+
};
81+
tripNoEventTimer = stubSingleTimeout(NO_EVENT_TIMER_ID);
82+
sinon.stub(window, 'clearTimeout');
6083
});
6184

6285
afterEach(() => {
6386
sinon.restore();
87+
universalLinksCb = null;
88+
const win = (window as unknown) as Record<string, unknown>;
89+
delete win.universalLinks;
90+
delete win.BuildInfo;
6491
});
6592

6693
describe('_openRedirect', () => {
@@ -73,14 +100,19 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
73100
utilsStubs._generateHandlerUrl.returns(
74101
Promise.resolve('https://localhost/__/auth/handler')
75102
);
76-
utilsStubs._performRedirect.returns(Promise.resolve());
77-
eventsStubs._generateNewEvent.returns(event);
103+
utilsStubs._performRedirect.returns(Promise.resolve({}));
104+
utilsStubs._waitForAppResume.returns(Promise.resolve());
105+
eventsStubs._generateNewEvent!.returns(event);
78106

79-
await resolver._openRedirect(
107+
const redirectPromise = resolver._openRedirect(
80108
auth,
81109
provider,
82110
AuthEventType.REAUTH_VIA_REDIRECT
83111
);
112+
// _openRedirect awaits the first event (eventManager initialized)
113+
tripNoEventTimer();
114+
await redirectPromise;
115+
84116
expect(utilsStubs._checkCordovaConfiguration).to.have.been.called;
85117
expect(utilsStubs._generateHandlerUrl).to.have.been.calledWith(
86118
auth,
@@ -90,42 +122,14 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
90122
expect(utilsStubs._performRedirect).to.have.been.calledWith(
91123
'https://localhost/__/auth/handler'
92124
);
125+
expect(utilsStubs._waitForAppResume).to.have.been.called;
93126
});
94127
});
95128

96129
describe('_initialize', () => {
97-
const NO_EVENT_TIMER_ID = 10001;
98-
const PACKAGE_NAME = 'my.package';
99-
const NOT_PACKAGE_NAME = 'not.my.package';
100-
let universalLinksCb:
101-
| ((eventData: Record<string, string> | null) => unknown)
102-
| null;
103-
let tripNoEventTimer: TimerTripFn;
104-
105-
beforeEach(() => {
106-
tripNoEventTimer = stubSingleTimeout(NO_EVENT_TIMER_ID);
107-
window.universalLinks = {
108-
subscribe(_unused, cb) {
109-
universalLinksCb = cb;
110-
}
111-
};
112-
window.BuildInfo = {
113-
packageName: PACKAGE_NAME,
114-
displayName: ''
115-
};
116-
sinon.stub(window, 'clearTimeout');
117-
});
118-
119-
afterEach(() => {
120-
universalLinksCb = null;
121-
const win = (window as unknown) as Record<string, unknown>;
122-
delete win.universalLinks;
123-
delete win.BuildInfo;
124-
});
125-
126130
function event(manager: EventManager): Promise<AuthEvent> {
127131
return new Promise(resolve => {
128-
(manager as CordovaAuthEventManager).addPassiveListener(resolve);
132+
(manager as events.CordovaAuthEventManager).addPassiveListener(resolve);
129133
});
130134
}
131135

@@ -177,8 +181,8 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
177181

178182
it('signals no event if partial parse turns up null', async () => {
179183
const promise = event(await resolver._initialize(auth));
180-
eventsStubs._eventFromPartialAndUrl.returns(null);
181-
eventsStubs._getAndRemoveEvent.returns(
184+
eventsStubs._eventFromPartialAndUrl!.returns(null);
185+
eventsStubs._getAndRemoveEvent!.returns(
182186
Promise.resolve({
183187
type: AuthEventType.REAUTH_VIA_REDIRECT
184188
} as AuthEvent)
@@ -204,14 +208,14 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
204208
type: AuthEventType.REAUTH_VIA_REDIRECT,
205209
postBody: 'foo'
206210
};
207-
eventsStubs._getAndRemoveEvent.returns(
211+
eventsStubs._getAndRemoveEvent!.returns(
208212
Promise.resolve({
209213
type: AuthEventType.REAUTH_VIA_REDIRECT
210214
} as AuthEvent)
211215
);
212216

213217
const promise = event(await resolver._initialize(auth));
214-
eventsStubs._eventFromPartialAndUrl.returns(finalEvent as AuthEvent);
218+
eventsStubs._eventFromPartialAndUrl!.returns(finalEvent as AuthEvent);
215219
await universalLinksCb!({ url: 'foo-bar' });
216220
expect(await promise).to.eq(finalEvent);
217221
expect(events._eventFromPartialAndUrl).to.have.been.calledWith(
@@ -241,14 +245,14 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
241245
type: AuthEventType.REAUTH_VIA_REDIRECT,
242246
postBody: 'foo'
243247
};
244-
eventsStubs._getAndRemoveEvent.returns(
248+
eventsStubs._getAndRemoveEvent!.returns(
245249
Promise.resolve({
246250
type: AuthEventType.REAUTH_VIA_REDIRECT
247251
} as AuthEvent)
248252
);
249253

250254
const promise = event(await resolver._initialize(auth));
251-
eventsStubs._eventFromPartialAndUrl.returns(finalEvent as AuthEvent);
255+
eventsStubs._eventFromPartialAndUrl!.returns(finalEvent as AuthEvent);
252256
handleOpenUrl(`${PACKAGE_NAME}://foo`);
253257
expect(await promise).to.eq(finalEvent);
254258
expect(events._eventFromPartialAndUrl).to.have.been.calledWith(

packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.ts

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ import { AuthErrorCode } from '../../core/errors';
3030
import {
3131
_checkCordovaConfiguration,
3232
_generateHandlerUrl,
33-
_performRedirect
33+
_performRedirect,
34+
_waitForAppResume
3435
} from './utils';
3536
import {
37+
CordovaAuthEventManager,
3638
_eventFromPartialAndUrl,
3739
_generateNewEvent,
38-
_getAndRemoveEvent
40+
_getAndRemoveEvent,
41+
_savePartialEvent
3942
} from './events';
4043
import { AuthEventManager } from '../../core/auth/auth_event_manager';
4144

@@ -45,37 +48,11 @@ import { AuthEventManager } from '../../core/auth/auth_event_manager';
4548
*/
4649
const INITIAL_EVENT_TIMEOUT_MS = 500;
4750

48-
/** Custom AuthEventManager that adds passive listeners to events */
49-
export class CordovaAuthEventManager extends AuthEventManager {
50-
private readonly passiveListeners = new Set<(e: AuthEvent) => void>();
51-
52-
addPassiveListener(cb: (e: AuthEvent) => void): void {
53-
this.passiveListeners.add(cb);
54-
}
55-
56-
removePassiveListener(cb: (e: AuthEvent) => void): void {
57-
this.passiveListeners.delete(cb);
58-
}
59-
60-
// In a Cordova environment, this manager can live through multiple redirect
61-
// operations
62-
resetRedirect(): void {
63-
this.queuedRedirectEvent = null;
64-
this.hasHandledPotentialRedirect = false;
65-
}
66-
67-
/** Override the onEvent method */
68-
onEvent(event: AuthEvent): boolean {
69-
this.passiveListeners.forEach(cb => cb(event));
70-
return super.onEvent(event);
71-
}
72-
}
73-
7451
class CordovaPopupRedirectResolver implements PopupRedirectResolver {
7552
readonly _redirectPersistence = browserSessionPersistence;
7653
private readonly eventManagers = new Map<string, CordovaAuthEventManager>();
7754

78-
_completeRedirectFn: () => Promise<null> = async () => null;
55+
_completeRedirectFn = async (): Promise<null> => null;
7956

8057
async _initialize(auth: Auth): Promise<CordovaAuthEventManager> {
8158
const key = auth._key();
@@ -99,9 +76,15 @@ class CordovaPopupRedirectResolver implements PopupRedirectResolver {
9976
eventId?: string
10077
): Promise<void> {
10178
_checkCordovaConfiguration(auth);
79+
const manager = await this._initialize(auth);
80+
await manager.initialized();
81+
manager.resetRedirect();
82+
10283
const event = _generateNewEvent(auth, authType, eventId);
84+
await _savePartialEvent(auth, event);
10385
const url = await _generateHandlerUrl(auth, event, provider);
104-
await _performRedirect(url);
86+
const iabRef = await _performRedirect(url);
87+
return _waitForAppResume(auth, manager, iabRef);
10588
}
10689

10790
_isIframeWebStorageSupported(
@@ -136,7 +119,10 @@ class CordovaPopupRedirectResolver implements PopupRedirectResolver {
136119
};
137120

138121
// Universal links subscriber doesn't exist for iOS, so we need to check
139-
if (typeof universalLinks.subscribe === 'function') {
122+
if (
123+
typeof universalLinks !== 'undefined' &&
124+
typeof universalLinks.subscribe === 'function'
125+
) {
140126
universalLinks.subscribe(null, universalLinksCb);
141127
}
142128

@@ -146,12 +132,9 @@ class CordovaPopupRedirectResolver implements PopupRedirectResolver {
146132
// https://github.com/EddyVerbruggen/Custom-URL-scheme
147133
// Do not overwrite the existing developer's URL handler.
148134
const existingHandleOpenUrl = window.handleOpenUrl;
135+
const packagePrefix = `${BuildInfo.packageName.toLowerCase()}://`;
149136
window.handleOpenUrl = async url => {
150-
if (
151-
url
152-
.toLowerCase()
153-
.startsWith(`${BuildInfo.packageName.toLowerCase()}://`)
154-
) {
137+
if (url.toLowerCase().startsWith(packagePrefix)) {
155138
// We want this intentionally to float
156139
// eslint-disable-next-line @typescript-eslint/no-floating-promises
157140
universalLinksCb({ url });

0 commit comments

Comments
 (0)