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 = '

'; + // OAuth parameter key input. + html += + ''; + // OAuth parameter value input. + html += + ''; + // Button to remove current key/value pair. + html += ''; + 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%; +}