Skip to content

Add app callback listeners to the cordova redirect handler #4449

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages-exp/auth-exp/src/core/auth/auth_event_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const EVENT_DUPLICATION_CACHE_DURATION_MS = 10 * 60 * 1000;
export class AuthEventManager implements EventManager {
private readonly cachedEventUids: Set<string> = new Set();
private readonly consumers: Set<AuthEventConsumer> = new Set();
private queuedRedirectEvent: AuthEvent | null = null;
private hasHandledPotentialRedirect = false;
protected queuedRedirectEvent: AuthEvent | null = null;
protected hasHandledPotentialRedirect = false;
private lastProcessedEventTime = Date.now();

constructor(private readonly auth: Auth) {}
Expand Down
2 changes: 2 additions & 0 deletions packages-exp/auth-exp/src/core/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ type GenericAuthErrorParams = {
| AuthErrorCode.DEPENDENT_SDK_INIT_BEFORE_AUTH
| AuthErrorCode.INTERNAL_ERROR
| AuthErrorCode.MFA_REQUIRED
| AuthErrorCode.NO_AUTH_EVENT
>]: {
appName: AppName;
email?: string;
Expand All @@ -413,6 +414,7 @@ export interface AuthErrorParams extends GenericAuthErrorParams {
[AuthErrorCode.ARGUMENT_ERROR]: { appName?: AppName };
[AuthErrorCode.DEPENDENT_SDK_INIT_BEFORE_AUTH]: { appName?: AppName };
[AuthErrorCode.INTERNAL_ERROR]: { appName?: AppName };
[AuthErrorCode.NO_AUTH_EVENT]: { appName?: AppName };
[AuthErrorCode.MFA_REQUIRED]: {
appName: AppName;
serverResponse: IdTokenMfaResponse;
Expand Down
13 changes: 11 additions & 2 deletions packages-exp/auth-exp/src/platform_cordova/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,23 @@ declare namespace cordova.plugins.browsertab {
}

declare namespace cordova.InAppBrowser {
function open(url: string, target: string, options: string): unknown;
function open(url: string, target: string, options: string): InAppBrowserRef;
}

declare namespace universalLinks {
function subscribe(n: null, cb: (event: unknown) => void): void;
function subscribe(
n: null,
cb: (event: Record<string, string> | null) => void
): void;
}

declare namespace BuildInfo {
const packageName: string;
const displayName: string;
}

declare function handleOpenUrl(url: string): void;

declare interface InAppBrowserRef {
close?: () => void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@ import { SingletonInstantiator } from '../../core/util/instantiator';
import {
AuthEvent,
AuthEventType,
EventManager,
PopupRedirectResolver
} from '../../model/popup_redirect';
import { cordovaPopupRedirectResolver } from './popup_redirect';
import {
CordovaAuthEventManager,
cordovaPopupRedirectResolver
} from './popup_redirect';
import { GoogleAuthProvider } from '../../core/providers/google';
import * as utils from './utils';
import * as events from './events';
import { FirebaseError } from '@firebase/util';
import {
stubSingleTimeout,
TimerTripFn
} from '../../../test/helpers/timeout_stub';

use(chaiAsPromised);
use(sinonChai);
Expand Down Expand Up @@ -85,6 +93,194 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => {
});
});

describe('_initialize', () => {
const NO_EVENT_TIMER_ID = 10001;
const PACKAGE_NAME = 'my.package';
const NOT_PACKAGE_NAME = 'not.my.package';
let universalLinksCb:
| ((eventData: Record<string, string> | null) => unknown)
| null;
let tripNoEventTimer: TimerTripFn;

beforeEach(() => {
tripNoEventTimer = stubSingleTimeout(NO_EVENT_TIMER_ID);
window.universalLinks = {
subscribe(_unused, cb) {
universalLinksCb = cb;
}
};
window.BuildInfo = {
packageName: PACKAGE_NAME,
displayName: ''
};
sinon.stub(window, 'clearTimeout');
});

afterEach(() => {
universalLinksCb = null;
const win = (window as unknown) as Record<string, unknown>;
delete win.universalLinks;
delete win.BuildInfo;
});

function event(manager: EventManager): Promise<AuthEvent> {
return new Promise(resolve => {
(manager as CordovaAuthEventManager).addPassiveListener(resolve);
});
}

context('when no event is present', () => {
it('clears local storage and dispatches no-event event', async () => {
const promise = event(await resolver._initialize(auth));
tripNoEventTimer();
const { error, ...rest } = await promise;

expect(error)
.to.be.instanceOf(FirebaseError)
.with.property('code', 'auth/no-auth-event');
expect(rest).to.eql({
type: AuthEventType.UNKNOWN,
eventId: null,
sessionId: null,
urlResponse: null,
postBody: null,
tenantId: null
});
expect(events._getAndRemoveEvent).to.have.been.called;
});
});

context('when an event is present', () => {
it('clears the no event timeout', async () => {
await resolver._initialize(auth);
await universalLinksCb!({});
expect(window.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID);
});

it('signals no event if no url in event data', async () => {
const promise = event(await resolver._initialize(auth));
await universalLinksCb!({});
const { error, ...rest } = await promise;

expect(error)
.to.be.instanceOf(FirebaseError)
.with.property('code', 'auth/no-auth-event');
expect(rest).to.eql({
type: AuthEventType.UNKNOWN,
eventId: null,
sessionId: null,
urlResponse: null,
postBody: null,
tenantId: null
});
});

it('signals no event if partial parse turns up null', async () => {
const promise = event(await resolver._initialize(auth));
eventsStubs._eventFromPartialAndUrl.returns(null);
eventsStubs._getAndRemoveEvent.returns(
Promise.resolve({
type: AuthEventType.REAUTH_VIA_REDIRECT
} as AuthEvent)
);
await universalLinksCb!({ url: 'foo-bar' });
const { error, ...rest } = await promise;

expect(error)
.to.be.instanceOf(FirebaseError)
.with.property('code', 'auth/no-auth-event');
expect(rest).to.eql({
type: AuthEventType.UNKNOWN,
eventId: null,
sessionId: null,
urlResponse: null,
postBody: null,
tenantId: null
});
});

it('signals the final event if partial expansion success', async () => {
const finalEvent = {
type: AuthEventType.REAUTH_VIA_REDIRECT,
postBody: 'foo'
};
eventsStubs._getAndRemoveEvent.returns(
Promise.resolve({
type: AuthEventType.REAUTH_VIA_REDIRECT
} as AuthEvent)
);

const promise = event(await resolver._initialize(auth));
eventsStubs._eventFromPartialAndUrl.returns(finalEvent as AuthEvent);
await universalLinksCb!({ url: 'foo-bar' });
expect(await promise).to.eq(finalEvent);
expect(events._eventFromPartialAndUrl).to.have.been.calledWith(
{ type: AuthEventType.REAUTH_VIA_REDIRECT },
'foo-bar'
);
});
});

context('when using global handleOpenUrl callback', () => {
it('ignores inbound callbacks that are not for this app', async () => {
await resolver._initialize(auth);
handleOpenUrl(`${NOT_PACKAGE_NAME}://foo`);

// Clear timeout is called in the handler so we can check that
expect(window.clearTimeout).not.to.have.been.called;
});

it('passes through callback if package name matches', async () => {
await resolver._initialize(auth);
handleOpenUrl(`${PACKAGE_NAME}://foo`);
expect(window.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID);
});

it('signals the final event if partial expansion success', async () => {
const finalEvent = {
type: AuthEventType.REAUTH_VIA_REDIRECT,
postBody: 'foo'
};
eventsStubs._getAndRemoveEvent.returns(
Promise.resolve({
type: AuthEventType.REAUTH_VIA_REDIRECT
} as AuthEvent)
);

const promise = event(await resolver._initialize(auth));
eventsStubs._eventFromPartialAndUrl.returns(finalEvent as AuthEvent);
handleOpenUrl(`${PACKAGE_NAME}://foo`);
expect(await promise).to.eq(finalEvent);
expect(events._eventFromPartialAndUrl).to.have.been.calledWith(
{ type: AuthEventType.REAUTH_VIA_REDIRECT },
`${PACKAGE_NAME}://foo`
);
});

it('calls the dev existing handleOpenUrl function', async () => {
const oldHandleOpenUrl = sinon.stub();
window.handleOpenUrl = oldHandleOpenUrl;

await resolver._initialize(auth);
handleOpenUrl(`${PACKAGE_NAME}://foo`);
expect(oldHandleOpenUrl).to.have.been.calledWith(
`${PACKAGE_NAME}://foo`
);
});

it('calls the dev existing handleOpenUrl function for other package', async () => {
const oldHandleOpenUrl = sinon.stub();
window.handleOpenUrl = oldHandleOpenUrl;

await resolver._initialize(auth);
handleOpenUrl(`${NOT_PACKAGE_NAME}://foo`);
expect(oldHandleOpenUrl).to.have.been.calledWith(
`${NOT_PACKAGE_NAME}://foo`
);
});
});
});

describe('_openPopup', () => {
it('throws an error', () => {
expect(() =>
Expand Down
Loading