diff --git a/packages-exp/auth-exp/index.cordova.ts b/packages-exp/auth-exp/index.cordova.ts new file mode 100644 index 00000000000..ede598050bd --- /dev/null +++ b/packages-exp/auth-exp/index.cordova.ts @@ -0,0 +1,53 @@ +/** + * @license + * Copyright 2021 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. + */ + +/** + * This is the file that people using Cordova will actually import. You + * should only include this file if you have something specific about your + * implementation that mandates having a separate entrypoint. Otherwise you can + * just use index.ts + */ + +import { FirebaseApp } from '@firebase/app-types-exp'; +import { Auth } from '@firebase/auth-types-exp'; +import { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db'; + +import { initializeAuth } from './src'; +import { registerAuth } from './src/core/auth/register'; +import { ClientPlatform } from './src/core/util/version'; + +// Core functionality shared by all clients +export * from './src'; + +// Cordova also supports indexedDB / browserSession / browserLocal +export { indexedDBLocalPersistence } from './src/platform_browser/persistence/indexed_db'; +export { browserLocalPersistence } from './src/platform_browser/persistence/local_storage'; +export { browserSessionPersistence } from './src/platform_browser/persistence/session_storage'; + +export { cordovaPopupRedirectResolver } from './src/platform_cordova/popup_redirect'; +export { signInWithRedirect } from './src/platform_cordova/strategies/redirect'; + +import { cordovaPopupRedirectResolver } from './src/platform_cordova/popup_redirect'; + +export function getAuth(app: FirebaseApp): Auth { + return initializeAuth(app, { + persistence: indexedDBLocalPersistence, + popupRedirectResolver: cordovaPopupRedirectResolver + }); +} + +registerAuth(ClientPlatform.CORDOVA); diff --git a/packages-exp/auth-exp/package.json b/packages-exp/auth-exp/package.json index fc3679d7b53..a95bfcb9669 100644 --- a/packages-exp/auth-exp/package.json +++ b/packages-exp/auth-exp/package.json @@ -7,6 +7,7 @@ "main": "dist/node/index.js", "react-native": "dist/rn/index.js", "browser": "dist/esm5/index.js", + "cordova": "dist/cordova/index.esm5.js", "module": "dist/esm5/index.js", "esm2017": "dist/esm2017/index.js", "webworker": "dist/index.webworker.esm5.js", @@ -26,7 +27,7 @@ "test:browser:integration": "karma start --single-run --integration", "test:browser:debug": "karma start --auto-watch", "test:browser:unit:debug": "karma start --auto-watch --unit", - "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/!(platform_browser|platform_react_native)/**/*.test.ts' --file index.node.ts --config ../../config/mocharc.node.js", + "test:node": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' nyc --reporter lcovonly -- mocha 'src/!(platform_browser|platform_react_native|platform_cordova)/**/*.test.ts' --file index.node.ts --config ../../config/mocharc.node.js", "api-report": "api-extractor run --local --verbose", "predoc": "node ../../scripts/exp/remove-exp.js temp", "doc": "api-documenter markdown --input temp --output docs", diff --git a/packages-exp/auth-exp/src/core/auth/auth_impl.ts b/packages-exp/auth-exp/src/core/auth/auth_impl.ts index fad70bd6033..cea00e11d32 100644 --- a/packages-exp/auth-exp/src/core/auth/auth_impl.ts +++ b/packages-exp/auth-exp/src/core/auth/auth_impl.ts @@ -101,6 +101,10 @@ export class AuthImpl implements Auth, _FirebaseService { persistenceHierarchy: Persistence[], popupRedirectResolver?: externs.PopupRedirectResolver ): Promise { + if (popupRedirectResolver) { + this._popupRedirectResolver = _getInstance(popupRedirectResolver); + } + // Have to check for app deletion throughout initialization (after each // promise resolution) this._initializationPromise = this.queue(async () => { @@ -108,10 +112,6 @@ export class AuthImpl implements Auth, _FirebaseService { return; } - if (popupRedirectResolver) { - this._popupRedirectResolver = _getInstance(popupRedirectResolver); - } - this.persistenceManager = await PersistenceUserManager.create( this, persistenceHierarchy diff --git a/packages-exp/auth-exp/src/core/auth/register.ts b/packages-exp/auth-exp/src/core/auth/register.ts index 823a0327094..26d195d938d 100644 --- a/packages-exp/auth-exp/src/core/auth/register.ts +++ b/packages-exp/auth-exp/src/core/auth/register.ts @@ -41,6 +41,8 @@ function getVersionForPlatform( return 'rn'; case ClientPlatform.WORKER: return 'webworker'; + case ClientPlatform.CORDOVA: + return 'cordova'; default: return undefined; } diff --git a/packages-exp/auth-exp/src/core/errors.ts b/packages-exp/auth-exp/src/core/errors.ts index e8e24d7cab2..dfab17b858b 100644 --- a/packages-exp/auth-exp/src/core/errors.ts +++ b/packages-exp/auth-exp/src/core/errors.ts @@ -402,6 +402,10 @@ export interface AuthErrorParams extends GenericAuthErrorParams { appName: AppName; serverResponse: IdTokenMfaResponse; }; + [AuthErrorCode.INVALID_CORDOVA_CONFIGURATION]: { + appName: AppName; + missingPlugin?: string; + }; } export const _DEFAULT_AUTH_ERROR_FACTORY = new ErrorFactory< diff --git a/packages-exp/auth-exp/src/core/util/resolver.ts b/packages-exp/auth-exp/src/core/util/resolver.ts new file mode 100644 index 00000000000..b05d0640941 --- /dev/null +++ b/packages-exp/auth-exp/src/core/util/resolver.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2021 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 externs from '@firebase/auth-types-exp'; +import { Auth } from '../../model/auth'; +import { PopupRedirectResolver } from '../../model/popup_redirect'; +import { AuthErrorCode } from '../errors'; +import { _assert } from './assert'; +import { _getInstance } from './instantiator'; + +/** + * Chooses a popup/redirect resolver to use. This prefers the override (which + * is directly passed in), and falls back to the property set on the auth + * object. If neither are available, this function errors w/ an argument error. + * + * @internal + */ +export function _withDefaultResolver( + auth: Auth, + resolverOverride: externs.PopupRedirectResolver | undefined +): PopupRedirectResolver { + if (resolverOverride) { + return _getInstance(resolverOverride); + } + + _assert(auth._popupRedirectResolver, auth, AuthErrorCode.ARGUMENT_ERROR); + + return auth._popupRedirectResolver; +} diff --git a/packages-exp/auth-exp/src/core/util/version.ts b/packages-exp/auth-exp/src/core/util/version.ts index c50f01d9f3f..c1e3e54b366 100644 --- a/packages-exp/auth-exp/src/core/util/version.ts +++ b/packages-exp/auth-exp/src/core/util/version.ts @@ -27,6 +27,7 @@ export const enum ClientPlatform { BROWSER = 'Browser', NODE = 'Node', REACT_NATIVE = 'ReactNative', + CORDOVA = 'Cordova', WORKER = 'Worker' } diff --git a/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts b/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts index cb3e4431f44..cfcb382f45a 100644 --- a/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts +++ b/packages-exp/auth-exp/src/platform_browser/popup_redirect.ts @@ -18,7 +18,6 @@ import { SDK_VERSION } from '@firebase/app-exp'; import * as externs from '@firebase/auth-types-exp'; import { isEmpty, querystring } from '@firebase/util'; -import { _getInstance } from '../core/util/instantiator'; import { AuthEventManager } from '../core/auth/auth_event_manager'; import { AuthErrorCode } from '../core/errors'; @@ -72,26 +71,6 @@ interface ManagerOrPromise { promise?: Promise; } -/** - * Chooses a popup/redirect resolver to use. This prefers the override (which - * is directly passed in), and falls back to the property set on the auth - * object. If neither are available, this function errors w/ an argument error. - * - * @internal - */ -export function _withDefaultResolver( - auth: Auth, - resolverOverride: externs.PopupRedirectResolver | undefined -): PopupRedirectResolver { - if (resolverOverride) { - return _getInstance(resolverOverride); - } - - _assert(auth._popupRedirectResolver, auth, AuthErrorCode.ARGUMENT_ERROR); - - return auth._popupRedirectResolver; -} - class BrowserPopupRedirectResolver implements PopupRedirectResolver { private readonly eventManagers: Record = {}; private readonly iframes: Record = {}; diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts b/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts index 72e50ebfc6b..f3d278b4b9e 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/popup.ts @@ -29,7 +29,7 @@ import { PopupRedirectResolver } from '../../model/popup_redirect'; import { User } from '../../model/user'; -import { _withDefaultResolver } from '../popup_redirect'; +import { _withDefaultResolver } from '../../core/util/resolver'; import { AuthPopup } from '../util/popup'; import { AbstractPopupRedirectOperation } from './abstract_popup_redirect_operation'; diff --git a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts index 45f944224df..fbdf9b934e6 100644 --- a/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts +++ b/packages-exp/auth-exp/src/platform_browser/strategies/redirect.ts @@ -30,7 +30,7 @@ import { PopupRedirectResolver } from '../../model/popup_redirect'; import { User, UserCredential } from '../../model/user'; -import { _withDefaultResolver } from '../popup_redirect'; +import { _withDefaultResolver } from '../../core/util/resolver'; import { AbstractPopupRedirectOperation } from './abstract_popup_redirect_operation'; /** diff --git a/packages-exp/auth-exp/src/platform_cordova/plugins.ts b/packages-exp/auth-exp/src/platform_cordova/plugins.ts new file mode 100644 index 00000000000..8db35822420 --- /dev/null +++ b/packages-exp/auth-exp/src/platform_cordova/plugins.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2021 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. + */ + +// For some reason, the linter doesn't recognize that these are used elsewhere +// in the SDK +/* eslint-disable @typescript-eslint/no-unused-vars */ + +declare namespace cordova.plugins.browsertab { + function isAvailable(cb: (available: boolean) => void): void; + function openUrl(url: string): void; +} + +declare namespace cordova.InAppBrowser { + function open(url: string, target: string, options: string): unknown; +} + +declare namespace universalLinks { + function subscribe(n: null, cb: (event: unknown) => void): void; +} + +declare namespace BuildInfo { + const packageName: string; +} diff --git a/packages-exp/auth-exp/src/platform_cordova/popup_redirect.test.ts b/packages-exp/auth-exp/src/platform_cordova/popup_redirect.test.ts new file mode 100644 index 00000000000..052d4eb9bf0 --- /dev/null +++ b/packages-exp/auth-exp/src/platform_cordova/popup_redirect.test.ts @@ -0,0 +1,143 @@ +/** + * @license + * Copyright 2021 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 chaiAsPromised from 'chai-as-promised'; +import * as sinonChai from 'sinon-chai'; +import { expect, use } from 'chai'; +import { testAuth, TestAuth } from '../../test/helpers/mock_auth'; +import { SingletonInstantiator } from '../core/util/instantiator'; +import { AuthEventType, PopupRedirectResolver } from '../model/popup_redirect'; +import { cordovaPopupRedirectResolver } from './popup_redirect'; +import { GoogleAuthProvider } from '../core/providers/google'; +import { FirebaseError } from '@firebase/util'; + +use(chaiAsPromised); +use(sinonChai); + +describe('platform_cordova/popup_redirect', () => { + let auth: TestAuth; + let resolver: PopupRedirectResolver; + + beforeEach(async () => { + auth = await testAuth(); + attachExpectedPlugins(); + resolver = new (cordovaPopupRedirectResolver as SingletonInstantiator)(); + }); + + describe('_openRedirect plugin checks', () => { + // TODO: Rest of the tests go here + it('does not reject if all plugins installed', () => { + expect(() => + resolver._openRedirect( + auth, + new GoogleAuthProvider(), + AuthEventType.SIGN_IN_VIA_REDIRECT + ) + ).not.to.throw; + }); + + it('rejects if universal links is missing', () => { + removeProp(window, 'universalLinks'); + expect(() => + resolver._openRedirect( + auth, + new GoogleAuthProvider(), + AuthEventType.SIGN_IN_VIA_REDIRECT + ) + ) + .to.throw(FirebaseError, 'auth/invalid-cordova-configuration') + .that.has.deep.property('customData', { + appName: 'test-app', + missingPlugin: 'cordova-universal-links-plugin-fix' + }); + }); + + it('rejects if build info is missing', () => { + removeProp(window.BuildInfo, 'packageName'); + expect(() => + resolver._openRedirect( + auth, + new GoogleAuthProvider(), + AuthEventType.SIGN_IN_VIA_REDIRECT + ) + ) + .to.throw(FirebaseError, 'auth/invalid-cordova-configuration') + .that.has.deep.property('customData', { + appName: 'test-app', + missingPlugin: 'cordova-plugin-buildInfo' + }); + }); + + it('rejects if browsertab openUrl is missing', () => { + removeProp(window.cordova.plugins.browsertab, 'openUrl'); + expect(() => + resolver._openRedirect( + auth, + new GoogleAuthProvider(), + AuthEventType.SIGN_IN_VIA_REDIRECT + ) + ) + .to.throw(FirebaseError, 'auth/invalid-cordova-configuration') + .that.has.deep.property('customData', { + appName: 'test-app', + missingPlugin: 'cordova-plugin-browsertab' + }); + }); + + it('rejects if InAppBrowser is missing', () => { + removeProp(window.cordova.InAppBrowser, 'open'); + expect(() => + resolver._openRedirect( + auth, + new GoogleAuthProvider(), + AuthEventType.SIGN_IN_VIA_REDIRECT + ) + ) + .to.throw(FirebaseError, 'auth/invalid-cordova-configuration') + .that.has.deep.property('customData', { + appName: 'test-app', + missingPlugin: 'cordova-plugin-inappbrowser' + }); + }); + }); +}); + +function attachExpectedPlugins(): void { + // Eventually these will be replaced with full mocks + const win = (window as unknown) as Record; + win.cordova = { + plugins: { + browsertab: { + isAvailable: () => {}, + openUrl: () => {} + } + }, + InAppBrowser: { + open: () => {} + } + }; + win.universalLinks = { + subscribe: () => {} + }; + win.BuildInfo = { + packageName: 'com.example.name.package' + }; +} + +function removeProp(obj: unknown, prop: string): void { + delete (obj as Record)[prop]; +} diff --git a/packages-exp/auth-exp/src/platform_cordova/popup_redirect.ts b/packages-exp/auth-exp/src/platform_cordova/popup_redirect.ts new file mode 100644 index 00000000000..d13e541adfd --- /dev/null +++ b/packages-exp/auth-exp/src/platform_cordova/popup_redirect.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2021 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 './plugins'; +import * as externs from '@firebase/auth-types-exp'; +import { browserSessionPersistence } from '../platform_browser/persistence/session_storage'; +import { Auth } from '../model/auth'; +import { + AuthEventType, + EventManager, + PopupRedirectResolver +} from '../model/popup_redirect'; +import { AuthPopup } from '../platform_browser/util/popup'; +import { _assert, _fail } from '../core/util/assert'; +import { AuthErrorCode } from '../core/errors'; + +class CordovaPopupRedirectResolver implements PopupRedirectResolver { + readonly _redirectPersistence = browserSessionPersistence; + _completeRedirectFn: () => Promise = async () => null; + + _initialize(_auth: Auth): Promise { + throw new Error('Method not implemented.'); + } + _openPopup(auth: Auth): Promise { + _fail(auth, AuthErrorCode.OPERATION_NOT_SUPPORTED); + } + _openRedirect( + auth: Auth, + _provider: externs.AuthProvider, + _authType: AuthEventType, + _eventId?: string + ): Promise { + checkCordovaConfiguration(auth); + return new Promise(() => {}); + } + _isIframeWebStorageSupported( + _auth: Auth, + _cb: (support: boolean) => unknown + ): void { + throw new Error('Method not implemented.'); + } +} + +function checkCordovaConfiguration(auth: Auth): void { + // 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', + auth, + AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, + { + missingPlugin: 'cordova-universal-links-plugin-fix' + } + ); + + // https://www.npmjs.com/package/cordova-plugin-buildinfo + _assert( + typeof window?.BuildInfo?.packageName !== 'undefined', + auth, + AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, + { + missingPlugin: 'cordova-plugin-buildInfo' + } + ); + + // https://github.com/google/cordova-plugin-browsertab + _assert( + typeof window?.cordova?.plugins?.browsertab?.openUrl === 'function', + auth, + AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, + { + missingPlugin: 'cordova-plugin-browsertab' + } + ); + + // https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/ + _assert( + typeof window?.cordova?.InAppBrowser?.open === 'function', + auth, + AuthErrorCode.INVALID_CORDOVA_CONFIGURATION, + { + missingPlugin: 'cordova-plugin-inappbrowser' + } + ); +} + +/** + * An implementation of {@link @firebase/auth-types#PopupRedirectResolver} suitable for Cordova + * based applications. + * + * @public + */ +export const cordovaPopupRedirectResolver: externs.PopupRedirectResolver = CordovaPopupRedirectResolver; diff --git a/packages-exp/auth-exp/src/platform_cordova/strategies/redirect.ts b/packages-exp/auth-exp/src/platform_cordova/strategies/redirect.ts new file mode 100644 index 00000000000..56302aa8af3 --- /dev/null +++ b/packages-exp/auth-exp/src/platform_cordova/strategies/redirect.ts @@ -0,0 +1,46 @@ +/** + * @license + * Copyright 2021 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 externs from '@firebase/auth-types-exp'; +import { _castAuth } from '../../core/auth/auth_impl'; +import { AuthErrorCode } from '../../core/errors'; +import { OAuthProvider } from '../../core/providers/oauth'; +import { _assert } from '../../core/util/assert'; +import { _withDefaultResolver } from '../../core/util/resolver'; +import { AuthEventType } from '../../model/popup_redirect'; + +// TODO: For now this code is largely a duplicate of platform_browser/strategies/redirect. +// It's likely we can just reuse that code + +export async function signInWithRedirect( + auth: externs.Auth, + provider: externs.AuthProvider, + resolver?: externs.PopupRedirectResolver +): Promise { + const authInternal = _castAuth(auth); + _assert( + provider instanceof OAuthProvider, + auth, + AuthErrorCode.ARGUMENT_ERROR + ); + + return _withDefaultResolver(authInternal, resolver)._openRedirect( + authInternal, + provider, + AuthEventType.SIGN_IN_VIA_REDIRECT + ); +}