diff --git a/packages-exp/auth-exp/cordova/demo/.gitignore b/packages-exp/auth-exp/cordova/demo/.gitignore
new file mode 100644
index 0000000000..24a9d84d53
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/.gitignore
@@ -0,0 +1,5 @@
+platforms/
+plugins/
+ul_web_hooks/
+src/config.js
+config.xml
\ No newline at end of file
diff --git a/packages-exp/auth-exp/cordova/demo/README.md b/packages-exp/auth-exp/cordova/demo/README.md
new file mode 100644
index 0000000000..cf72e9ec6d
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/README.md
@@ -0,0 +1,119 @@
+# Cordova Auth Demo App
+This package contains a demo of the various Firebase Auth features bundled in
+an Apache Cordova app.
+
+## Dev Setup
+
+Follow [this guide](https://cordova.apache.org/docs/en/10.x/guide/cli/) to run
+set up Cordova CLI. tl;dr:
+
+```bash
+npm install -g cordova
+cordova requirements
+```
+
+### Preparing the deps
+
+At this point you should have a basic environment set up that can support
+Cordova. Now you'll need to update some config files to make everything work.
+First, read through steps 1 - 5, 7 in the
+[Firebase Auth Cordova docs page](https://firebase.google.com/docs/auth/web/cordova)
+so that you get a sense of the values you need to add / adjust.
+
+Make a copy of `sample-config.xml` to `config.xml`, and replace the values
+outlined in curly braces. Notably, you'll need the package name / bundle ID
+of a registered app in the Firebase Console. You'll also need the Auth Domain
+that comes from the Web config.
+
+Next, you'll need to make a copy of `src/sample-config.js` to `src/config.js`,
+and you'll need to supply a Firebase JS SDK configuration in the form of that
+file.
+
+Once all this is done, you can get the Cordova setup ready in this specific
+project:
+
+```bash
+cordova prepare
+```
+
+Work through any issues until no more errors are printed.
+
+## Building and running the demo
+
+The app consists of a bundled script, `www/dist/bundle.js`. This script is built
+from the `./src` directory. To change it, modify the source code in
+`src` and then rebuild the bundle:
+
+```bash
+# Build the deps the first time, and subsequent times if changing the core SDK
+npm run build:deps
+npm run build:js
+```
+
+### Android
+
+You can now build and test the app on Android Emulator:
+
+```bash
+cordova build android
+cordova emulate android
+
+# Or
+cordova run android
+```
+
+TODO: Any tips or gotchas?
+
+### iOS
+
+```bash
+cordova build ios
+cordova emulate ios
+```
+
+Please ignore the command-line output mentioning `logPath` -- that file
+[will not actually exist](https://github.com/ios-control/ios-sim/issues/167) and
+won't contain JavaScript console logs. The Simulator app itself does not
+expose console logs either (System Log won't help).
+
+The recommended way around this is to use the remote debugger in Safari. Launch
+the Safari app on the same MacBook, and then go to Safari menu > Preferences >
+Advanced and check the "Show Develop menu in menu bar" option. Then, you should
+be able to see the Simulator under the Safari `Developer` menu and choose the
+app web view (Hello World > Hello World). This only works when the Simulator has
+already started and is running the Cordova app. This gives you full access to
+console logs AND uncaught errors in the JavaScript code.
+
+WARNING: This may not catch any JavaScript errors during initialization (or
+before the debugger was opened). If nothing seems working, try clicking the
+Reload Page button in the top-left corner of the inspector, which will reload
+the whole web view and hopefully catch the error this time.
+
+#### Xcode
+
+If you really want to, you can also start the Simulator via Xcode. Note that
+this will only give you access to console log but WON'T show JavaScript errors
+-- for which you still need the Safari remote debugger.
+
+```bash
+cordova build ios
+open ./platforms/ios/HelloWorld.xcworkspace/
+```
+
+Select/add a Simulator through the nav bar and click on "Run" to start it. You
+can then find console logs in the corresponding Xcode panel (not in the
+Simulator window itself).
+
+If you go this route,
+[DO NOT edit files in Xcode IDE](https://cordova.apache.org/docs/en/10.x/guide/platforms/ios/index.html#open-a-project-within-xcode).
+Instead, edit files in the `www` folder and run `cordova build ios` to copy the
+changes over (and over).
+
+### Notes
+
+You may need to update the cordova-universal-links-plugin `manifestWriter.js`
+to point to the correct Android manifest. For example:
+
+```js
+var pathToManifest = path.join(cordovaContext.opts.projectRoot, 'platforms', 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
+```
diff --git a/packages-exp/auth-exp/cordova/demo/package.json b/packages-exp/auth-exp/cordova/demo/package.json
new file mode 100644
index 0000000000..089c945fe3
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "cordova-auth-demo",
+ "displayName": "Cordova Auth Demo",
+ "version": "1.0.0",
+ "scripts": {
+ "build:js": "rollup -c",
+ "build:deps": "lerna run --scope @firebase/'{app-exp,auth-exp/cordova}' --include-dependencies build"
+ },
+ "keywords": [
+ "ecosystem:cordova"
+ ],
+ "devDependencies": {
+ "cordova-android": "^9.1.0",
+ "cordova-ios": "^6.2.0",
+ "cordova-plugin-whitelist": "^1.3.4",
+ "cordova-universal-links-plugin": "^1.2.1",
+ "rollup": "^2.52.2"
+ },
+ "cordova": {
+ "plugins": {
+ "cordova-plugin-whitelist": {},
+ "cordova-plugin-buildinfo": {},
+ "cordova-universal-links-plugin": {},
+ "cordova-plugin-browsertab": {},
+ "cordova-plugin-inappbrowser": {},
+ "cordova-plugin-customurlscheme": {
+ "URL_SCHEME": "com.example.hello",
+ "ANDROID_SCHEME": " ",
+ "ANDROID_HOST": " ",
+ "ANDROID_PATHPREFIX": "/"
+ }
+ },
+ "platforms": [
+ "ios",
+ "android"
+ ]
+ },
+ "dependencies": {
+ "@firebase/app-exp": "0.0.900",
+ "@firebase/auth-exp": "0.0.900",
+ "@firebase/logger": "0.2.6",
+ "@firebase/util": "0.3.4",
+ "cordova-plugin-browsertab": "0.2.0",
+ "cordova-plugin-buildinfo": "4.0.0",
+ "cordova-plugin-compat": "1.2.0",
+ "cordova-plugin-customurlscheme": "5.0.2",
+ "cordova-plugin-inappbrowser": "4.1.0",
+ "cordova-universal-links-plugin-fix": "^1.2.1",
+ "lerna": "^4.0.0",
+ "tslib": "^2.1.0"
+ }
+}
diff --git a/packages-exp/auth-exp/cordova/demo/rollup.config.js b/packages-exp/auth-exp/cordova/demo/rollup.config.js
new file mode 100644
index 0000000000..252e2e6b62
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/rollup.config.js
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright 2019 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 resolve from '@rollup/plugin-node-resolve';
+import strip from '@rollup/plugin-strip';
+
+/**
+ * Common plugins for all builds
+ */
+const commonPlugins = [
+ strip({
+ functions: ['debugAssert.*']
+ })
+];
+
+const es5Builds = [
+ /**
+ * Browser Builds
+ */
+ {
+ input: 'src/index.js',
+ output: [{ file: 'www/dist/bundle.js', format: 'esm', sourcemap: true }],
+ plugins: [
+ ...commonPlugins,
+ resolve({
+ mainFields: ['module', 'main']
+ })
+ ]
+ }
+];
+
+export default [...es5Builds];
diff --git a/packages-exp/auth-exp/cordova/demo/sample-config.xml b/packages-exp/auth-exp/cordova/demo/sample-config.xml
new file mode 100644
index 0000000000..a22a6cd84e
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/sample-config.xml
@@ -0,0 +1,16 @@
+
+
+ Cordova Auth Demo
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages-exp/auth-exp/cordova/demo/src/index.js b/packages-exp/auth-exp/cordova/demo/src/index.js
new file mode 100644
index 0000000000..c8bc760c31
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/src/index.js
@@ -0,0 +1,1568 @@
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/**
+ * @license
+ * Copyright 2017 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.
+ */
+
+/**
+ * @fileoverview This code is for the most part copied over from the packages/auth/demo
+ * package.
+ */
+
+import { initializeApp } from '@firebase/app-exp';
+import {
+ applyActionCode,
+ browserLocalPersistence,
+ browserSessionPersistence,
+ confirmPasswordReset,
+ createUserWithEmailAndPassword,
+ EmailAuthProvider,
+ fetchSignInMethodsForEmail,
+ indexedDBLocalPersistence,
+ initializeAuth,
+ getAuth,
+ inMemoryPersistence,
+ isSignInWithEmailLink,
+ linkWithCredential,
+ multiFactor,
+ reauthenticateWithCredential,
+ sendEmailVerification,
+ sendPasswordResetEmail,
+ sendSignInLinkToEmail,
+ signInAnonymously,
+ signInWithCredential,
+ signInWithCustomToken,
+ signInWithEmailAndPassword,
+ unlink,
+ updateEmail,
+ updatePassword,
+ updateProfile,
+ verifyPasswordResetCode,
+ getMultiFactorResolver,
+ OAuthProvider,
+ GoogleAuthProvider,
+ FacebookAuthProvider,
+ TwitterAuthProvider,
+ GithubAuthProvider,
+ signInWithRedirect,
+ linkWithRedirect,
+ reauthenticateWithRedirect,
+ getRedirectResult,
+ cordovaPopupRedirectResolver
+} from '@firebase/auth-exp/dist/cordova';
+
+import { config } from './config';
+import {
+ alertError,
+ alertNotImplemented,
+ alertSuccess,
+ clearLogs,
+ log,
+ logAtLevel_
+} from './logging';
+
+let app = null;
+let auth = null;
+let currentTab = null;
+let lastUser = null;
+let applicationVerifier = null;
+let multiFactorErrorResolver = null;
+let selectedMultiFactorHint = null;
+let recaptchaSize = 'normal';
+let webWorker = null;
+
+// The corresponding Font Awesome icons for each provider.
+const providersIcons = {
+ 'google.com': 'fa-google',
+ 'facebook.com': 'fa-facebook-official',
+ 'twitter.com': 'fa-twitter-square',
+ 'github.com': 'fa-github',
+ 'yahoo.com': 'fa-yahoo',
+ 'phone': 'fa-phone'
+};
+
+/**
+ * Returns the active user (i.e. currentUser or lastUser).
+ * @return {!firebase.User}
+ */
+function activeUser() {
+ const type = $('input[name=toggle-user-selection]:checked').val();
+ if (type === 'lastUser') {
+ return lastUser;
+ } else {
+ return auth.currentUser;
+ }
+}
+
+/**
+ * Refreshes the current user data in the UI, displaying a user info box if
+ * a user is signed in, or removing it.
+ */
+function refreshUserData() {
+ if (activeUser()) {
+ const user = activeUser();
+ $('.profile').show();
+ $('body').addClass('user-info-displayed');
+ $('div.profile-email,span.profile-email').text(user.email || 'No Email');
+ $('div.profile-phone,span.profile-phone').text(
+ user.phoneNumber || 'No Phone'
+ );
+ $('div.profile-uid,span.profile-uid').text(user.uid);
+ $('div.profile-name,span.profile-name').text(user.displayName || 'No Name');
+ $('input.profile-name').val(user.displayName);
+ $('input.photo-url').val(user.photoURL);
+ if (user.photoURL != null) {
+ let photoURL = user.photoURL;
+ // Append size to the photo URL for Google hosted images to avoid requesting
+ // the image with its original resolution (using more bandwidth than needed)
+ // when it is going to be presented in smaller size.
+ if (
+ photoURL.indexOf('googleusercontent.com') !== -1 ||
+ photoURL.indexOf('ggpht.com') !== -1
+ ) {
+ photoURL = photoURL + '?sz=' + $('img.profile-image').height();
+ }
+ $('img.profile-image').attr('src', photoURL).show();
+ } else {
+ $('img.profile-image').hide();
+ }
+ $('.profile-email-verified').toggle(user.emailVerified);
+ $('.profile-email-not-verified').toggle(!user.emailVerified);
+ $('.profile-anonymous').toggle(user.isAnonymous);
+ // Display/Hide providers icons.
+ $('.profile-providers').empty();
+ if (user['providerData'] && user['providerData'].length) {
+ const providersCount = user['providerData'].length;
+ for (let i = 0; i < providersCount; i++) {
+ addProviderIcon(user['providerData'][i]['providerId']);
+ }
+ }
+ // Show enrolled second factors if available for the active user.
+ showMultiFactorStatus(user);
+ // Change color.
+ if (user === auth.currentUser) {
+ $('#user-info').removeClass('last-user');
+ $('#user-info').addClass('current-user');
+ } else {
+ $('#user-info').removeClass('current-user');
+ $('#user-info').addClass('last-user');
+ }
+ } else {
+ $('.profile').slideUp();
+ $('body').removeClass('user-info-displayed');
+ $('input.profile-data').val('');
+ }
+}
+
+/**
+ * Sets last signed in user and updates UI.
+ * @param {?firebase.User} user The last signed in user.
+ */
+function setLastUser(user) {
+ lastUser = user;
+ if (user) {
+ // Displays the toggle.
+ $('#toggle-user').show();
+ $('#toggle-user-placeholder').hide();
+ } else {
+ $('#toggle-user').hide();
+ $('#toggle-user-placeholder').show();
+ }
+}
+
+/**
+ * Add a provider icon to the profile info.
+ * @param {string} providerId The providerId of the provider.
+ */
+function addProviderIcon(providerId) {
+ const pElt = $('')
+ .addClass('fa ' + providersIcons[providerId])
+ .attr('title', providerId)
+ .data({
+ 'toggle': 'tooltip',
+ 'placement': 'bottom'
+ });
+ $('.profile-providers').append(pElt);
+ pElt.tooltip();
+}
+
+/**
+ * Updates the active user's multi-factor enrollment status.
+ * @param {!firebase.User} activeUser The corresponding user.
+ */
+function showMultiFactorStatus(activeUser) {
+ mfaUser = multiFactor(activeUser);
+ const enrolledFactors = (mfaUser && mfaUser.enrolledFactors) || [];
+ const $listGroup = $('#user-info .dropdown-menu.enrolled-second-factors');
+ // Hide the drop down menu initially.
+ $listGroup.empty().parent().hide();
+ if (enrolledFactors.length) {
+ // If enrolled factors are available, show the drop down menu.
+ $listGroup.parent().show();
+ // Populate the enrolled factors.
+ showMultiFactors(
+ $listGroup,
+ enrolledFactors,
+ // On row click, do nothing. This is needed to prevent the drop down
+ // menu from closing.
+ e => {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ // On delete click unenroll the selected factor.
+ function (e) {
+ e.preventDefault();
+ // Get the corresponding second factor index.
+ const index = parseInt($(this).attr('data-index'), 10);
+ // Get the second factor info.
+ const info = enrolledFactors[index];
+ // Get the display name. If not available, use uid.
+ const label = info && (info.displayName || info.uid);
+ if (label) {
+ $('#enrolled-factors-drop-down').removeClass('open');
+ mfaUser.unenroll(info).then(() => {
+ refreshUserData();
+ alertSuccess('Multi-factor successfully unenrolled.');
+ }, onAuthError);
+ }
+ }
+ );
+ }
+}
+
+/**
+ * Updates the UI when the user is successfully authenticated.
+ * @param {!firebase.User} user User authenticated.
+ */
+function onAuthSuccess(user) {
+ console.log(user);
+ alertSuccess('User authenticated, id: ' + user.uid);
+ refreshUserData();
+}
+
+/**
+ * Displays an error message when the authentication failed.
+ * @param {!Error} error Error message to display.
+ */
+function onAuthError(error) {
+ logAtLevel_(error, 'error');
+ if (error.code === 'auth/multi-factor-auth-required') {
+ // Handle second factor sign-in.
+ handleMultiFactorSignIn(getMultiFactorResolver(auth, error));
+ } else {
+ alertError('Error: ' + error.code);
+ }
+}
+
+/**
+ * Changes the UI when the user has been signed out.
+ */
+function signOut() {
+ log('User successfully signed out.');
+ alertSuccess('User successfully signed out.');
+ refreshUserData();
+}
+
+/**
+ * Saves the new language code provided in the language code input field.
+ */
+function onSetLanguageCode() {
+ const languageCode = $('#language-code').val() || null;
+ try {
+ auth.languageCode = languageCode;
+ alertSuccess('Language code changed to "' + languageCode + '".');
+ } catch (error) {
+ alertError('Error: ' + error.code);
+ }
+}
+
+/**
+ * Switches Auth instance language to device language.
+ */
+function onUseDeviceLanguage() {
+ auth.useDeviceLanguage();
+ $('#language-code').val(auth.languageCode);
+ alertSuccess('Using device language "' + auth.languageCode + '".');
+}
+
+/**
+ * Changes the Auth state persistence to the specified one.
+ */
+function onSetPersistence() {
+ const type = $('#persistence-type').val();
+ let persistence;
+ switch (type) {
+ case 'local':
+ persistence = browserLocalPersistence;
+ break;
+ case 'session':
+ persistence = browserSessionPersistence;
+ break;
+ case 'indexedDB':
+ persistence = indexedDBLocalPersistence;
+ break;
+ case 'none':
+ persistence = inMemoryPersistence;
+ break;
+ default:
+ alertError('Unexpected persistence type: ' + type);
+ }
+ try {
+ auth.setPersistence(persistence).then(
+ () => {
+ log('Persistence state change to "' + type + '".');
+ alertSuccess('Persistence state change to "' + type + '".');
+ },
+ error => {
+ alertError('Error: ' + error.code);
+ }
+ );
+ } catch (error) {
+ alertError('Error: ' + error.code);
+ }
+}
+
+/**
+ * Signs up a new user with an email and a password.
+ */
+function onSignUp() {
+ const email = $('#signup-email').val();
+ const password = $('#signup-password').val();
+ createUserWithEmailAndPassword(auth, email, password).then(
+ onAuthUserCredentialSuccess,
+ onAuthError
+ );
+}
+
+/**
+ * Signs in a user with an email and a password.
+ */
+function onSignInWithEmailAndPassword() {
+ const email = $('#signin-email').val();
+ const password = $('#signin-password').val();
+ signInWithEmailAndPassword(auth, email, password).then(
+ onAuthUserCredentialSuccess,
+ onAuthError
+ );
+}
+
+/**
+ * Signs in a user with an email link.
+ */
+function onSignInWithEmailLink() {
+ const email = $('#sign-in-with-email-link-email').val();
+ const link = $('#sign-in-with-email-link-link').val() || undefined;
+ if (isSignInWithEmailLink(auth, link)) {
+ signInWithEmailLink(auth, email, link).then(onAuthSuccess, onAuthError);
+ } else {
+ alertError('Sign in link is invalid');
+ }
+}
+
+/**
+ * Links a user with an email link.
+ */
+function onLinkWithEmailLink() {
+ const email = $('#link-with-email-link-email').val();
+ const link = $('#link-with-email-link-link').val() || undefined;
+ const credential = EmailAuthProvider.credentialWithLink(email, link);
+ linkWithCredential(activeUser(), credential).then(
+ onAuthUserCredentialSuccess,
+ onAuthError
+ );
+}
+
+/**
+ * Re-authenticate a user with email link credential.
+ */
+function onReauthenticateWithEmailLink() {
+ const email = $('#link-with-email-link-email').val();
+ const link = $('#link-with-email-link-link').val() || undefined;
+ const credential = EmailAuthProvider.credentialWithLink(email, link);
+ reauthenticateWithCredential(activeUser(), credential).then(result => {
+ logAdditionalUserInfo(result);
+ refreshUserData();
+ alertSuccess('User reauthenticated!');
+ }, onAuthError);
+}
+
+/**
+ * Signs in with a custom token.
+ * @param {DOMEvent} _event HTML DOM event returned by the listener.
+ */
+function onSignInWithCustomToken(_event) {
+ // The token can be directly specified on the html element.
+ const token = $('#user-custom-token').val();
+
+ signInWithCustomToken(auth, token).then(
+ onAuthUserCredentialSuccess,
+ onAuthError
+ );
+}
+
+/**
+ * Signs in anonymously.
+ */
+function onSignInAnonymously() {
+ signInAnonymously(auth).then(onAuthUserCredentialSuccess, onAuthError);
+}
+
+/**
+ * Signs in with a generic IdP credential.
+ */
+function onSignInWithGenericIdPCredential() {
+ alertNotImplemented();
+ // var providerId = $('#signin-generic-idp-provider-id').val();
+ // var idToken = $('#signin-generic-idp-id-token').val() || undefined;
+ // var rawNonce = $('#signin-generic-idp-raw-nonce').val() || undefined;
+ // var accessToken = $('#signin-generic-idp-access-token').val() || undefined;
+ // var provider = new OAuthProvider(providerId);
+ // signInWithCredential(
+ // auth,
+ // provider.credential({
+ // idToken: idToken,
+ // accessToken: accessToken,
+ // rawNonce: rawNonce,
+ // })).then(onAuthUserCredentialSuccess, onAuthError);
+}
+
+/** @return {!Object} The Action Code Settings object. */
+function getActionCodeSettings() {
+ const actionCodeSettings = {};
+ const url = $('#continueUrl').val();
+ const apn = $('#apn').val();
+ const amv = $('#amv').val();
+ const ibi = $('#ibi').val();
+ const installApp = $('input[name=install-app]:checked').val() === 'Yes';
+ const handleCodeInApp =
+ $('input[name=handle-in-app]:checked').val() === 'Yes';
+ if (url || apn || ibi) {
+ actionCodeSettings['url'] = url;
+ if (apn) {
+ actionCodeSettings['android'] = {
+ 'packageName': apn,
+ 'installApp': !!installApp,
+ 'minimumVersion': amv || undefined
+ };
+ }
+ if (ibi) {
+ actionCodeSettings['iOS'] = {
+ 'bundleId': ibi
+ };
+ }
+ actionCodeSettings['handleCodeInApp'] = handleCodeInApp;
+ }
+ return actionCodeSettings;
+}
+
+/** Reset action code settings form. */
+function onActionCodeSettingsReset() {
+ $('#continueUrl').val('');
+ $('#apn').val('');
+ $('#amv').val('');
+ $('#ibi').val('');
+}
+
+/**
+ * Changes the user's email.
+ */
+function onChangeEmail() {
+ const email = $('#changed-email').val();
+ updateEmail(activeUser(), email).then(() => {
+ refreshUserData();
+ alertSuccess('Email changed!');
+ }, onAuthError);
+}
+
+/**
+ * Changes the user's password.
+ */
+function onChangePassword() {
+ const password = $('#changed-password').val();
+ updatePassword(activeUser(), password).then(() => {
+ refreshUserData();
+ alertSuccess('Password changed!');
+ }, onAuthError);
+}
+
+/**
+ * Changes the user's password.
+ */
+function onUpdateProfile() {
+ const displayName = $('#display-name').val();
+ const photoURL = $('#photo-url').val();
+ updateProfile(activeUser(), {
+ displayName,
+ photoURL
+ }).then(() => {
+ refreshUserData();
+ alertSuccess('Profile updated!');
+ }, onAuthError);
+}
+
+/**
+ * Sends sign in with email link to the user.
+ */
+function onSendSignInLinkToEmail() {
+ const email = $('#sign-in-with-email-link-email').val();
+ sendSignInLinkToEmail(auth, email, getActionCodeSettings()).then(() => {
+ alertSuccess('Email sent!');
+ }, onAuthError);
+}
+
+/**
+ * Sends sign in with email link to the user and pass in current url.
+ */
+function onSendSignInLinkToEmailCurrentUrl() {
+ const email = $('#sign-in-with-email-link-email').val();
+ const actionCodeSettings = {
+ 'url': window.location.href,
+ 'handleCodeInApp': true
+ };
+
+ sendSignInLinkToEmail(auth, email, actionCodeSettings).then(() => {
+ if ('localStorage' in window && window['localStorage'] !== null) {
+ window.localStorage.setItem(
+ 'emailForSignIn',
+ // Save the email and the timestamp.
+ JSON.stringify({
+ email,
+ timestamp: new Date().getTime()
+ })
+ );
+ }
+ alertSuccess('Email sent!');
+ }, onAuthError);
+}
+
+/**
+ * Sends email link to link the user.
+ */
+function onSendLinkEmailLink() {
+ const email = $('#link-with-email-link-email').val();
+ sendSignInLinkToEmail(auth, email, getActionCodeSettings()).then(() => {
+ alertSuccess('Email sent!');
+ }, onAuthError);
+}
+
+/**
+ * Sends password reset email to the user.
+ */
+function onSendPasswordResetEmail() {
+ const email = $('#password-reset-email').val();
+ sendPasswordResetEmail(auth, email, getActionCodeSettings()).then(() => {
+ alertSuccess('Email sent!');
+ }, onAuthError);
+}
+
+/**
+ * Verifies the password reset code entered by the user.
+ */
+function onVerifyPasswordResetCode() {
+ const code = $('#password-reset-code').val();
+ verifyPasswordResetCode(auth, code).then(() => {
+ alertSuccess('Password reset code is valid!');
+ }, onAuthError);
+}
+
+/**
+ * Confirms the password reset with the code and password supplied by the user.
+ */
+function onConfirmPasswordReset() {
+ const code = $('#password-reset-code').val();
+ const password = $('#password-reset-password').val();
+ confirmPasswordReset(auth, code, password).then(() => {
+ alertSuccess('Password has been changed!');
+ }, onAuthError);
+}
+
+/**
+ * Gets the list of possible sign in methods for the given email address.
+ */
+function onFetchSignInMethodsForEmail() {
+ const email = $('#fetch-sign-in-methods-email').val();
+ fetchSignInMethodsForEmail(auth, email).then(signInMethods => {
+ log('Sign in methods for ' + email + ' :');
+ log(signInMethods);
+ if (signInMethods.length === 0) {
+ alertSuccess('Sign In Methods for ' + email + ': N/A');
+ } else {
+ alertSuccess(
+ 'Sign In Methods for ' + email + ': ' + signInMethods.join(', ')
+ );
+ }
+ }, onAuthError);
+}
+
+/**
+ * Fetches and logs the user's providers data.
+ */
+function onGetProviderData() {
+ log('Providers data:');
+ log(activeUser()['providerData']);
+}
+
+/**
+ * Links a signed in user with an email and password account.
+ */
+function onLinkWithEmailAndPassword() {
+ const email = $('#link-email').val();
+ const password = $('#link-password').val();
+ linkWithCredential(
+ activeUser(),
+ EmailAuthProvider.credential(email, password)
+ ).then(onAuthUserCredentialSuccess, onAuthError);
+}
+
+/**
+ * Links with a generic IdP credential.
+ */
+function onLinkWithGenericIdPCredential() {
+ alertNotImplemented();
+ // var providerId = $('#link-generic-idp-provider-id').val();
+ // var idToken = $('#link-generic-idp-id-token').val() || undefined;
+ // var rawNonce = $('#link-generic-idp-raw-nonce').val() || undefined;
+ // var accessToken = $('#link-generic-idp-access-token').val() || undefined;
+ // var provider = new OAuthProvider(providerId);
+ // activeUser().linkWithCredential(
+ // provider.credential({
+ // idToken: idToken,
+ // accessToken: accessToken,
+ // rawNonce: rawNonce,
+ // })).then(onAuthUserCredentialSuccess, onAuthError);
+}
+
+/**
+ * Unlinks the specified provider.
+ */
+function onUnlinkProvider() {
+ const providerId = $('#unlinked-provider-id').val();
+ unlink(activeUser(), providerId).then(_user => {
+ alertSuccess('Provider unlinked from user.');
+ refreshUserData();
+ }, onAuthError);
+}
+
+/**
+ * Sends email verification to the user.
+ */
+function onSendEmailVerification() {
+ sendEmailVerification(activeUser(), getActionCodeSettings()).then(() => {
+ alertSuccess('Email verification sent!');
+ }, onAuthError);
+}
+
+/**
+ * Confirms the email verification code given.
+ */
+function onApplyActionCode() {
+ var code = $('#email-verification-code').val();
+ applyActionCode(auth, code).then(function () {
+ alertSuccess('Email successfully verified!');
+ refreshUserData();
+ }, onAuthError);
+}
+
+/**
+ * Gets or refreshes the ID token.
+ * @param {boolean} forceRefresh Whether to force the refresh of the token
+ * or not.
+ */
+function getIdToken(forceRefresh) {
+ if (activeUser() == null) {
+ alertError('No user logged in.');
+ return;
+ }
+ if (activeUser().getIdToken) {
+ activeUser()
+ .getIdToken(forceRefresh)
+ .then(alertSuccess, () => {
+ log('No token');
+ });
+ } else {
+ activeUser()
+ .getToken(forceRefresh)
+ .then(alertSuccess, () => {
+ log('No token');
+ });
+ }
+}
+
+/**
+ * Gets or refreshes the ID token result.
+ * @param {boolean} forceRefresh Whether to force the refresh of the token
+ * or not
+ */
+function getIdTokenResult(forceRefresh) {
+ if (activeUser() == null) {
+ alertError('No user logged in.');
+ return;
+ }
+ activeUser()
+ .getIdTokenResult(forceRefresh)
+ .then(idTokenResult => {
+ alertSuccess(JSON.stringify(idTokenResult));
+ }, onAuthError);
+}
+
+/**
+ * Triggers the retrieval of the ID token result.
+ */
+function onGetIdTokenResult() {
+ getIdTokenResult(false);
+}
+
+/**
+ * Triggers the refresh of the ID token result.
+ */
+function onRefreshTokenResult() {
+ getIdTokenResult(true);
+}
+
+/**
+ * Triggers the retrieval of the ID token.
+ */
+function onGetIdToken() {
+ getIdToken(false);
+}
+
+/**
+ * Triggers the refresh of the ID token.
+ */
+function onRefreshToken() {
+ getIdToken(true);
+}
+
+/**
+ * Signs out the user.
+ */
+function onSignOut() {
+ setLastUser(auth.currentUser);
+ auth.signOut().then(signOut, onAuthError);
+}
+
+/**
+ * Handles multi-factor sign-in completion.
+ * @param {!MultiFactorResolver} resolver The multi-factor error
+ * resolver.
+ */
+function handleMultiFactorSignIn(resolver) {
+ // Save multi-factor error resolver.
+ multiFactorErrorResolver = resolver;
+ // Populate 2nd factor options from resolver.
+ const $listGroup = $('#multiFactorModal div.enrolled-second-factors');
+ // Populate the list of 2nd factors in the list group specified.
+ showMultiFactors(
+ $listGroup,
+ multiFactorErrorResolver.hints,
+ // On row click, select the corresponding second factor to complete
+ // sign-in with.
+ function (e) {
+ e.preventDefault();
+ // Remove all other active entries.
+ $listGroup.find('a').removeClass('active');
+ // Mark current entry as active.
+ $(this).addClass('active');
+ // Select current factor.
+ onSelectMultiFactorHint(parseInt($(this).attr('data-index'), 10));
+ },
+ // Do not show delete option
+ null
+ );
+ // Hide phone form (other second factor types could be supported).
+ $('#multi-factor-phone').addClass('hidden');
+ // Show second factor recovery dialog.
+ $('#multiFactorModal').modal();
+}
+
+/**
+ * Displays the list of multi-factors in the provided list group.
+ * @param {!jQuery} $listGroup The list group where the enrolled
+ * factors will be displayed.
+ * @param {!Array} multiFactorInfo The list of
+ * multi-factors to display.
+ * @param {?function(!jQuery.Event)} onClick The click handler when a second
+ * factor is clicked.
+ * @param {?function(!jQuery.Event)} onDelete The click handler when a second
+ * factor is delete. If not provided, no delete button is shown.
+ */
+function showMultiFactors($listGroup, multiFactorInfo, onClick, onDelete) {
+ // Append entry to list.
+ $listGroup.empty();
+ $.each(multiFactorInfo, i => {
+ // Append entry to list.
+ const info = multiFactorInfo[i];
+ const displayName = info.displayName || 'N/A';
+ const $a = $('')
+ .addClass('list-group-item')
+ .addClass('list-group-item-action')
+ // Set index on entry.
+ .attr('data-index', i)
+ .appendTo($listGroup);
+ $a.append($('').text(info.uid));
+ $a.append($('').text(info.factorId));
+ $a.append($('
').text(displayName));
+ if (info.phoneNumber) {
+ $a.append($('').text(info.phoneNumber));
+ }
+ // Check if a delete button is to be displayed.
+ if (onDelete) {
+ const $deleteBtn = $(
+ '' +
+ '' +
+ ''
+ );
+ // Append delete button to row.
+ $a.append($deleteBtn);
+ // Add delete button click handler.
+ $a.find('button.delete-factor').click(onDelete);
+ }
+ // On entry click.
+ if (onClick) {
+ $a.click(onClick);
+ }
+ });
+}
+
+/**
+ * Handles the user selection of second factor to complete sign-in with.
+ * @param {number} index The selected multi-factor hint index.
+ */
+function onSelectMultiFactorHint(index) {
+ // Hide all forms for handling each type of second factors.
+ // Currently only phone is supported.
+ $('#multi-factor-phone').addClass('hidden');
+ if (
+ !multiFactorErrorResolver ||
+ typeof multiFactorErrorResolver.hints[index] === 'undefined'
+ ) {
+ return;
+ }
+
+ if (multiFactorErrorResolver.hints[index].factorId === 'phone') {
+ // Save selected second factor.
+ selectedMultiFactorHint = multiFactorErrorResolver.hints[index];
+ // Show options for phone 2nd factor.
+ // Get reCAPTCHA ready.
+ clearApplicationVerifier();
+ makeApplicationVerifier('send-2fa-phone-code');
+ // Show sign-in with phone second factor menu.
+ $('#multi-factor-phone').removeClass('hidden');
+ // Clear all input.
+ $('#multi-factor-sign-in-verification-id').val('');
+ $('#multi-factor-sign-in-verification-code').val('');
+ } else {
+ // 2nd factor not found or not supported by app.
+ alertError('Selected 2nd factor is not supported!');
+ }
+}
+
+/**
+ * Adds a new row to insert an OAuth custom parameter key/value pair.
+ * @param {!jQuery.Event} _event The jQuery event object.
+ */
+function onPopupRedirectAddCustomParam(_event) {
+ // Form container.
+ let html = '';
+ // Create jQuery node.
+ const $node = $(html);
+ // Add button click event listener to remove item.
+ $node.find('button').on('click', function (e) {
+ // Remove button click event listener.
+ $(this).off('click');
+ // Get row container and remove it.
+ $(this).closest('form.customParamItem').remove();
+ e.preventDefault();
+ });
+ // Append constructed row to parameter list container.
+ $('#popup-redirect-custom-parameters').append($node);
+}
+
+/**
+ * Performs the corresponding popup/redirect action for a generic provider.
+ */
+function onPopupRedirectGenericProviderClick() {
+ var providerId = $('#popup-redirect-generic-providerid').val();
+ var provider = new OAuthProvider(providerId);
+ signInWithPopupRedirect(provider);
+}
+
+/**
+ * Performs the corresponding popup/redirect action for a SAML provider.
+ */
+function onPopupRedirectSamlProviderClick() {
+ alertNotImplemented();
+ // var providerId = $('#popup-redirect-saml-providerid').val();
+ // var provider = new SAMLAuthProvider(providerId);
+ // signInWithPopupRedirect(provider);
+}
+
+/**
+ * Performs the corresponding popup/redirect action based on user's selection.
+ * @param {!jQuery.Event} _event The jQuery event object.
+ */
+function onPopupRedirectProviderClick(_event) {
+ const providerId = $(event.currentTarget).data('provider');
+ let provider = null;
+ switch (providerId) {
+ case 'google.com':
+ provider = new GoogleAuthProvider();
+ break;
+ case 'facebook.com':
+ provider = new FacebookAuthProvider();
+ break;
+ case 'github.com':
+ provider = new GithubAuthProvider();
+ break;
+ case 'twitter.com':
+ provider = new TwitterAuthProvider();
+ break;
+ default:
+ return;
+ }
+ signInWithPopupRedirect(provider);
+}
+
+/**
+ * Performs a popup/redirect action based on a given provider and the user's
+ * selections.
+ * @param {!AuthProvider} provider The provider with which to
+ * sign in.
+ */
+function signInWithPopupRedirect(provider) {
+ let action = $('input[name=popup-redirect-action]:checked').val();
+ let type = $('input[name=popup-redirect-type]:checked').val();
+ let method = null;
+ let inst = null;
+
+ switch (action) {
+ case 'link':
+ if (!activeUser()) {
+ alertError('No user logged in.');
+ return;
+ }
+ inst = activeUser();
+ method = type === 'popup' ? alertNotImplemented() : linkWithRedirect;
+ break;
+ case 'reauthenticate':
+ if (!activeUser()) {
+ alertError('No user logged in.');
+ return;
+ }
+ inst = activeUser();
+ method =
+ type === 'popup' ? alertNotImplemented() : reauthenticateWithRedirect;
+ break;
+ default:
+ inst = auth;
+ method = type === 'popup' ? alertNotImplemented() : signInWithRedirect;
+ }
+
+ // Get custom OAuth parameters.
+ const customParameters = {};
+ // For each entry.
+ $('form.customParamItem').each(function (_index) {
+ // Get parameter key.
+ const key = $(this).find('input.customParamKey').val();
+ // Get parameter value.
+ const value = $(this).find('input.customParamValue').val();
+ // Save to list if valid.
+ if (key && value) {
+ customParameters[key] = value;
+ }
+ });
+ console.log('customParameters: ', customParameters);
+ // For older jscore versions that do not support this.
+ if (provider.setCustomParameters) {
+ // Set custom parameters on current provider.
+ provider.setCustomParameters(customParameters);
+ }
+
+ // Add scopes for providers who do have scopes available (i.e. not Twitter).
+ if (provider.addScope) {
+ // String.prototype.trim not available in IE8.
+ const scopes = $.trim($('#scopes').val()).split(/\s*,\s*/);
+ for (let i = 0; i < scopes.length; i++) {
+ provider.addScope(scopes[i]);
+ }
+ }
+ console.log('Provider:');
+ console.log(provider);
+ method(inst, provider, cordovaPopupRedirectResolver)
+ .then(() => getRedirectResult(auth))
+ .then(response => {
+ console.log('Popup response:');
+ console.log(response);
+ alertSuccess(action + ' with ' + provider['providerId'] + ' successful!');
+ logAdditionalUserInfo(response);
+ onAuthSuccess(activeUser());
+ }, onAuthError);
+}
+
+/**
+ * Displays user credential result.
+ * @param {!UserCredential} result The UserCredential result
+ * object.
+ */
+function onAuthUserCredentialSuccess(result) {
+ onAuthSuccess(result.user);
+ logAdditionalUserInfo(result);
+}
+
+/**
+ * Displays redirect result.
+ */
+function onGetRedirectResult() {
+ getRedirectResult(auth, cordovaPopupRedirectResolver).then(function (
+ response
+ ) {
+ log('Redirect results:');
+ if (response.credential) {
+ log('Credential:');
+ log(response.credential);
+ } else {
+ log('No credential');
+ }
+ if (response.user) {
+ log("User's id:");
+ log(response.user.uid);
+ } else {
+ log('No user');
+ }
+ logAdditionalUserInfo(response);
+ console.log(response);
+ },
+ onAuthError);
+}
+
+/**
+ * Logs additional user info returned by a sign-in event, if available.
+ * @param {!Object} response
+ */
+function logAdditionalUserInfo(response) {
+ if (response?.additionalUserInfo) {
+ if (response.additionalUserInfo.username) {
+ log(
+ response.additionalUserInfo['providerId'] +
+ ' username: ' +
+ response.additionalUserInfo.username
+ );
+ }
+ if (response.additionalUserInfo.profile) {
+ log(response.additionalUserInfo['providerId'] + ' profile information:');
+ log(JSON.stringify(response.additionalUserInfo.profile, null, 2));
+ }
+ if (typeof response.additionalUserInfo.isNewUser !== 'undefined') {
+ log(
+ response.additionalUserInfo['providerId'] +
+ ' isNewUser: ' +
+ response.additionalUserInfo.isNewUser
+ );
+ }
+ if (response.credential) {
+ log('credential: ' + JSON.stringify(response.credential.toJSON()));
+ }
+ }
+}
+
+/**
+ * Deletes the user account.
+ */
+function onDelete() {
+ activeUser()
+ ['delete']()
+ .then(() => {
+ log('User successfully deleted.');
+ alertSuccess('User successfully deleted.');
+ refreshUserData();
+ }, onAuthError);
+}
+
+/**
+ * Gets a specific query parameter from the current URL.
+ * @param {string} name Name of the parameter.
+ * @return {string} The query parameter requested.
+ */
+function getParameterByName(name) {
+ const url = window.location.href;
+ name = name.replace(/[\[\]]/g, '\\$&');
+ const regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)');
+ const results = regex.exec(url);
+ if (!results) {
+ return null;
+ }
+ if (!results[2]) {
+ return '';
+ }
+ return decodeURIComponent(results[2].replace(/\+/g, ' '));
+}
+
+/**
+ * Detects if an action code is passed in the URL, and populates accordingly
+ * the input field for the confirm email verification process.
+ */
+function populateActionCodes() {
+ let emailForSignIn = null;
+ let signInTime = 0;
+ if ('localStorage' in window && window['localStorage'] !== null) {
+ try {
+ // Try to parse as JSON first using new storage format.
+ const emailForSignInData = JSON.parse(
+ window.localStorage.getItem('emailForSignIn')
+ );
+ emailForSignIn = emailForSignInData['email'] || null;
+ signInTime = emailForSignInData['timestamp'] || 0;
+ } catch (e) {
+ // JSON parsing failed. This means the email is stored in the old string
+ // format.
+ emailForSignIn = window.localStorage.getItem('emailForSignIn');
+ }
+ if (emailForSignIn) {
+ // Clear old codes. Old format codes should be cleared immediately.
+ if (new Date().getTime() - signInTime >= 1 * 24 * 3600 * 1000) {
+ // Remove email from storage.
+ window.localStorage.removeItem('emailForSignIn');
+ }
+ }
+ }
+ const actionCode = getParameterByName('oobCode');
+ if (actionCode != null) {
+ const mode = getParameterByName('mode');
+ if (mode === 'verifyEmail') {
+ $('#email-verification-code').val(actionCode);
+ } else if (mode === 'resetPassword') {
+ $('#password-reset-code').val(actionCode);
+ } else if (mode === 'signIn') {
+ if (emailForSignIn) {
+ $('#sign-in-with-email-link-email').val(emailForSignIn);
+ $('#sign-in-with-email-link-link').val(window.location.href);
+ onSignInWithEmailLink();
+ // Remove email from storage as the code is only usable once.
+ window.localStorage.removeItem('emailForSignIn');
+ }
+ } else {
+ $('#email-verification-code').val(actionCode);
+ $('#password-reset-code').val(actionCode);
+ }
+ }
+}
+
+/**
+ * Provides basic Database checks for authenticated and unauthenticated access.
+ * The Database node being tested has the following rule:
+ * "users": {
+ * "$user_id": {
+ * ".read": "$user_id === auth.uid",
+ * ".write": "$user_id === auth.uid"
+ * }
+ * }
+ * This applies when Real-time database service is available.
+ */
+function checkDatabaseAuthAccess() {
+ const randomString = Math.floor(Math.random() * 10000000).toString();
+ let dbRef;
+ let dbPath;
+ let errMessage;
+ // Run this check only when Database module is available.
+ if (
+ typeof firebase !== 'undefined' &&
+ typeof firebase.database !== 'undefined'
+ ) {
+ if (lastUser && !auth.currentUser) {
+ dbPath = 'users/' + lastUser.uid;
+ // After sign out, confirm read/write access to users/$user_id blocked.
+ dbRef = firebase.database().ref(dbPath);
+ dbRef
+ .set({
+ 'test': randomString
+ })
+ .then(() => {
+ alertError(
+ 'Error: Unauthenticated write to Database node ' +
+ dbPath +
+ ' unexpectedly succeeded!'
+ );
+ })
+ .catch(error => {
+ errMessage = error.message.toLowerCase();
+ // Permission denied error should be thrown.
+ if (errMessage.indexOf('permission_denied') === -1) {
+ alertError('Error: ' + error.code);
+ return;
+ }
+ dbRef
+ .once('value')
+ .then(() => {
+ alertError(
+ 'Error: Unauthenticated read to Database node ' +
+ dbPath +
+ ' unexpectedly succeeded!'
+ );
+ })
+ .catch(error => {
+ errMessage = error.message.toLowerCase();
+ // Permission denied error should be thrown.
+ if (errMessage.indexOf('permission_denied') === -1) {
+ alertError('Error: ' + error.code);
+ return;
+ }
+ log(
+ 'Unauthenticated read/write to Database node ' +
+ dbPath +
+ ' failed as expected!'
+ );
+ });
+ });
+ } else if (auth.currentUser) {
+ dbPath = 'users/' + auth.currentUser.uid;
+ // Confirm read/write access to users/$user_id allowed.
+ dbRef = firebase.database().ref(dbPath);
+ dbRef
+ .set({
+ 'test': randomString
+ })
+ .then(() => {
+ return dbRef.once('value');
+ })
+ .then(snapshot => {
+ if (snapshot.val().test === randomString) {
+ // read/write successful.
+ log(
+ 'Authenticated read/write to Database node ' +
+ dbPath +
+ ' succeeded!'
+ );
+ } else {
+ throw new Error(
+ 'Authenticated read/write to Database node ' + dbPath + ' failed!'
+ );
+ }
+ // Clean up: clear that node's content.
+ return dbRef.remove();
+ })
+ .catch(error => {
+ alertError('Error: ' + error.code);
+ });
+ }
+ }
+}
+
+/** Copy current user of auth to tempAuth. */
+function onCopyActiveUser() {
+ tempAuth.updateCurrentUser(activeUser()).then(
+ () => {
+ alertSuccess('Copied active user to temp Auth');
+ },
+ error => {
+ alertError('Error: ' + error.code);
+ }
+ );
+}
+
+/** Copy last user to auth. */
+function onCopyLastUser() {
+ // If last user is null, NULL_USER error will be thrown.
+ auth.updateCurrentUser(lastUser).then(
+ () => {
+ alertSuccess('Copied last user to Auth');
+ },
+ error => {
+ alertError('Error: ' + error.code);
+ }
+ );
+}
+
+/** Applies selected auth settings change. */
+function onApplyAuthSettingsChange() {
+ try {
+ auth.settings.appVerificationDisabledForTesting =
+ $('input[name=enable-app-verification]:checked').val() === 'No';
+ alertSuccess('Auth settings changed');
+ } catch (error) {
+ alertError('Error: ' + error.code);
+ }
+}
+
+/**
+ * Initiates the application by setting event listeners on the various buttons.
+ */
+function initApp() {
+ log('Initializing app...');
+ app = initializeApp(config);
+ auth = getAuth(app);
+
+ tempApp = initializeApp(
+ {
+ apiKey: config.apiKey,
+ authDomain: config.authDomain
+ },
+ `${auth.name}-temp`
+ );
+ tempAuth = initializeAuth(tempApp, {
+ persistence: inMemoryPersistence,
+ popupRedirectResolver: cordovaPopupRedirectResolver
+ });
+
+ // Listen to reCAPTCHA config togglers.
+ initRecaptchaToggle(size => {
+ clearApplicationVerifier();
+ recaptchaSize = size;
+ });
+
+ // The action code for email verification or password reset
+ // can be passed in the url address as a parameter, and for convenience
+ // this preloads the input field.
+ populateActionCodes();
+
+ // Allows to login the user if previously logged in.
+ if (auth.onIdTokenChanged) {
+ auth.onIdTokenChanged(user => {
+ refreshUserData();
+ if (user) {
+ user.getIdTokenResult(false).then(
+ idTokenResult => {
+ log(JSON.stringify(idTokenResult));
+ },
+ () => {
+ log('No token.');
+ }
+ );
+ } else {
+ log('No user logged in.');
+ }
+ });
+ }
+
+ if (auth.onAuthStateChanged) {
+ auth.onAuthStateChanged(user => {
+ if (user) {
+ log('user state change detected: ' + user.uid);
+ } else {
+ log('user state change detected: no user');
+ }
+ // Check Database Auth access.
+ checkDatabaseAuthAccess();
+ });
+ }
+
+ if (tempAuth.onAuthStateChanged) {
+ tempAuth.onAuthStateChanged(user => {
+ if (user) {
+ log('user state change on temp Auth detect: ' + JSON.stringify(user));
+ alertSuccess('user state change on temp Auth detect: ' + user.uid);
+ }
+ });
+ }
+
+ /**
+ * @fileoverview Utilities for Auth test app features.
+ */
+
+ /**
+ * Initializes the widget for toggling reCAPTCHA size.
+ * @param {function(string):void} callback The callback to call when the
+ * size toggler is changed, which takes in the new reCAPTCHA size.
+ */
+ function initRecaptchaToggle(callback) {
+ // Listen to recaptcha config togglers.
+ const $recaptchaConfigTogglers = $('.toggleRecaptcha');
+ $recaptchaConfigTogglers.click(function (e) {
+ // Remove currently active option.
+ $recaptchaConfigTogglers.removeClass('active');
+ // Set currently selected option.
+ $(this).addClass('active');
+ // Get the current reCAPTCHA setting label.
+ const size = $(e.target).text().toLowerCase();
+ callback(size);
+ });
+ }
+
+ // Install servicerWorker if supported.
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker
+ .register('/service-worker.js')
+ .then(reg => {
+ // Registration worked.
+ console.log('Registration succeeded. Scope is ' + reg.scope);
+ })
+ .catch(error => {
+ // Registration failed.
+ console.log('Registration failed with ' + error.message);
+ });
+ }
+
+ if (window.Worker) {
+ webWorker = new Worker('/web-worker.js');
+ /**
+ * Handles the incoming message from the web worker.
+ * @param {!Object} e The message event received.
+ */
+ webWorker.onmessage = function (e) {
+ console.log('User data passed through web worker: ', e.data);
+ switch (e.data.type) {
+ case 'GET_USER_INFO':
+ alertSuccess(
+ 'User data passed through web worker: ' + JSON.stringify(e.data)
+ );
+ break;
+ case 'RUN_TESTS':
+ if (e.data.status === 'success') {
+ alertSuccess('Web worker tests ran successfully!');
+ } else {
+ alertError('Error: ' + JSON.stringify(e.data.error));
+ }
+ break;
+ default:
+ return;
+ }
+ };
+ }
+
+ /**
+ * Asks the web worker, if supported in current browser, to return the user info
+ * corresponding to the currentUser as seen within the worker.
+ */
+ function onGetCurrentUserDataFromWebWorker() {
+ if (webWorker) {
+ webWorker.postMessage({ type: 'GET_USER_INFO' });
+ } else {
+ alertError(
+ 'Error: Web workers are not supported in the current browser!'
+ );
+ }
+ }
+
+ // We check for redirect result to refresh user's data.
+ getRedirectResult(auth, cordovaPopupRedirectResolver).then(function (
+ response
+ ) {
+ refreshUserData();
+ logAdditionalUserInfo(response);
+ },
+ onAuthError);
+
+ // Bootstrap tooltips.
+ $('[data-toggle="tooltip"]').tooltip();
+
+ // Auto submit the choose library type form.
+ $('#library-form').on('change', 'input.library-option', () => {
+ $('#library-form').submit();
+ });
+
+ // To clear the logs in the page.
+ $('.clear-logs').click(clearLogs);
+
+ // Disables JS forms.
+ $('form.no-submit').on('submit', () => {
+ return false;
+ });
+
+ // Keeps track of the current tab opened.
+ $('#tab-menu a').click(event => {
+ currentTab = $(event.currentTarget).attr('href');
+ });
+
+ // Toggles user.
+ $('input[name=toggle-user-selection]').change(refreshUserData);
+
+ // Actions listeners.
+ $('#sign-up-with-email-and-password').click(onSignUp);
+ $('#sign-in-with-email-and-password').click(onSignInWithEmailAndPassword);
+ $('.sign-in-with-custom-token').click(onSignInWithCustomToken);
+ $('#sign-in-anonymously').click(onSignInAnonymously);
+ $('#sign-in-with-generic-idp-credential').click(
+ onSignInWithGenericIdPCredential
+ );
+ $('#sign-in-with-email-link').click(onSignInWithEmailLink);
+ $('#link-with-email-link').click(onLinkWithEmailLink);
+ $('#reauth-with-email-link').click(onReauthenticateWithEmailLink);
+
+ $('#change-email').click(onChangeEmail);
+ $('#change-password').click(onChangePassword);
+ $('#update-profile').click(onUpdateProfile);
+
+ $('#send-sign-in-link-to-email').click(onSendSignInLinkToEmail);
+ $('#send-sign-in-link-to-email-current-url').click(
+ onSendSignInLinkToEmailCurrentUrl
+ );
+ $('#send-link-email-link').click(onSendLinkEmailLink);
+
+ $('#send-password-reset-email').click(onSendPasswordResetEmail);
+ $('#verify-password-reset-code').click(onVerifyPasswordResetCode);
+ $('#confirm-password-reset').click(onConfirmPasswordReset);
+
+ $('#get-provider-data').click(onGetProviderData);
+ $('#link-with-email-and-password').click(onLinkWithEmailAndPassword);
+ $('#link-with-generic-idp-credential').click(onLinkWithGenericIdPCredential);
+ $('#unlink-provider').click(onUnlinkProvider);
+
+ $('#send-email-verification').click(onSendEmailVerification);
+ $('#confirm-email-verification').click(onApplyActionCode);
+ $('#get-token-result').click(onGetIdTokenResult);
+ $('#refresh-token-result').click(onRefreshTokenResult);
+ $('#get-token').click(onGetIdToken);
+ $('#refresh-token').click(onRefreshToken);
+ $('#get-token-worker').click(onGetCurrentUserDataFromWebWorker);
+ $('#sign-out').click(onSignOut);
+
+ $('.popup-redirect-provider').click(onPopupRedirectProviderClick);
+ $('#popup-redirect-generic').click(onPopupRedirectGenericProviderClick);
+ $('#popup-redirect-get-redirect-result').click(onGetRedirectResult);
+ $('#popup-redirect-add-custom-parameter').click(
+ onPopupRedirectAddCustomParam
+ );
+ $('#popup-redirect-saml').click(onPopupRedirectSamlProviderClick);
+
+ $('#action-code-settings-reset').click(onActionCodeSettingsReset);
+
+ $('#delete').click(onDelete);
+
+ $('#set-persistence').click(onSetPersistence);
+
+ $('#set-language-code').click(onSetLanguageCode);
+ $('#use-device-language').click(onUseDeviceLanguage);
+
+ $('#fetch-sign-in-methods-for-email').click(onFetchSignInMethodsForEmail);
+
+ $('#copy-active-user').click(onCopyActiveUser);
+ $('#copy-last-user').click(onCopyLastUser);
+
+ $('#apply-auth-settings-change').click(onApplyAuthSettingsChange);
+}
+
+document.addEventListener(
+ 'deviceready',
+ function () {
+ initApp();
+ },
+ false
+);
diff --git a/packages-exp/auth-exp/cordova/demo/src/logging.js b/packages-exp/auth-exp/cordova/demo/src/logging.js
new file mode 100644
index 0000000000..dae567befe
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/src/logging.js
@@ -0,0 +1,121 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+/**
+ * @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.
+ */
+
+// Fix for IE8 when developer's console is not opened.
+if (!window.console) {
+ window.console = {
+ log() {},
+ error() {}
+ };
+}
+
+/**
+ * Logs the message in the console and on the log window in the app
+ * using the level given.
+ * @param {?Object} message Object or message to log.
+ * @param {string} level The level of log (log, error, debug).
+ * @private
+ */
+export function logAtLevel_(message, level) {
+ if (message != null) {
+ const messageDiv = $('');
+ messageDiv.addClass(level);
+ if (typeof message === 'object') {
+ messageDiv.text(JSON.stringify(message, null, ' '));
+ } else {
+ messageDiv.text(message);
+ }
+ $('.logs').append(messageDiv);
+ }
+ console[level](message);
+}
+
+/**
+ * Logs info level.
+ * @param {string} message Object or message to log.
+ */
+export function log(message) {
+ logAtLevel_(message, 'log');
+}
+
+/**
+ * Clear the logs.
+ */
+export function clearLogs() {
+ $('.logs').text('');
+}
+
+/**
+ * Displays for a few seconds a box with a specific message and then fades
+ * it out.
+ * @param {string} message Small message to display.
+ * @param {string} cssClass The class(s) to give the alert box.
+ * @private
+ */
+function alertMessage_(message, cssClass) {
+ const alertBox = $('')
+ .addClass(cssClass)
+ .css('display', 'none')
+ .text(message);
+ // When modals are visible, display the alert in the modal layer above the
+ // grey background.
+ const visibleModal = $('.modal.in');
+ if (visibleModal.size() > 0) {
+ // Check first if the model has an overlaying-alert. If not, append the
+ // overlaying-alert container.
+ if (visibleModal.find('.overlaying-alert').size() === 0) {
+ const $overlayingAlert = $(
+ ''
+ );
+ visibleModal.append($overlayingAlert);
+ }
+ visibleModal.find('.overlaying-alert').prepend(alertBox);
+ } else {
+ $('#alert-messages').prepend(alertBox);
+ }
+ alertBox.fadeIn({
+ complete() {
+ setTimeout(() => {
+ alertBox.slideUp(400, () => {
+ // On completion, remove the alert element from the DOM.
+ alertBox.remove();
+ });
+ }, 3000);
+ }
+ });
+}
+
+/**
+ * Alerts a small success message in a overlaying alert box.
+ * @param {string} message Small message to display.
+ */
+export function alertSuccess(message) {
+ alertMessage_(message, 'alert alert-success');
+}
+
+/**
+ * Alerts a small error message in a overlaying alert box.
+ * @param {string} message Small message to display.
+ */
+export function alertError(message) {
+ alertMessage_(message, 'alert alert-danger');
+}
+
+export function alertNotImplemented() {
+ alertError('Method not yet implemented in the new SDK');
+}
diff --git a/packages-exp/auth-exp/cordova/demo/src/sample-config.js b/packages-exp/auth-exp/cordova/demo/src/sample-config.js
new file mode 100644
index 0000000000..871a1674d5
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/src/sample-config.js
@@ -0,0 +1,23 @@
+/*
+ * @license
+ * Copyright 2017 Google LLC All Rights Reserved.
+ *
+ * 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.
+ */
+
+export const config = {
+ apiKey: 'YOUR_API_KEY',
+ authDomain: 'your-app.firebaseapp.com',
+ databaseURL: 'https://your-app.firebaseio.com',
+ projectId: 'your-app',
+ storageBucket: 'your-app.appspot.com',
+ messagingSenderId: 'MESSAGING_SENDER_ID'
+};
diff --git a/packages-exp/auth-exp/cordova/demo/www/index.html b/packages-exp/auth-exp/cordova/demo/www/index.html
new file mode 100644
index 0000000000..a7199342fa
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/www/index.html
@@ -0,0 +1,664 @@
+
+
+
+
+ Headless App
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
[anonymous] /
+ uid:
+
+
+
+
+
/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Auth State Persistence
+
+
+
+
Language code
+
+
+
+
Sign Up
+
+
+
+
+
Sign In
+
+
+
+
+
+
+
Sign In with Email Link
+
+
+
+
+
Password Reset
+
+
+
+
Fetch Sign In Methods
+
+
+
+
Update Current User
+
+
+
+
+
+
Update Profile
+
+
+
+
+
+
+
Linking/Unlinking
+
+
+
+
+
+
+
+
+
+
Enroll Second Factor
+
+
+
+
Other Actions
+
+
+
+
+
+
+
+
+
+
Delete account
+
+
+
+
+
Web
+
+
+
+
Android
+
+
iOS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages-exp/auth-exp/cordova/demo/www/style.css b/packages-exp/auth-exp/cordova/demo/www/style.css
new file mode 100644
index 0000000000..c9f10255d0
--- /dev/null
+++ b/packages-exp/auth-exp/cordova/demo/www/style.css
@@ -0,0 +1,207 @@
+/**
+ * Copyright 2017 Google Inc.
+ *
+ * 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.
+ */
+
+ body {
+ padding-top: 70px;
+}
+body.user-info-displayed {
+ padding-top: 120px;
+}
+
+@media (min-width: 768px) {
+ body {
+ padding-bottom: 100px;
+ }
+}
+@media (max-width: 767px) {
+ body {
+ padding-bottom: 15px;
+ }
+}
+
+#tab-menu {
+ margin-bottom: 15px;
+}
+
+#user-info {
+ display: none;
+ padding: 10px 15px;
+ position: fixed;
+ top: 50px;
+ width: 100%;
+ z-index: 1000;
+}
+
+#toggle-user-placeholder {
+ margin-bottom: 15px;
+}
+
+#user-info.current-user,
+#toggle-user-placeholder .current-user,
+#toggle-user label.active.current-user {
+ background-color: #d6e9c6;
+ border: 1px solid #dff0d8;
+ color: #3c763d;
+}
+
+#user-info.last-user,
+#toggle-user label.active.last-user {
+ background-color: #d9edf7;
+ border: 1px solid #bce8f1;
+ color: #31708f;
+}
+
+#recaptcha-container {
+ border: 5px solid red;
+ bottom: 0;
+ position: fixed;
+ right: 0;
+ z-index: 1500;
+}
+
+#recaptcha-container:empty {
+ border: none;
+}
+
+.logs {
+ color: #555;
+ font-family: 'Courier New', Courier;
+ font-size: 0.9em;
+ word-wrap: break-word;
+}
+
+.logs > .error {
+ color: #d9534f;
+}
+
+/* Margin top for small screens when the logs are below the buttons */
+@media (max-width: 767px) {
+ .logs {
+ margin-top: 20px;
+ }
+}
+
+.overlaying-alert {
+ bottom: 15px;
+ pointer-events: none;
+ position: fixed;
+ width: 100%;
+ word-wrap: break-word;
+ z-index: 1010;
+}
+
+.actions {
+ margin-bottom: 15px;
+}
+
+.group {
+ border-top: 1px solid #555;
+ color: #555;
+ font-size: 0.9em;
+ font-weight: bold;
+ letter-spacing: 0.2em;
+ margin-bottom: 15px;
+ padding: 2px 0 0 10px;
+}
+
+button + .group,
+div + .group,
+.btn-block + .group,
+.form + .group {
+ margin-top: 30px;
+}
+
+.form {
+ text-align: center;
+}
+
+.form-bordered {
+ border: 1px solid #CCC;
+ border-radius: 9px;
+ padding: 5px;
+}
+
+button + .form,
+input + .form,
+.form + .form {
+ margin: 15px 0;
+}
+
+.form + button,
+.form-control + .btn-block,
+.form-control + .form-control {
+ margin-top: 5px;
+}
+
+/* Bootstrap .hidden adds the !important which invalides jQuery .show() */
+.hidden,
+.hide,
+.profile,
+.overlaying-alert > .alert,
+#toggle-user {
+ display: none;
+}
+
+.profile {
+ line-height: 30px;
+}
+
+@media (max-width: 767px) {
+ .profile {
+ /* Use a smaller line height for small screens so user information doesn't
+ take up half the screen. */
+ line-height: normal;
+ }
+}
+
+.profile-uid {
+ font-family: 'Courier New', Courier;
+}
+
+.profile-image {
+ float: left;
+ height: 30px;
+ margin-right: 10px;
+}
+
+.profile-email-not-verified {
+ color: #d9534f;
+}
+
+.profile-providers {
+ color: #333;
+}
+
+.profile-providers > i {
+ margin: 0 5px;
+}
+
+.radio-block {
+ margin-bottom: 15px;
+ width: 100%;
+}
+
+.radio-block > label {
+ width: 50%;
+}
+
+/** Overrides default drop down menu styles for enrolled factors display. */
+.enrolled-second-factors {
+ left: initial;
+ min-width: 400px;
+ right: 0px;
+ width: 100%;
+}