Skip to content

Commit 7c17826

Browse files
authored
Add app callback listeners to the cordova redirect handler (#4449)
* Add callback listeners to the cordova redirect handler * Formatting * PR feedback * Formatting
1 parent 9384255 commit 7c17826

File tree

6 files changed

+333
-11
lines changed

6 files changed

+333
-11
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ const EVENT_DUPLICATION_CACHE_DURATION_MS = 10 * 60 * 1000;
3232
export class AuthEventManager implements EventManager {
3333
private readonly cachedEventUids: Set<string> = new Set();
3434
private readonly consumers: Set<AuthEventConsumer> = new Set();
35-
private queuedRedirectEvent: AuthEvent | null = null;
36-
private hasHandledPotentialRedirect = false;
35+
protected queuedRedirectEvent: AuthEvent | null = null;
36+
protected hasHandledPotentialRedirect = false;
3737
private lastProcessedEventTime = Date.now();
3838

3939
constructor(private readonly auth: Auth) {}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ type GenericAuthErrorParams = {
402402
| AuthErrorCode.DEPENDENT_SDK_INIT_BEFORE_AUTH
403403
| AuthErrorCode.INTERNAL_ERROR
404404
| AuthErrorCode.MFA_REQUIRED
405+
| AuthErrorCode.NO_AUTH_EVENT
405406
>]: {
406407
appName: AppName;
407408
email?: string;
@@ -413,6 +414,7 @@ export interface AuthErrorParams extends GenericAuthErrorParams {
413414
[AuthErrorCode.ARGUMENT_ERROR]: { appName?: AppName };
414415
[AuthErrorCode.DEPENDENT_SDK_INIT_BEFORE_AUTH]: { appName?: AppName };
415416
[AuthErrorCode.INTERNAL_ERROR]: { appName?: AppName };
417+
[AuthErrorCode.NO_AUTH_EVENT]: { appName?: AppName };
416418
[AuthErrorCode.MFA_REQUIRED]: {
417419
appName: AppName;
418420
serverResponse: IdTokenMfaResponse;

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,23 @@ declare namespace cordova.plugins.browsertab {
2525
}
2626

2727
declare namespace cordova.InAppBrowser {
28-
function open(url: string, target: string, options: string): unknown;
28+
function open(url: string, target: string, options: string): InAppBrowserRef;
2929
}
3030

3131
declare namespace universalLinks {
32-
function subscribe(n: null, cb: (event: unknown) => void): void;
32+
function subscribe(
33+
n: null,
34+
cb: (event: Record<string, string> | null) => void
35+
): void;
3336
}
3437

3538
declare namespace BuildInfo {
3639
const packageName: string;
3740
const displayName: string;
3841
}
42+
43+
declare function handleOpenUrl(url: string): void;
44+
45+
declare interface InAppBrowserRef {
46+
close?: () => void;
47+
}

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

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,21 @@ import { SingletonInstantiator } from '../../core/util/instantiator';
2525
import {
2626
AuthEvent,
2727
AuthEventType,
28+
EventManager,
2829
PopupRedirectResolver
2930
} from '../../model/popup_redirect';
30-
import { cordovaPopupRedirectResolver } from './popup_redirect';
31+
import {
32+
CordovaAuthEventManager,
33+
cordovaPopupRedirectResolver
34+
} from './popup_redirect';
3135
import { GoogleAuthProvider } from '../../core/providers/google';
3236
import * as utils from './utils';
3337
import * as events from './events';
3438
import { FirebaseError } from '@firebase/util';
39+
import {
40+
stubSingleTimeout,
41+
TimerTripFn
42+
} from '../../../test/helpers/timeout_stub';
3543

3644
use(chaiAsPromised);
3745
use(sinonChai);
@@ -85,6 +93,194 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
8593
});
8694
});
8795

96+
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+
126+
function event(manager: EventManager): Promise<AuthEvent> {
127+
return new Promise(resolve => {
128+
(manager as CordovaAuthEventManager).addPassiveListener(resolve);
129+
});
130+
}
131+
132+
context('when no event is present', () => {
133+
it('clears local storage and dispatches no-event event', async () => {
134+
const promise = event(await resolver._initialize(auth));
135+
tripNoEventTimer();
136+
const { error, ...rest } = await promise;
137+
138+
expect(error)
139+
.to.be.instanceOf(FirebaseError)
140+
.with.property('code', 'auth/no-auth-event');
141+
expect(rest).to.eql({
142+
type: AuthEventType.UNKNOWN,
143+
eventId: null,
144+
sessionId: null,
145+
urlResponse: null,
146+
postBody: null,
147+
tenantId: null
148+
});
149+
expect(events._getAndRemoveEvent).to.have.been.called;
150+
});
151+
});
152+
153+
context('when an event is present', () => {
154+
it('clears the no event timeout', async () => {
155+
await resolver._initialize(auth);
156+
await universalLinksCb!({});
157+
expect(window.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID);
158+
});
159+
160+
it('signals no event if no url in event data', async () => {
161+
const promise = event(await resolver._initialize(auth));
162+
await universalLinksCb!({});
163+
const { error, ...rest } = await promise;
164+
165+
expect(error)
166+
.to.be.instanceOf(FirebaseError)
167+
.with.property('code', 'auth/no-auth-event');
168+
expect(rest).to.eql({
169+
type: AuthEventType.UNKNOWN,
170+
eventId: null,
171+
sessionId: null,
172+
urlResponse: null,
173+
postBody: null,
174+
tenantId: null
175+
});
176+
});
177+
178+
it('signals no event if partial parse turns up null', async () => {
179+
const promise = event(await resolver._initialize(auth));
180+
eventsStubs._eventFromPartialAndUrl.returns(null);
181+
eventsStubs._getAndRemoveEvent.returns(
182+
Promise.resolve({
183+
type: AuthEventType.REAUTH_VIA_REDIRECT
184+
} as AuthEvent)
185+
);
186+
await universalLinksCb!({ url: 'foo-bar' });
187+
const { error, ...rest } = await promise;
188+
189+
expect(error)
190+
.to.be.instanceOf(FirebaseError)
191+
.with.property('code', 'auth/no-auth-event');
192+
expect(rest).to.eql({
193+
type: AuthEventType.UNKNOWN,
194+
eventId: null,
195+
sessionId: null,
196+
urlResponse: null,
197+
postBody: null,
198+
tenantId: null
199+
});
200+
});
201+
202+
it('signals the final event if partial expansion success', async () => {
203+
const finalEvent = {
204+
type: AuthEventType.REAUTH_VIA_REDIRECT,
205+
postBody: 'foo'
206+
};
207+
eventsStubs._getAndRemoveEvent.returns(
208+
Promise.resolve({
209+
type: AuthEventType.REAUTH_VIA_REDIRECT
210+
} as AuthEvent)
211+
);
212+
213+
const promise = event(await resolver._initialize(auth));
214+
eventsStubs._eventFromPartialAndUrl.returns(finalEvent as AuthEvent);
215+
await universalLinksCb!({ url: 'foo-bar' });
216+
expect(await promise).to.eq(finalEvent);
217+
expect(events._eventFromPartialAndUrl).to.have.been.calledWith(
218+
{ type: AuthEventType.REAUTH_VIA_REDIRECT },
219+
'foo-bar'
220+
);
221+
});
222+
});
223+
224+
context('when using global handleOpenUrl callback', () => {
225+
it('ignores inbound callbacks that are not for this app', async () => {
226+
await resolver._initialize(auth);
227+
handleOpenUrl(`${NOT_PACKAGE_NAME}://foo`);
228+
229+
// Clear timeout is called in the handler so we can check that
230+
expect(window.clearTimeout).not.to.have.been.called;
231+
});
232+
233+
it('passes through callback if package name matches', async () => {
234+
await resolver._initialize(auth);
235+
handleOpenUrl(`${PACKAGE_NAME}://foo`);
236+
expect(window.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID);
237+
});
238+
239+
it('signals the final event if partial expansion success', async () => {
240+
const finalEvent = {
241+
type: AuthEventType.REAUTH_VIA_REDIRECT,
242+
postBody: 'foo'
243+
};
244+
eventsStubs._getAndRemoveEvent.returns(
245+
Promise.resolve({
246+
type: AuthEventType.REAUTH_VIA_REDIRECT
247+
} as AuthEvent)
248+
);
249+
250+
const promise = event(await resolver._initialize(auth));
251+
eventsStubs._eventFromPartialAndUrl.returns(finalEvent as AuthEvent);
252+
handleOpenUrl(`${PACKAGE_NAME}://foo`);
253+
expect(await promise).to.eq(finalEvent);
254+
expect(events._eventFromPartialAndUrl).to.have.been.calledWith(
255+
{ type: AuthEventType.REAUTH_VIA_REDIRECT },
256+
`${PACKAGE_NAME}://foo`
257+
);
258+
});
259+
260+
it('calls the dev existing handleOpenUrl function', async () => {
261+
const oldHandleOpenUrl = sinon.stub();
262+
window.handleOpenUrl = oldHandleOpenUrl;
263+
264+
await resolver._initialize(auth);
265+
handleOpenUrl(`${PACKAGE_NAME}://foo`);
266+
expect(oldHandleOpenUrl).to.have.been.calledWith(
267+
`${PACKAGE_NAME}://foo`
268+
);
269+
});
270+
271+
it('calls the dev existing handleOpenUrl function for other package', async () => {
272+
const oldHandleOpenUrl = sinon.stub();
273+
window.handleOpenUrl = oldHandleOpenUrl;
274+
275+
await resolver._initialize(auth);
276+
handleOpenUrl(`${NOT_PACKAGE_NAME}://foo`);
277+
expect(oldHandleOpenUrl).to.have.been.calledWith(
278+
`${NOT_PACKAGE_NAME}://foo`
279+
);
280+
});
281+
});
282+
});
283+
88284
describe('_openPopup', () => {
89285
it('throws an error', () => {
90286
expect(() =>

0 commit comments

Comments
 (0)