diff --git a/packages-exp/auth-compat-exp/src/auth.test.ts b/packages-exp/auth-compat-exp/src/auth.test.ts index 77c5e7bc11f..b69ce48a151 100644 --- a/packages-exp/auth-compat-exp/src/auth.test.ts +++ b/packages-exp/auth-compat-exp/src/auth.test.ts @@ -21,6 +21,7 @@ import { expect, use } from 'chai'; import * as sinon from 'sinon'; import * as sinonChai from 'sinon-chai'; import { Auth } from './auth'; +import { CompatPopupRedirectResolver } from './popup_redirect'; use(sinonChai); @@ -69,7 +70,7 @@ describe('auth compat', () => { exp._getInstance(exp.inMemoryPersistence), exp._getInstance(exp.indexedDBLocalPersistence) ], - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ); } }); diff --git a/packages-exp/auth-compat-exp/src/auth.ts b/packages-exp/auth-compat-exp/src/auth.ts index 992f6e27218..8a9d827d926 100644 --- a/packages-exp/auth-compat-exp/src/auth.ts +++ b/packages-exp/auth-compat-exp/src/auth.ts @@ -27,6 +27,7 @@ import { import { _validatePersistenceArgument, Persistence } from './persistence'; import { _isPopupRedirectSupported } from './platform'; +import { CompatPopupRedirectResolver } from './popup_redirect'; import { User } from './user'; import { convertConfirmationResult, @@ -71,7 +72,7 @@ export class Auth // eslint-disable-next-line @typescript-eslint/no-floating-promises this.auth._initializeWithPersistence( hierarchy, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ); } @@ -142,7 +143,7 @@ export class Auth ); const credential = await exp.getRedirectResult( this.auth, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ); if (!credential) { return { @@ -283,7 +284,7 @@ export class Auth exp.signInWithPopup( this.auth, provider as exp.AuthProvider, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ) ); } @@ -297,7 +298,7 @@ export class Auth return exp.signInWithRedirect( this.auth, provider as exp.AuthProvider, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ); } updateCurrentUser(user: compat.User | null): Promise { diff --git a/packages-exp/auth-compat-exp/src/platform.ts b/packages-exp/auth-compat-exp/src/platform.ts index 680997d3266..5d5d879a463 100644 --- a/packages-exp/auth-compat-exp/src/platform.ts +++ b/packages-exp/auth-compat-exp/src/platform.ts @@ -31,6 +31,8 @@ declare global { } } +const CORDOVA_ONDEVICEREADY_TIMEOUT_MS = 1000; + function _getCurrentScheme(): string | null { return self?.location?.protocol || null; } @@ -47,7 +49,7 @@ function _isHttpOrHttps(): boolean { * @return {boolean} Whether the app is rendered in a mobile iOS or Android * Cordova environment. */ -function _isAndroidOrIosCordovaScheme(ua: string = getUA()): boolean { +export function _isAndroidOrIosCordovaScheme(ua: string = getUA()): boolean { return !!( (_getCurrentScheme() === 'file:' || _getCurrentScheme() === 'ionic:') && ua.toLowerCase().match(/iphone|ipad|ipod|android/) @@ -159,3 +161,21 @@ export function _getClientPlatform(): impl.ClientPlatform { } return impl.ClientPlatform.BROWSER; } + +export async function _isCordova(): Promise { + if (!_isAndroidOrIosCordovaScheme() || typeof document === 'undefined') { + return false; + } + + return new Promise(resolve => { + const timeoutId = setTimeout(() => { + // We've waited long enough; the telltale Cordova event didn't happen + resolve(false); + }, CORDOVA_ONDEVICEREADY_TIMEOUT_MS); + + document.addEventListener('deviceready', () => { + clearTimeout(timeoutId); + resolve(true); + }); + }); +} diff --git a/packages-exp/auth-compat-exp/src/popup_redirect.test.ts b/packages-exp/auth-compat-exp/src/popup_redirect.test.ts new file mode 100644 index 00000000000..8de3d9642ef --- /dev/null +++ b/packages-exp/auth-compat-exp/src/popup_redirect.test.ts @@ -0,0 +1,151 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import * as sinonChai from 'sinon-chai'; +import * as sinon from 'sinon'; +import * as exp from '@firebase/auth-exp/internal'; +import * as platform from './platform'; +import { CompatPopupRedirectResolver } from './popup_redirect'; +import { FirebaseApp } from '@firebase/app-compat'; + +use(sinonChai); + +describe('popup_redirect/CompatPopupRedirectResolver', () => { + let compatResolver: CompatPopupRedirectResolver; + let auth: exp.AuthImpl; + + beforeEach(() => { + compatResolver = new CompatPopupRedirectResolver(); + const app = { options: { apiKey: 'api-key' } } as FirebaseApp; + auth = new exp.AuthImpl(app, { + apiKey: 'api-key' + } as exp.Config); + }); + + afterEach(() => { + sinon.restore(); + }); + + context('initialization and resolver selection', () => { + const browserResolver = exp._getInstance( + exp.browserPopupRedirectResolver + ); + const cordovaResolver = exp._getInstance( + exp.cordovaPopupRedirectResolver + ); + + beforeEach(() => { + sinon.stub(browserResolver, '_initialize'); + sinon.stub(cordovaResolver, '_initialize'); + }); + + it('selects the Cordova resolver if in Cordova', async () => { + sinon.stub(platform, '_isCordova').returns(Promise.resolve(true)); + await compatResolver._initialize(auth); + expect(cordovaResolver._initialize).to.have.been.calledWith(auth); + expect(browserResolver._initialize).not.to.have.been.called; + }); + + it('selects the Browser resolver if in Browser', async () => { + sinon.stub(platform, '_isCordova').returns(Promise.resolve(false)); + await compatResolver._initialize(auth); + expect(cordovaResolver._initialize).not.to.have.been.called; + expect(browserResolver._initialize).to.have.been.calledWith(auth); + }); + }); + + context('callthrough methods', () => { + let underlyingResolver: sinon.SinonStubbedInstance; + let provider: exp.AuthProvider; + + beforeEach(() => { + underlyingResolver = sinon.createStubInstance(FakeResolver); + ((compatResolver as unknown) as { + underlyingResolver: exp.PopupRedirectResolverInternal; + }).underlyingResolver = underlyingResolver; + provider = new exp.GoogleAuthProvider(); + }); + + it('_openPopup', async () => { + await compatResolver._openPopup( + auth, + provider, + exp.AuthEventType.LINK_VIA_POPUP, + 'eventId' + ); + expect(underlyingResolver._openPopup).to.have.been.calledWith( + auth, + provider, + exp.AuthEventType.LINK_VIA_POPUP, + 'eventId' + ); + }); + + it('_openRedirect', async () => { + await compatResolver._openRedirect( + auth, + provider, + exp.AuthEventType.LINK_VIA_REDIRECT, + 'eventId' + ); + expect(underlyingResolver._openRedirect).to.have.been.calledWith( + auth, + provider, + exp.AuthEventType.LINK_VIA_REDIRECT, + 'eventId' + ); + }); + + it('_isIframeWebStorageSupported', () => { + const cb = (): void => {}; + compatResolver._isIframeWebStorageSupported(auth, cb); + expect( + underlyingResolver._isIframeWebStorageSupported + ).to.have.been.calledWith(auth, cb); + }); + + it('_originValidation', async () => { + await compatResolver._originValidation(auth); + expect(underlyingResolver._originValidation).to.have.been.calledWith( + auth + ); + }); + }); +}); + +class FakeResolver implements exp.PopupRedirectResolverInternal { + _completeRedirectFn = async (): Promise => null; + _redirectPersistence = exp.inMemoryPersistence; + + _initialize(): Promise { + throw new Error('Method not implemented.'); + } + _openPopup(): Promise { + throw new Error('Method not implemented.'); + } + _openRedirect(): Promise { + throw new Error('Method not implemented.'); + } + _isIframeWebStorageSupported(): void { + throw new Error('Method not implemented.'); + } + + _originValidation(): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/packages-exp/auth-compat-exp/src/popup_redirect.ts b/packages-exp/auth-compat-exp/src/popup_redirect.ts new file mode 100644 index 00000000000..0e83ca90fbd --- /dev/null +++ b/packages-exp/auth-compat-exp/src/popup_redirect.ts @@ -0,0 +1,94 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as exp from '@firebase/auth-exp/internal'; +import { _isCordova } from './platform'; + +const _assert: typeof exp._assert = exp._assert; + +/** Platform-agnostic popup-redirect resolver */ +export class CompatPopupRedirectResolver + implements exp.PopupRedirectResolverInternal { + private underlyingResolver: exp.PopupRedirectResolverInternal | null = null; + _redirectPersistence = exp.browserSessionPersistence; + + _completeRedirectFn: ( + auth: exp.Auth, + resolver: exp.PopupRedirectResolver, + bypassAuthState: boolean + ) => Promise = exp._getRedirectResult; + + async _initialize(auth: exp.AuthImpl): Promise { + if (this.underlyingResolver) { + return this.underlyingResolver._initialize(auth); + } + + // We haven't yet determined whether or not we're in Cordova; go ahead + // and determine that state now. + const isCordova = await _isCordova(); + this.underlyingResolver = exp._getInstance( + isCordova + ? exp.cordovaPopupRedirectResolver + : exp.browserPopupRedirectResolver + ); + return this.assertedUnderlyingResolver._initialize(auth); + } + + _openPopup( + auth: exp.AuthImpl, + provider: exp.AuthProvider, + authType: exp.AuthEventType, + eventId?: string + ): Promise { + return this.assertedUnderlyingResolver._openPopup( + auth, + provider, + authType, + eventId + ); + } + + _openRedirect( + auth: exp.AuthImpl, + provider: exp.AuthProvider, + authType: exp.AuthEventType, + eventId?: string + ): Promise { + return this.assertedUnderlyingResolver._openRedirect( + auth, + provider, + authType, + eventId + ); + } + + _isIframeWebStorageSupported( + auth: exp.AuthImpl, + cb: (support: boolean) => unknown + ): void { + this.assertedUnderlyingResolver._isIframeWebStorageSupported(auth, cb); + } + + _originValidation(auth: exp.Auth): Promise { + return this.assertedUnderlyingResolver._originValidation(auth); + } + + private get assertedUnderlyingResolver(): exp.PopupRedirectResolverInternal { + _assert(this.underlyingResolver, exp.AuthErrorCode.INTERNAL_ERROR); + return this.underlyingResolver; + } +} diff --git a/packages-exp/auth-compat-exp/src/user.ts b/packages-exp/auth-compat-exp/src/user.ts index 6a1dc97849d..abf430d54ee 100644 --- a/packages-exp/auth-compat-exp/src/user.ts +++ b/packages-exp/auth-compat-exp/src/user.ts @@ -17,6 +17,7 @@ import * as exp from '@firebase/auth-exp/internal'; import * as compat from '@firebase/auth-types'; +import { CompatPopupRedirectResolver } from './popup_redirect'; import { convertConfirmationResult, convertCredential @@ -91,7 +92,7 @@ export class User implements compat.User, Wrapper { exp.linkWithPopup( this.user, provider as exp.AuthProvider, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ) ); } @@ -99,7 +100,7 @@ export class User implements compat.User, Wrapper { return exp.linkWithRedirect( this.user, provider as exp.AuthProvider, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ); } reauthenticateAndRetrieveDataWithCredential( @@ -139,7 +140,7 @@ export class User implements compat.User, Wrapper { exp.reauthenticateWithPopup( this.user, provider as exp.AuthProvider, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ) ); } @@ -147,7 +148,7 @@ export class User implements compat.User, Wrapper { return exp.reauthenticateWithRedirect( this.user, provider as exp.AuthProvider, - exp.browserPopupRedirectResolver + CompatPopupRedirectResolver ); } sendEmailVerification( diff --git a/packages-exp/auth-exp/internal/index.ts b/packages-exp/auth-exp/internal/index.ts index b66c1059389..c1d4e10824d 100644 --- a/packages-exp/auth-exp/internal/index.ts +++ b/packages-exp/auth-exp/internal/index.ts @@ -26,6 +26,11 @@ export { PersistenceInternal } from '../src/core/persistence'; export { _persistenceKeyName } from '../src/core/persistence/persistence_user_manager'; export { UserImpl } from '../src/core/user/user_impl'; export { _getInstance } from '../src/core/util/instantiator'; +export { + PopupRedirectResolverInternal, + EventManager, + AuthEventType +} from '../src/model/popup_redirect'; export { UserCredentialInternal, UserParameters } from '../src/model/user'; export { registerAuth } from '../src/core/auth/register'; export { DefaultConfig, AuthImpl } from '../src/core/auth/auth_impl'; @@ -35,3 +40,6 @@ export { ClientPlatform, _getClientVersion } from '../src/core/util/version'; export { _generateEventId } from '../src/core/util/event_id'; export { _fail, _assert } from '../src/core/util/assert'; +export { AuthPopup } from '../src/platform_browser/util/popup'; +export { _getRedirectResult } from '../src/platform_browser/strategies/redirect'; +export { cordovaPopupRedirectResolver } from '../src/platform_cordova/popup_redirect/popup_redirect'; diff --git a/packages-exp/auth-exp/src/platform_cordova/plugins.ts b/packages-exp/auth-exp/src/platform_cordova/plugins.ts index 0fde5b359aa..c884e7caf5a 100644 --- a/packages-exp/auth-exp/src/platform_cordova/plugins.ts +++ b/packages-exp/auth-exp/src/platform_cordova/plugins.ts @@ -15,34 +15,40 @@ * limitations under the License. */ -// For some reason, the linter doesn't recognize that these are used elsewhere -// in the SDK -/* eslint-disable @typescript-eslint/no-unused-vars */ +export interface CordovaWindow extends Window { + cordova: { + plugins: { + browsertab: { + isAvailable(cb: (available: boolean) => void): void; + openUrl(url: string): void; + close(): void; + }; + }; -declare namespace cordova.plugins.browsertab { - function isAvailable(cb: (available: boolean) => void): void; - function openUrl(url: string): void; - function close(): void; -} + InAppBrowser: { + open(url: string, target: string, options: string): InAppBrowserRef; + }; + }; -declare namespace cordova.InAppBrowser { - function open(url: string, target: string, options: string): InAppBrowserRef; -} + universalLinks: { + subscribe( + n: null, + cb: (event: Record | null) => void + ): void; + }; -declare namespace universalLinks { - function subscribe( - n: null, - cb: (event: Record | null) => void - ): void; -} + BuildInfo: { + readonly packageName: string; + readonly displayName: string; + }; -declare namespace BuildInfo { - const packageName: string; - const displayName: string; + handleOpenUrl(url: string): void; } -declare function handleOpenUrl(url: string): void; - -declare interface InAppBrowserRef { +export interface InAppBrowserRef { close?: () => void; } + +export function _cordovaWindow(): CordovaWindow { + return (window as unknown) as CordovaWindow; +} diff --git a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.test.ts b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.test.ts index 1112565dfeb..b738051f264 100644 --- a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.test.ts +++ b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.test.ts @@ -37,10 +37,13 @@ import { stubSingleTimeout, TimerTripFn } from '../../../test/helpers/timeout_stub'; +import { _cordovaWindow } from '../plugins'; use(chaiAsPromised); use(sinonChai); +const win = _cordovaWindow(); + describe('platform_cordova/popup_redirect/popup_redirect', () => { const PACKAGE_NAME = 'my.package'; const NOT_PACKAGE_NAME = 'not.my.package'; @@ -69,25 +72,25 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => { _getDeepLinkFromCallback: sinon.stub(events, '_getDeepLinkFromCallback') }; - window.universalLinks = { + win.universalLinks = { subscribe(_unused, cb) { universalLinksCb = cb; } }; - window.BuildInfo = { + win.BuildInfo = { packageName: PACKAGE_NAME, displayName: '' }; tripNoEventTimer = stubSingleTimeout(NO_EVENT_TIMER_ID); - sinon.stub(window, 'clearTimeout'); + sinon.stub(win, 'clearTimeout'); }); afterEach(() => { sinon.restore(); universalLinksCb = null; - const win = (window as unknown) as Record; - delete win.universalLinks; - delete win.BuildInfo; + const anyWindow = (win as unknown) as Record; + delete anyWindow.universalLinks; + delete anyWindow.BuildInfo; }); describe('_openRedirect', () => { @@ -158,7 +161,7 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => { it('clears the no event timeout', async () => { await resolver._initialize(auth); await universalLinksCb!({}); - expect(window.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID); + expect(win.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID); }); it('signals no event if no url in event data', async () => { @@ -228,16 +231,16 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => { 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`); + win.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; + expect(win.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); + win.handleOpenUrl(`${PACKAGE_NAME}://foo`); + expect(win.clearTimeout).to.have.been.calledWith(NO_EVENT_TIMER_ID); }); it('signals the final event if partial expansion success', async () => { @@ -253,7 +256,7 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => { const promise = event(await resolver._initialize(auth)); eventsStubs._eventFromPartialAndUrl!.returns(finalEvent as AuthEvent); - handleOpenUrl(`${PACKAGE_NAME}://foo`); + win.handleOpenUrl(`${PACKAGE_NAME}://foo`); expect(await promise).to.eq(finalEvent); expect(events._eventFromPartialAndUrl).to.have.been.calledWith( { type: AuthEventType.REAUTH_VIA_REDIRECT }, @@ -263,10 +266,10 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => { it('calls the dev existing handleOpenUrl function', async () => { const oldHandleOpenUrl = sinon.stub(); - window.handleOpenUrl = oldHandleOpenUrl; + win.handleOpenUrl = oldHandleOpenUrl; await resolver._initialize(auth); - handleOpenUrl(`${PACKAGE_NAME}://foo`); + win.handleOpenUrl(`${PACKAGE_NAME}://foo`); expect(oldHandleOpenUrl).to.have.been.calledWith( `${PACKAGE_NAME}://foo` ); @@ -274,10 +277,10 @@ describe('platform_cordova/popup_redirect/popup_redirect', () => { it('calls the dev existing handleOpenUrl function for other package', async () => { const oldHandleOpenUrl = sinon.stub(); - window.handleOpenUrl = oldHandleOpenUrl; + win.handleOpenUrl = oldHandleOpenUrl; await resolver._initialize(auth); - handleOpenUrl(`${NOT_PACKAGE_NAME}://foo`); + win.handleOpenUrl(`${NOT_PACKAGE_NAME}://foo`); expect(oldHandleOpenUrl).to.have.been.calledWith( `${NOT_PACKAGE_NAME}://foo` ); diff --git a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.ts b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.ts index 20faf58d326..3973522aa1b 100644 --- a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.ts +++ b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/popup_redirect.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import '../plugins'; import { AuthProvider, PopupRedirectResolver } from '../../model/public_types'; import { browserSessionPersistence } from '../../platform_browser/persistence/session_storage'; import { AuthInternal } from '../../model/auth'; @@ -43,6 +42,7 @@ import { import { AuthEventManager } from '../../core/auth/auth_event_manager'; import { _getRedirectResult } from '../../platform_browser/strategies/redirect'; import { _clearRedirectOutcomes } from '../../core/strategies/redirect'; +import { _cordovaWindow } from '../plugins'; /** * How long to wait for the initial auth event before concluding no @@ -109,6 +109,9 @@ class CordovaPopupRedirectResolver implements PopupRedirectResolverInternal { auth: AuthInternal, manager: AuthEventManager ): void { + // Get the global plugins + const { universalLinks, handleOpenUrl, BuildInfo } = _cordovaWindow(); + const noEventTimeout = setTimeout(async () => { // We didn't see that initial event. Clear any pending object and // dispatch no event @@ -145,9 +148,9 @@ class CordovaPopupRedirectResolver implements PopupRedirectResolverInternal { // For this to work, cordova-plugin-customurlscheme needs to be installed. // https://github.com/EddyVerbruggen/Custom-URL-scheme // Do not overwrite the existing developer's URL handler. - const existingHandleOpenUrl = window.handleOpenUrl; + const existingHandleOpenUrl = handleOpenUrl; const packagePrefix = `${BuildInfo.packageName.toLowerCase()}://`; - window.handleOpenUrl = async url => { + _cordovaWindow().handleOpenUrl = async url => { if (url.toLowerCase().startsWith(packagePrefix)) { // We want this intentionally to float // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.test.ts b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.test.ts index 8b151eab4ce..0a0859b8f94 100644 --- a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.test.ts +++ b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.test.ts @@ -36,6 +36,7 @@ import { TimerTripFn } from '../../../test/helpers/timeout_stub'; import { FirebaseError } from '@firebase/util'; +import { InAppBrowserRef, _cordovaWindow } from '../plugins'; const ANDROID_UA = 'UserAgent/5.0 (Linux; Android 0.0.0)'; const IOS_UA = 'UserAgent/5.0 (iPhone; CPU iPhone 0.0.0)'; @@ -45,6 +46,8 @@ const DESKTOP_UA = 'UserAgent/5.0 (Linux; Ubuntu 0.0.0)'; use(chaiAsPromised); use(sinonChai); +const win = _cordovaWindow(); + describe('platform_cordova/popup_redirect/utils', () => { let auth: TestAuth; @@ -56,9 +59,9 @@ describe('platform_cordova/popup_redirect/utils', () => { afterEach(() => { sinon.restore(); // Clean up the window object from attachExpectedPlugins() - removeProp(window, 'cordova'); - removeProp(window, 'BuildInfo'); - removeProp(window, 'universalLinks'); + removeProp(win, 'cordova'); + removeProp(win, 'BuildInfo'); + removeProp(win, 'universalLinks'); }); function setUA(ua: string): void { @@ -72,7 +75,7 @@ describe('platform_cordova/popup_redirect/utils', () => { }); it('rejects if universal links is missing', () => { - removeProp(window, 'universalLinks'); + removeProp(win, 'universalLinks'); expect(() => _checkCordovaConfiguration(auth)) .to.throw(fbUtils.FirebaseError, 'auth/invalid-cordova-configuration') .that.has.deep.property('customData', { @@ -82,7 +85,7 @@ describe('platform_cordova/popup_redirect/utils', () => { }); it('rejects if build info is missing', () => { - removeProp(window.BuildInfo, 'packageName'); + removeProp(win.BuildInfo, 'packageName'); expect(() => _checkCordovaConfiguration(auth)) .to.throw(fbUtils.FirebaseError, 'auth/invalid-cordova-configuration') .that.has.deep.property('customData', { @@ -92,7 +95,7 @@ describe('platform_cordova/popup_redirect/utils', () => { }); it('rejects if browsertab openUrl is missing', () => { - removeProp(window.cordova.plugins.browsertab, 'openUrl'); + removeProp(win.cordova.plugins.browsertab, 'openUrl'); expect(() => _checkCordovaConfiguration(auth)) .to.throw(fbUtils.FirebaseError, 'auth/invalid-cordova-configuration') .that.has.deep.property('customData', { @@ -102,7 +105,7 @@ describe('platform_cordova/popup_redirect/utils', () => { }); it('rejects if InAppBrowser is missing', () => { - removeProp(window.cordova.InAppBrowser, 'open'); + removeProp(win.cordova.InAppBrowser, 'open'); expect(() => _checkCordovaConfiguration(auth)) .to.throw(fbUtils.FirebaseError, 'auth/invalid-cordova-configuration') .that.has.deep.property('customData', { @@ -165,6 +168,7 @@ describe('platform_cordova/popup_redirect/utils', () => { it('does not attach a display name if none is present', async () => { setUA(ANDROID_UA); + delete (win.BuildInfo as { displayName?: string }).displayName; const params = getParams( await _generateHandlerUrl(auth, event, provider) ); @@ -173,7 +177,7 @@ describe('platform_cordova/popup_redirect/utils', () => { it('attaches the relevant display name', async () => { setUA(IOS_UA); - (BuildInfo as { displayName: string }).displayName = 'This is my app'; + (win.BuildInfo as { displayName: string }).displayName = 'This is my app'; const params = getParams( await _generateHandlerUrl(auth, event, provider) ); @@ -186,27 +190,27 @@ describe('platform_cordova/popup_redirect/utils', () => { beforeEach(() => { isBrowsertabAvailable = false; sinon - .stub(cordova.plugins.browsertab, 'isAvailable') + .stub(win.cordova.plugins.browsertab, 'isAvailable') .callsFake(cb => cb(isBrowsertabAvailable)); - sinon.stub(cordova.plugins.browsertab, 'openUrl'); - sinon.stub(cordova.InAppBrowser, 'open'); + sinon.stub(win.cordova.plugins.browsertab, 'openUrl'); + sinon.stub(win.cordova.InAppBrowser, 'open'); }); it('uses browserTab if that is available', async () => { isBrowsertabAvailable = true; await _performRedirect('https://localhost/__/auth/handler'); - expect(cordova.plugins.browsertab.openUrl).to.have.been.calledWith( + expect(win.cordova.plugins.browsertab.openUrl).to.have.been.calledWith( 'https://localhost/__/auth/handler' ); - expect(cordova.InAppBrowser.open).not.to.have.been.called; + expect(win.cordova.InAppBrowser.open).not.to.have.been.called; }); it('falls back to InAppBrowser if need be', async () => { isBrowsertabAvailable = false; setUA(ANDROID_UA); await _performRedirect('https://localhost/__/auth/handler'); - expect(cordova.plugins.browsertab.openUrl).not.to.have.been.called; - expect(cordova.InAppBrowser.open).to.have.been.calledWith( + expect(win.cordova.plugins.browsertab.openUrl).not.to.have.been.called; + expect(win.cordova.InAppBrowser.open).to.have.been.calledWith( 'https://localhost/__/auth/handler', '_system', 'location=yes' @@ -217,8 +221,8 @@ describe('platform_cordova/popup_redirect/utils', () => { isBrowsertabAvailable = false; setUA(IOS_8_UA); await _performRedirect('https://localhost/__/auth/handler'); - expect(cordova.plugins.browsertab.openUrl).not.to.have.been.called; - expect(cordova.InAppBrowser.open).to.have.been.calledWith( + expect(win.cordova.plugins.browsertab.openUrl).not.to.have.been.called; + expect(win.cordova.InAppBrowser.open).to.have.been.calledWith( 'https://localhost/__/auth/handler', '_blank', 'location=yes' @@ -271,13 +275,13 @@ describe('platform_cordova/popup_redirect/utils', () => { FirebaseError, 'auth/redirect-cancelled-by-user' ); - expect(window.setTimeout).to.have.been.calledOnce; + expect(win.setTimeout).to.have.been.calledOnce; }); it('cleans up listeners and cancels timer', async () => { sinon.stub(document, 'removeEventListener').callThrough(); sinon.stub(eventManager, 'removePassiveListener'); - sinon.stub(window, 'clearTimeout'); + sinon.stub(win, 'clearTimeout'); const promise = _waitForAppResume(auth, eventManager, null); document.dispatchEvent(new CustomEvent('resume')); tripCancelTimer(); @@ -297,7 +301,7 @@ describe('platform_cordova/popup_redirect/utils', () => { expect(eventManager.removePassiveListener).to.have.been.calledWith( sinon.match.func ); - expect(window.clearTimeout).to.have.been.calledWith(CANCEL_TIMER_ID); + expect(win.clearTimeout).to.have.been.calledWith(CANCEL_TIMER_ID); }); }); @@ -308,6 +312,12 @@ describe('platform_cordova/popup_redirect/utils', () => { ); } + let cordova: typeof win.cordova; + + beforeEach(() => { + cordova = win.cordova; + }); + it('resolves the promise', async () => { const promise = _waitForAppResume(auth, eventManager, null); sendEvent(); @@ -333,7 +343,7 @@ describe('platform_cordova/popup_redirect/utils', () => { it('cleans up listeners and cancels timer', async () => { sinon.stub(document, 'removeEventListener').callThrough(); sinon.stub(eventManager, 'removePassiveListener'); - sinon.stub(window, 'clearTimeout'); + sinon.stub(win, 'clearTimeout'); const promise = _waitForAppResume(auth, eventManager, null); document.dispatchEvent(new CustomEvent('resume')); sendEvent(); @@ -350,7 +360,7 @@ describe('platform_cordova/popup_redirect/utils', () => { expect(eventManager.removePassiveListener).to.have.been.calledWith( sinon.match.func ); - expect(window.clearTimeout).to.have.been.calledWith(CANCEL_TIMER_ID); + expect(win.clearTimeout).to.have.been.calledWith(CANCEL_TIMER_ID); }); }); }); @@ -358,7 +368,6 @@ describe('platform_cordova/popup_redirect/utils', () => { function attachExpectedPlugins(): void { // Eventually these will be replaced with full mocks - const win = (window as unknown) as Record; win.cordova = { plugins: { browsertab: { @@ -368,14 +377,15 @@ function attachExpectedPlugins(): void { } }, InAppBrowser: { - open: () => {} + open: () => ({}) } }; win.universalLinks = { subscribe: () => {} }; win.BuildInfo = { - packageName: 'com.example.name.package' + packageName: 'com.example.name.package', + displayName: 'display name' }; } diff --git a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.ts b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.ts index 4eb78b69a09..25cf9488e96 100644 --- a/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.ts +++ b/packages-exp/auth-exp/src/platform_cordova/popup_redirect/utils.ts @@ -27,6 +27,7 @@ import { _isAndroid, _isIOS, _isIOS7Or8 } from '../../core/util/browser'; import { _getRedirectUrl } from '../../core/util/handler'; import { AuthInternal } from '../../model/auth'; import { AuthEvent } from '../../model/popup_redirect'; +import { InAppBrowserRef, _cordovaWindow } from '../plugins'; /** * How long to wait after the app comes back into focus before concluding that @@ -42,6 +43,8 @@ export async function _generateHandlerUrl( event: AuthEvent, provider: AuthProvider ): Promise { + // Get the cordova plugins + const { BuildInfo } = _cordovaWindow(); debugAssert(event.sessionId, 'AuthEvent did not contain a session ID'); const sessionDigest = await computeSha256(event.sessionId); @@ -76,6 +79,9 @@ export async function _generateHandlerUrl( export function _performRedirect( handlerUrl: string ): Promise { + // Get the cordova plugins + const { cordova } = _cordovaWindow(); + return new Promise(resolve => { cordova.plugins.browsertab.isAvailable(browserTabIsAvailable => { let iabRef: InAppBrowserRef | null = null; @@ -111,6 +117,9 @@ export async function _waitForAppResume( eventListener: PassiveAuthEventListener, iabRef: InAppBrowserRef | null ): Promise { + // Get the cordova plugins + const { cordova } = _cordovaWindow(); + let cleanup = (): void => {}; try { await new Promise((resolve, reject) => { @@ -185,13 +194,14 @@ export async function _waitForAppResume( * missing plugin. */ export function _checkCordovaConfiguration(auth: AuthInternal): void { + const win = _cordovaWindow(); // Check all dependencies installed. // https://github.com/nordnet/cordova-universal-links-plugin // Note that cordova-universal-links-plugin has been abandoned. // A fork with latest fixes is available at: // https://www.npmjs.com/package/cordova-universal-links-plugin-fix _assert( - typeof window?.universalLinks?.subscribe === 'function', + typeof win?.universalLinks?.subscribe === 'function', auth, AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, { @@ -201,7 +211,7 @@ export function _checkCordovaConfiguration(auth: AuthInternal): void { // https://www.npmjs.com/package/cordova-plugin-buildinfo _assert( - typeof window?.BuildInfo?.packageName !== 'undefined', + typeof win?.BuildInfo?.packageName !== 'undefined', auth, AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, { @@ -211,7 +221,7 @@ export function _checkCordovaConfiguration(auth: AuthInternal): void { // https://github.com/google/cordova-plugin-browsertab _assert( - typeof window?.cordova?.plugins?.browsertab?.openUrl === 'function', + typeof win?.cordova?.plugins?.browsertab?.openUrl === 'function', auth, AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, { @@ -219,7 +229,7 @@ export function _checkCordovaConfiguration(auth: AuthInternal): void { } ); _assert( - typeof window?.cordova?.plugins?.browsertab?.isAvailable === 'function', + typeof win?.cordova?.plugins?.browsertab?.isAvailable === 'function', auth, AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, { @@ -229,7 +239,7 @@ export function _checkCordovaConfiguration(auth: AuthInternal): void { // https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/ _assert( - typeof window?.cordova?.InAppBrowser?.open === 'function', + typeof win?.cordova?.InAppBrowser?.open === 'function', auth, AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, {