From d355eec36b97d337548e5d84e590f2cddada7dcf Mon Sep 17 00:00:00 2001 From: wti806 <32399754+wti806@users.noreply.github.com> Date: Mon, 5 Feb 2018 13:00:42 -0800 Subject: [PATCH] FirebaseUI release (#305) * Implements anonymous user upgrade functionality with FirebaseUI Sanitizes display name in firebaseui. Clears cached `app.getRedirectResult` after `signInWithRedirect` or `linkWithRedirect` resolves. Fixes the bug when account linking doesn't get triggered in Cordova applications. Adds description for building localized npm builds and how to require them. Implements isPendingRedirect needed to tell whether there is a pending redirect opearation. Fixes dangling internal auth state when the firebaseui instance is reset. PiperOrigin-RevId: 184305421 Change-Id: I684d83fd5ba96ea3a78850ce374207a590171cee * Revert some of the changes in soy template. 'for' doesn't work with open source version compiler. Change back to 'foreach' Change-Id: I14040b19cb294b1f4f90cf10400ce575c53414cf * added changelog bumps gstatic CDN version Sort changelog and add one more change fix typo and style --- README.md | 203 +- changelog.txt | 9 +- javascript/testing/acclient.js | 10 +- javascript/testing/auth.js | 9 + javascript/utils/acclient.js | 8 +- javascript/utils/account.js | 2 +- javascript/utils/config.js | 4 +- javascript/utils/phoneauthresult.js | 71 + javascript/utils/phoneauthresult_test.js | 103 + javascript/utils/sni.js | 4 +- javascript/utils/storage.js | 46 +- javascript/utils/storage_test.js | 28 + javascript/utils/util.js | 2 +- javascript/widgets/authui.js | 608 +++- javascript/widgets/authui_test.js | 2617 ++++++++++++++++- javascript/widgets/authuierror.js | 87 + javascript/widgets/authuierror_test.js | 64 + javascript/widgets/config.js | 40 +- javascript/widgets/config_test.js | 48 +- javascript/widgets/exports_app.js | 8 + javascript/widgets/handler/actioncode_test.js | 11 + javascript/widgets/handler/callback.js | 10 +- javascript/widgets/handler/callback_test.js | 275 ++ javascript/widgets/handler/common.js | 156 +- javascript/widgets/handler/common_test.js | 745 +++++ .../widgets/handler/emailmismatch_test.js | 75 + .../widgets/handler/federatedlinking_test.js | 119 +- .../widgets/handler/federatedsignin_test.js | 5 +- javascript/widgets/handler/passwordlinking.js | 12 +- .../widgets/handler/passwordlinking_test.js | 55 + .../widgets/handler/passwordrecovery_test.js | 1 + .../widgets/handler/passwordsignin_test.js | 117 +- javascript/widgets/handler/passwordsignup.js | 21 +- .../widgets/handler/passwordsignup_test.js | 108 + .../widgets/handler/phonesigninfinish.js | 27 +- .../widgets/handler/phonesigninfinish_test.js | 148 + .../widgets/handler/phonesigninstart.js | 9 +- .../widgets/handler/phonesigninstart_test.js | 280 ++ .../widgets/handler/providersignin_test.js | 582 ++++ javascript/widgets/handler/signin_test.js | 1 + javascript/widgets/handler/testhelper.js | 31 +- soy/pages.soy | 4 +- soy/strings.soy | 17 + translations/ar-XB.xtb | 1 + translations/ar.xtb | 1 + translations/bg.xtb | 1 + translations/ca.xtb | 1 + translations/cs.xtb | 1 + translations/da.xtb | 1 + translations/de.xtb | 5 +- translations/el.xtb | 1 + translations/en-GB.xtb | 1 + translations/en-XA.xtb | 1 + translations/es-419.xtb | 1 + translations/es.xtb | 1 + translations/fa.xtb | 19 +- translations/fi.xtb | 1 + translations/fil.xtb | 1 + translations/fr.xtb | 1 + translations/hi.xtb | 1 + translations/hr.xtb | 1 + translations/hu.xtb | 1 + translations/id.xtb | 1 + translations/it.xtb | 1 + translations/iw.xtb | 1 + translations/ja.xtb | 1 + translations/ko.xtb | 1 + translations/lt.xtb | 1 + translations/lv.xtb | 1 + translations/nl.xtb | 1 + translations/no.xtb | 1 + translations/pl.xtb | 1 + translations/pt-PT.xtb | 7 +- translations/pt.xtb | 3 +- translations/ro.xtb | 1 + translations/ru.xtb | 3 +- translations/sk.xtb | 1 + translations/sl.xtb | 1 + translations/sr.xtb | 1 + translations/sv.xtb | 1 + translations/th.xtb | 1 + translations/tr.xtb | 1 + translations/uk.xtb | 1 + translations/vi.xtb | 1 + translations/zh-CN.xtb | 1 + translations/zh-TW.xtb | 1 + 86 files changed, 6710 insertions(+), 145 deletions(-) create mode 100644 javascript/utils/phoneauthresult.js create mode 100644 javascript/utils/phoneauthresult_test.js create mode 100644 javascript/widgets/authuierror.js create mode 100644 javascript/widgets/authuierror_test.js diff --git a/README.md b/README.md index ad040703..81f7ec2b 100644 --- a/README.md +++ b/README.md @@ -63,17 +63,17 @@ Localized versions of the widget are available through the CDN. To use a localiz localized JS library instead of the default library: ```html - - + + ``` where `{LANGUAGE_CODE}` is replaced by the code of the language you want. For example, the French version of the library is available at -`https://www.gstatic.com/firebasejs/ui/2.5.1/firebase-ui-auth__fr.js`. The list of available +`https://www.gstatic.com/firebasejs/ui/2.6.0/firebase-ui-auth__fr.js`. The list of available languages and their respective language codes can be found at [LANGUAGES.md](LANGUAGES.md). Right-to-left languages also require the right-to-left version of the stylesheet, available at -`https://www.gstatic.com/firebasejs/ui/2.5.1/firebase-ui-auth-rtl.css`, instead of the default +`https://www.gstatic.com/firebasejs/ui/2.6.0/firebase-ui-auth-rtl.css`, instead of the default stylesheet. The supported right-to-left languages are Arabic (ar), Farsi (fa), and Hebrew (iw). ### Option 2: npm Module @@ -129,6 +129,9 @@ FirebaseUI includes the following flows: by default.) 6. [Account Chooser](https://www.accountchooser.com/learnmore.html?lang=en) for remembering emails +7. Integration with +[one-tap sign-up](https://developers.google.com/identity/one-tap/web/overview) +8. Ability to upgrade anonymous users through sign-in/sign-up. ### Configuring sign-in providers @@ -203,6 +206,18 @@ for a more in-depth example, showcasing a Single Page Application mode. ``` +When redirecting back from accountchooser.com or Identity Providers like Google +and Facebook, `start()` method needs to be called to finish the sign-in flow. +To check if there is a pending redirect operation to complete a sign-in attempt, +check `isPendingRedirect()` before deciding whether to render FirebaseUI +via `start()`. + +```javascript +if (ui.isPendingRedirect()) { + ui.start('#firebaseui-auth-container', uiConfig); +} +``` + Here is how you would track the Auth state across all your pages: ```html @@ -284,6 +299,19 @@ FirebaseUI supports the following configuration parameters. +autoUpgradeAnonymousUsers +No + + Whether to automatically upgrade existing anonymous users on sign-in/sign-up. + See Upgrading anonymous users. +
+ Default: + false + When set to true, signInFailure callback is + required to be provided to handle merge conflicts. + + + callbacks No @@ -594,6 +622,29 @@ static `signInSuccessUrl` in config. If the callback returns `false` or nothing, the page is not automatically redirected. +#### `signInFailure(error)` + +The `signInFailure` callback is provided to handle any unrecoverable error +encountered during the sign-in process. +The error provided here is a `firebaseui.auth.AuthUIError` error with the +following properties. + +**firebaseui.auth.AuthUIError properties:** + +|Name |Type |Optional |Description | +|---------|----------------|---------|-----------------------| +|`code` |`string` |No |The corresponding error code. Currently the only error code supported is `firebaseui/anonymous-upgrade-merge-conflict` | +|`credential` |`firebase.auth.AuthCredential`|Yes |The existing non-anonymous user credential the user tried to sign in with.| + +**Should return: `Promise|void`** + +FirebaseUI will wait for the returned promise to handle the reported error +before clearing the UI. If no promise is returned, the UI will be cleared on +completion. Even when this callback resolves, `signInSuccess` callback will not +be triggered. + +This callback is required when `autoUpgradeAnonymousUsers` is enabled. + #### `uiShown()` This callback is triggered the first time the widget UI is rendered. This is @@ -624,6 +675,14 @@ FirebaseUI is displayed. // or whether we leave that to developer to handle. return true; }, + signInFailure: function(error) { + // Some unrecoverable error occurred during sign-in. + // Return a promise when error handling is completed and FirebaseUI + // will reset, clearing any UI. This commonly occurs for error code + // 'firebaseui/anonymous-upgrade-merge-conflict' when merge conflict + // occurs. Check below for more details on this. + return handleUIError(error); + }, uiShown: function() { // The widget is rendered. // Hide the loader. @@ -677,6 +736,130 @@ FirebaseUI is displayed. ``` +### Upgrading anonymous users + +#### Enabling anonymous user upgrade + +When an anonymous user signs in or signs up with a permanent account, you want +to be sure the user can continue with what they were doing before signing up. +For example, an anonymous user might have items in their shopping cart. +At check-out, you prompt the user to sign in or sign up. After the user is +signed in, the user's shopping cart should contain any items the user added +while signed in anonymously. + +To support this behavior, FirebaseUI makes it easy to "upgrade" an anonymous +account to a permanent account. To do so, simply set `autoUpgradeAnonymousUsers` +to `true` when you configure the sign-in UI (this option is disabled by +default). + +FirebaseUI links the new credential with the anonymous account using Firebase +Auth's `linkWithCredential` method: +```javascript +anonymousUser.linkWithCredential(permanentCredential); +``` +The user will retain the same `uid` at the end of the flow and all data keyed +on that identifier would still be associated with that same user. + +#### Handling anonymous user upgrade merge conflicts + +There are cases when a user, initially signed in anonymously, tries to +upgrade to an existing Firebase user. For example, a user may have signed up +with a Google credential on another device. When trying to upgrade to the +existing Google user, an error `auth/credential-already-in-use` will be thrown +by Firebase Auth as an existing user cannot be linked to another existing user. +No two users can share the same credential. In that case, both user data +have to be merged before one user is discarded (typically the anonymous user). +In the case above, the anonymous user shopping cart will be copied locally, +the anonymous user will be deleted and then the user is signed in with the +permanent credential. The anonymous user data in temporary storage will be +copied back to the non-anonymous user. + +FirebaseUI will trigger the `signInFailure` callback with an error code +`firebaseui/anonymous-upgrade-merge-conflict` when the above occurs. The error +object will also contain the permanent credential. +Sign-in with the permanent credential should be triggered in the callback to +complete sign-in. +Before sign-in can be completed via +`auth.signInWithCredential(error.credential)`, the data of the anonymous user +must be copied and the anonymous user deleted. After sign-in completion, the +data has to be copied back to the non-anonymous user. An example below +illustrates how this flow would work if user data is persisted using Firebase +Realtime Database. + +**Example:** + +```javascript +// Temp variable to hold the anonymous user data if needed. +var data = null; +// Hold a reference to the anonymous current user. +var anonymousUser = firebase.auth().currentUser; +ui.start('#firebaseui-auth-container', { + // Whether to upgrade anonymous users should be explicitly provided. + // The user must already be signed in anonymously before FirebaseUI is + // rendered. + autoUpgradeAnonymousUsers: true, + signInSuccessUrl: '', + signInOptions: [ + firebase.auth.GoogleAuthProvider.PROVIDER_ID, + firebase.auth.FacebookAuthProvider.PROVIDER_ID, + firebase.auth.EmailAuthProvider.PROVIDER_ID, + firebase.auth.PhoneAuthProvider.PROVIDER_ID + ], + callbacks: { + signInSuccess: function(user, credential, redirectUrl) { + // Process result. This will not trigger on merge conflicts. + // On success redirect to signInSuccessUrl. + return true; + }, + // signInFailure callback must be provided to handle merge conflicts which + // occur when an existing credential is linked to an anonymous user. + signInFailure: function(error) { + // For merge conflicts, the error.code will be + // 'firebaseui/anonymous-upgrade-merge-conflict'. + if (error.code != 'firebaseui/anonymous-upgrade-merge-conflict') { + return Promise.resolve(); + } + // The credential the user tried to sign in with. + var cred = error.credential; + // If using Firebase Realtime Database. The anonymous user data has to be + // copied to the non-anonymous user. + var app = firebase.app(); + // Save anonymous user data first. + return app.database().ref('users/' + firebase.auth().currentUser.uid) + .once('value') + .then(function(snapshot) { + data = snapshot.val(); + // This will trigger onAuthStateChanged listener which + // could trigger a redirect to another page. + // Ensure the upgrade flow is not interrupted by that callback + // and that this is given enough time to complete before + // redirection. + return firebase.auth().signInWithCredential(cred); + }) + .then(function(user) { + // Original Anonymous Auth instance now has the new user. + return app.database().ref('users/' + user.uid).set(data); + }) + .then(function() { + // Delete anonymnous user. + return anonymousUser.delete(); + }).then(function() { + // Clear data in case a new user signs in, and the state change + // triggers. + data = null; + // FirebaseUI will reset and the UI cleared when this promise + // resolves. + // signInSuccess will not run. Successful sign-in logic has to be + // run explicitly. + window.location.assign(''); + }); + + } + } +}); +``` + + ## Customizing FirebaseUI for authentication Currently, FirebaseUI does not offer customization out of the box. However, the @@ -762,6 +945,18 @@ where `{LANGUAGE_CODE}` is replaced by the can be built with `npm run build build-js-fr`. This will create a binary `firebaseui__fr.js` in the `dist/` folder. +To build a localized npm FirebaseUI module, run: +```bash +npm run build build-npm-{LANGUAGE_CODE} +``` +Make sure all underscore symbols in the `LANGUAGE_CODE` are replaced with +dashes. +This will generate `dist/npm__{LANGUAGE_CODE}.js`. +You can then import/require it: +```javascript +import firebaseui from './npm__{LANGUAGE_CODE}'; +``` + ### Running the demo app To run the demo app, you must have a Firebase project set up on the diff --git a/changelog.txt b/changelog.txt index 8b137891..8a518f55 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1 +1,8 @@ - +feature - Implements anonymous user upgrade functionality with FirebaseUI. +feature - Implements `isPendingRedirect` needed to tell whether there is a pending redirect operation. +fixed - Sanitizes display name in firebaseui. +fixed - Clears cached `app.getRedirectResult` after `signInWithRedirect` or `linkWithRedirect` resolves. +fixed - Fixes the bug when account linking doesn't get triggered in Cordova applications. +fixed - Adds description for building localized npm builds and how to require them. +fixed - Fixes dangling internal auth state when the firebaseui instance is reset. +fixed - Updates closure open source builder to latest version fixing dependency on `eval` and older `marked` module which had some vulnerability issues. \ No newline at end of file diff --git a/javascript/testing/acclient.js b/javascript/testing/acclient.js index 4a110bfa..606690bd 100644 --- a/javascript/testing/acclient.js +++ b/javascript/testing/acclient.js @@ -34,6 +34,7 @@ var acClient = goog.require('firebaseui.auth.acClient'); var FakeAcClient = function() { this.selectTried_ = false; this.skipSelect_ = false; + this.preSkip_ = null; this.available_ = true; this.localAccounts_ = null; this.acResult_ = null; @@ -97,9 +98,13 @@ FakeAcClient.prototype.setAvailability = /** * Sets whether to skip selecting an account. * @param {boolean} skip Whether to skip selecting an account. + * @param {function()=} opt_preSkip Callback to invoke before onSkip callback. */ -FakeAcClient.prototype.setSkipSelect = function(skip) { +FakeAcClient.prototype.setSkipSelect = function(skip, opt_preSkip) { this.skipSelect_ = skip; + if (skip) { + this.preSkip_ = opt_preSkip || null; + } }; @@ -176,6 +181,9 @@ FakeAcClient.prototype.trySelectAccount_ = function( this.localAccounts_ = opt_localAccounts; this.callbackUrl_ = opt_callbackUrl; if (this.skipSelect_) { + if (this.preSkip_) { + this.preSkip_(); + } onSkipSelect(this.available_); } }; diff --git a/javascript/testing/auth.js b/javascript/testing/auth.js index 2dfa4f25..2095da4a 100644 --- a/javascript/testing/auth.js +++ b/javascript/testing/auth.js @@ -189,6 +189,8 @@ FakeAuthClient.AuthAsyncMethod = { APPLY_ACTION_CODE: 'applyActionCode', CHECK_ACTION_CODE: 'checkActionCode', CONFIRM_PASSWORD_RESET: 'confirmPasswordReset', + CREATE_USER_AND_RETRIEVE_DATA_WITH_EMAIL_AND_PASSWORD: + 'createUserAndRetrieveDataWithEmailAndPassword', CREATE_USER_WITH_EMAIL_AND_PASSWORD: 'createUserWithEmailAndPassword', FETCH_PROVIDERS_FOR_EMAIL: 'fetchProvidersForEmail', GET_REDIRECT_RESULT: 'getRedirectResult', @@ -196,6 +198,12 @@ FakeAuthClient.AuthAsyncMethod = { SET_PERSISTENCE: 'setPersistence', SIGN_IN_AND_RETRIEVE_DATA_WITH_CREDENTIAL: 'signInAndRetrieveDataWithCredential', + SIGN_IN_AND_RETRIEVE_DATA_WITH_CUSTOM_TOKEN: + 'signInAndRetrieveDataWithCustomToken', + SIGN_IN_AND_RETRIEVE_DATA_WITH_EMAIL_AND_PASSWORD: + 'signInAndRetrieveDataWithEmailAndPassword', + SIGN_IN_ANONYMOUSLY_AND_RETRIEVE_DATA: + 'signInAnonymouslyAndRetrieveData', SIGN_IN_WITH_CREDENTIAL: 'signInWithCredential', SIGN_IN_WITH_CUSTOM_TOKEN: 'signInWithCustomToken', SIGN_IN_WITH_EMAIL_AND_PASSWORD: 'signInWithEmailAndPassword', @@ -242,6 +250,7 @@ FakeAuthClient.UserProperty = { DISPLAY_NAME: 'displayName', EMAIL: 'email', EMAIL_VERIFIED: 'emailVerified', + IS_ANONYMOUS: 'isAnonymous', PHONE_NUMBER: 'phoneNumber', PHOTO_URL: 'photoURL', PROVIDER_DATA: 'providerData', diff --git a/javascript/utils/acclient.js b/javascript/utils/acclient.js index 8613f695..84d3d068 100644 --- a/javascript/utils/acclient.js +++ b/javascript/utils/acclient.js @@ -123,12 +123,12 @@ firebaseui.auth.acClient.init = function( /** * Starts the flow to select an account from accountchooser.com. * It first checks whether accountchooser.com has accounts. If not, - * {@code onSkipSelect} is called instead of redirecting to accountchooser.com. + * `onSkipSelect` is called instead of redirecting to accountchooser.com. * * @param {function(boolean)} onSkipSelect The callback function invoked when * the account selection can be skipped. A boolean availability flag is * passed. It is true if accountchooser.com is available, false otherwise. - * @param {Array} opt_localAccounts The local account + * @param {Array=} opt_localAccounts The local account * list to pass to accountchooser.com. * @param {string=} opt_callbackUrl The URL to return to when the flow finishes. * The default is current URL. @@ -163,7 +163,7 @@ firebaseui.auth.acClient.trySelectAccount = function( /** * Starts the flow to store or update an account into accountchooser.com. * It first checks whether the account needs to be stored or updated. If not, - * the {@code onSkipStore} is called instead of redirecting to + * the `onSkipStore` is called instead of redirecting to * accountchooser.com. * * @param {firebaseui.auth.Account} account The account to add. @@ -298,7 +298,7 @@ firebaseui.auth.acClient.DummyApi.prototype.update = /** * Checkes if accountchooser.com is disabled. The callback is always invoked - * with a {@code true}. + * with a `true`. * * @param {function(boolean=, Object=)} callback The callback function. */ diff --git a/javascript/utils/account.js b/javascript/utils/account.js index d1a86148..18fde0a1 100644 --- a/javascript/utils/account.js +++ b/javascript/utils/account.js @@ -80,7 +80,7 @@ firebaseui.auth.Account.prototype.toPlainObject = function() { /** - * Converts a plain account object to {@code firebaseui.auth.Account}. + * Converts a plain account object to `firebaseui.auth.Account`. * @param {!Object} account The plain object representation of an account. * @return {firebaseui.auth.Account} The account. */ diff --git a/javascript/utils/config.js b/javascript/utils/config.js index a70fb69a..05a0fe8d 100644 --- a/javascript/utils/config.js +++ b/javascript/utils/config.js @@ -97,7 +97,7 @@ firebaseui.auth.Config.prototype.update = function(name, value) { /** * Gets the configuration value for the given name. If an unrecognized name is - * specified, an {@code Error} is thrown. + * specified, an `Error` is thrown. * * @param {string} name The name of the configuration. * @return {*|undefined} The configuration value. @@ -112,7 +112,7 @@ firebaseui.auth.Config.prototype.get = function(name) { /** * Gets the configuration value for the given name. If an unrecognized name is - * specified or the value is not provided, an {@code Error} is thrown. + * specified or the value is not provided, an `Error` is thrown. * * @param {string} name The name of the configuration. * @return {*} The configuration value. diff --git a/javascript/utils/phoneauthresult.js b/javascript/utils/phoneauthresult.js new file mode 100644 index 00000000..c67c48ee --- /dev/null +++ b/javascript/utils/phoneauthresult.js @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Google Inc. 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. + */ + +/** + * @fileoverview FirebaseUI phone Auth result. + */ + +goog.provide('firebaseui.auth.PhoneAuthResult'); + +goog.require('goog.Promise'); + + +/** + * Wrapper object for firebase.auth.ConfirmationResult with additional error + * handler for confirm method. + * @param {!firebase.auth.ConfirmationResult} confirmationResult The + * confirmation result from phone Auth. + * @param {(function(!Error):!goog.Promise)=} opt_errorHandler The error handler + * for confirm method. + * @constructor + */ +firebaseui.auth.PhoneAuthResult = function( + confirmationResult, opt_errorHandler) { + /** + * @const @private {!firebase.auth.ConfirmationResult} The confirmation result + * from a phone number sign-in or link. + */ + this.confirmationResult_ = confirmationResult; + /** + * @const @private {function(*):*} The error handler for confirm method. + * If not provided, the error will be rethrown. + */ + this.errorHandler_ = opt_errorHandler || function(error) {throw error;}; + /** @const {string} The verification ID in confirmation result. */ + this.verificationId = confirmationResult['verificationId']; + +}; + + +/** + * @param {string} verificationCode The verification code. + * @return {!goog.Promise} The user credential. + */ +firebaseui.auth.PhoneAuthResult.prototype.confirm = function(verificationCode) { + return goog.Promise.resolve( + this.confirmationResult_.confirm(verificationCode)) + .thenCatch(this.errorHandler_); +}; + + +/** + * @return {!firebase.auth.ConfirmationResult} The confirmation result. + */ +firebaseui.auth.PhoneAuthResult.prototype.getConfirmationResult = function() { + return this.confirmationResult_; +}; + + + + diff --git a/javascript/utils/phoneauthresult_test.js b/javascript/utils/phoneauthresult_test.js new file mode 100644 index 00000000..449663b4 --- /dev/null +++ b/javascript/utils/phoneauthresult_test.js @@ -0,0 +1,103 @@ +/* + * Copyright 2018 Google Inc. 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. + */ + +/** + * @fileoverview Tests for phoneauthresult.js + */ + +goog.provide('firebaseui.auth.PhoneAuthResultTest'); + +goog.require('firebaseui.auth.PhoneAuthResult'); +goog.require('goog.Promise'); +goog.require('goog.testing.jsunit'); +goog.require('goog.testing.recordFunction'); + +goog.setTestOnly(); + + +function testPhoneAuthResult_defaultErrorHandler() { + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE' + }; + var confirmationResult = { + 'verificationId': '1234567890', + 'confirm': goog.testing.recordFunction(function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + }) + }; + // Test default error handler. + var phoneAuthResult = new firebaseui.auth.PhoneAuthResult(confirmationResult); + assertEquals(confirmationResult, phoneAuthResult.getConfirmationResult()); + return phoneAuthResult.confirm('123456').then(fail, function(error) { + assertEquals(1, confirmationResult.confirm.getCallCount()); + assertObjectEquals(expectedError, error); + }); +} + + +function testPhoneAuthResult_success() { + var cred = { + 'providerId': 'phone', + 'verificationId': '123456abc', + 'verificationCode': '123456' + }; + var expectedUserCredential = { + 'user': {'uid': '1234567890'}, + 'credential': cred, + 'operationType': 'signIn' + }; + var confirmationResult = { + 'verificationId': '1234567890', + 'confirm': goog.testing.recordFunction(function(code) { + assertEquals('123456', code); + return goog.Promise.resolve(expectedUserCredential); + }) + }; + var phoneAuthResult = new firebaseui.auth.PhoneAuthResult(confirmationResult); + assertEquals(confirmationResult, phoneAuthResult.getConfirmationResult()); + return phoneAuthResult.confirm('123456').then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + }); +} + + +function testPhoneAuthResult_errorHandlerProvided() { + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE' + }; + var confirmationResult = { + 'verificationId': '1234567890', + 'confirm': goog.testing.recordFunction(function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + }) + }; + // Test with provided error handler. + var errorHandler = goog.testing.recordFunction(function(error) { + assertEquals(1, confirmationResult.confirm.getCallCount()); + assertEquals(expectedError, error); + throw error; + }); + var phoneAuthResult = new firebaseui.auth.PhoneAuthResult( + confirmationResult, errorHandler); + assertEquals(confirmationResult, phoneAuthResult.getConfirmationResult()); + return phoneAuthResult.confirm('123456').then(fail, function(error) { + assertEquals(1, errorHandler.getCallCount()); + assertEquals(expectedError, errorHandler.getLastCall().getArgument(0)); + assertEquals(expectedError, error); + }); +} diff --git a/javascript/utils/sni.js b/javascript/utils/sni.js index 01ade1e1..5882b53a 100644 --- a/javascript/utils/sni.js +++ b/javascript/utils/sni.js @@ -155,7 +155,7 @@ firebaseui.auth.sni.Version.prototype.compare = function(version) { * Checks the version is equal to or greater than another one. * * @param {firebaseui.auth.sni.Version|string} version The version to compare. - * @return {boolean} {@code true} if it's equal to or greater than the other. + * @return {boolean} `true` if it's equal to or greater than the other. */ firebaseui.auth.sni.Version.prototype.ge = function(version) { return this.compare(version) >= 0; @@ -167,7 +167,7 @@ firebaseui.auth.sni.Version.prototype.ge = function(version) { * * @param {string=} opt_userAgent The user agent string. If not provided, * window.navigator.userAgent. - * @return {boolean} {@code true} if SNI is supported. + * @return {boolean} `true` if SNI is supported. */ firebaseui.auth.sni.isSupported = function(opt_userAgent) { var ua = opt_userAgent || (window.navigator && window.navigator.userAgent); diff --git a/javascript/utils/storage.js b/javascript/utils/storage.js index ddb76d8f..821d9a8c 100644 --- a/javascript/utils/storage.js +++ b/javascript/utils/storage.js @@ -84,6 +84,7 @@ storage.isAvailable = function() { storage.Key = { // Temporary storage. PENDING_EMAIL_CREDENTIAL: {name: 'pendingEmailCredential', persistent: false}, + PENDING_REDIRECT_KEY: {name: 'pendingRedirect', persistent: false}, REDIRECT_URL: {name: 'redirectUrl', persistent: false}, REMEMBER_ACCOUNT: {name: 'rememberAccount', persistent: false}, @@ -91,6 +92,11 @@ storage.Key = { REMEMBERED_ACCOUNTS: {name: 'rememberedAccounts', persistent: true} }; +/** + * @const @private{string} The pending redirect flag. + */ +storage.PENDING_FLAG_ = 'pending'; + /** * @param {boolean} persistent Whether to use the persistent storage. @@ -221,7 +227,7 @@ storage.hasRememberAccount = function(opt_id) { /** * @param {string=} opt_id When operating in multiple app mode, this ID * associates storage values with specific apps. - * @return {boolean} Whether or not to remember the account. {@code false} is + * @return {boolean} Whether or not to remember the account. `false` is * returned if there is no such setting. */ storage.isRememberAccount = function(opt_id) { @@ -357,4 +363,42 @@ storage.setPendingEmailCredential = function(pendingEmailCredential, opt_id) { pendingEmailCredential.toPlainObject(), opt_id); }; + + +/** + * @param {string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + * @return {boolean} Whether there is a pending redirect operation for the + * provided app ID. + */ +storage.hasPendingRedirectStatus = function(opt_id) { + return ( + storage.get_(storage.Key.PENDING_REDIRECT_KEY, opt_id) === + storage.PENDING_FLAG_); +}; + + +/** + * Removes the stored pending redirect status for provided app ID. + * + * @param {string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + */ +storage.removePendingRedirectStatus = function(opt_id) { + storage.remove_(storage.Key.PENDING_REDIRECT_KEY, opt_id); +}; + + +/** + * Stores the pending redirect status for the provided application ID。 + * + * @param {string=} opt_id When operating in multiple app mode, this ID + * associates storage values with specific apps. + */ +storage.setPendingRedirectStatus = function(opt_id) { + storage.set_( + storage.Key.PENDING_REDIRECT_KEY, + storage.PENDING_FLAG_, + opt_id); +}; }); diff --git a/javascript/utils/storage_test.js b/javascript/utils/storage_test.js index b94fa2f3..c3c46a96 100644 --- a/javascript/utils/storage_test.js +++ b/javascript/utils/storage_test.js @@ -290,3 +290,31 @@ function testGetSetRemoveEmailPendingCredential_withAppId() { firebaseui.auth.storage.removePendingEmailCredential(appId2); assertFalse(firebaseui.auth.storage.hasPendingEmailCredential(appId2)); } + + +function testGetSetRemovePendingRedirectStatus() { + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus()); + + firebaseui.auth.storage.setPendingRedirectStatus(); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus()); + + firebaseui.auth.storage.removePendingRedirectStatus(); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus()); +} + + +function testGetSetRemovePendingRedirectStatus_withAppId() { + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(appId)); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(appId2)); + + firebaseui.auth.storage.setPendingRedirectStatus(appId); + firebaseui.auth.storage.setPendingRedirectStatus(appId2); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(appId)); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(appId2)); + + firebaseui.auth.storage.removePendingRedirectStatus(appId); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(appId)); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(appId2)); + firebaseui.auth.storage.removePendingRedirectStatus(appId2); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(appId2)); +} diff --git a/javascript/utils/util.js b/javascript/utils/util.js index 7b395329..9c34c120 100644 --- a/javascript/utils/util.js +++ b/javascript/utils/util.js @@ -170,7 +170,7 @@ firebaseui.auth.util.popup = /** * Gets the element in the current document by the query selector. * If an Element is passed in, it is returned. - * An {@code Error} is thrown if the element can not be found. + * An `Error` is thrown if the element can not be found. * * @param {string|Element} element The element or the query selector. * @param {string=} opt_notFoundDesc Error description when element not diff --git a/javascript/widgets/authui.js b/javascript/widgets/authui.js index 81a0464b..d558d7ce 100644 --- a/javascript/widgets/authui.js +++ b/javascript/widgets/authui.js @@ -19,8 +19,10 @@ goog.provide('firebaseui.auth.AuthUI'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.EventDispatcher'); goog.require('firebaseui.auth.GoogleYolo'); +goog.require('firebaseui.auth.PhoneAuthResult'); goog.require('firebaseui.auth.log'); goog.require('firebaseui.auth.storage'); goog.require('firebaseui.auth.util'); @@ -141,6 +143,19 @@ firebaseui.auth.AuthUI = function(auth, opt_appId) { // to include it. /** @private {!firebaseui.auth.GoogleYolo} The One-Tap UI wrapper. */ this.googleYolo_ = firebaseui.auth.GoogleYolo.getInstance(); + /** + * @private {?firebase.Promise} Promise that resolves when internal Auth + * instance is signed out, after which start is safe to execute. + */ + this.pendingInternalAuthSignOut_ = null; + /** + * @private {?firebase.User} The latest current user on the external Auth + * instance. This is currently only relevant for upgrade anonymous user + * flows. + */ + this.currentUser_ = null; + /** @private {boolean} Whether initial external Auth state is ready. */ + this.initialStateReady_ = false; }; @@ -235,8 +250,56 @@ firebaseui.auth.AuthUI.prototype.getRedirectResult = function() { // Check if instance is already destroyed. this.checkIfDestroyed_(); if (!this.getRedirectResult_) { - this.getRedirectResult_ = goog.Promise.resolve( - this.getAuth().getRedirectResult()); + var self = this; + var cb = function(user) { + if (user && + // If email already exists, user must first be signed in it to the + // existing account on the internal Auth instance before the + // credential is linked and merge conflict is triggered. + !firebaseui.auth.storage.getPendingEmailCredential(self.getAppId())) { + // Anonymous user eligible for upgrade detected. + // Linking occurs on external Auth instance. + // This could occur when anonymous user linking with redirect fails for + // some reason. + return /** @type {!goog.Promise} */ ( + goog.Promise.resolve(self.getExternalAuth().getRedirectResult() + .then(function(result) { + // Should not happen in real life as this will only run when the + // user is anonymous. + return result; + }, function(error) { + // This will trigger account linking flow. + if (error && + error['code'] == 'auth/email-already-in-use' && + error['email'] && error['credential']) { + throw error; + } + return self.onUpgradeError(error); + }))); + } else { + // No eligible anonymous user detected on external instance. + // Get redirect result from internal instance first. + return goog.Promise.resolve( + self.getAuth().getRedirectResult().then(function(result) { + // Anonymous user could have successfully completed + // linkWithRedirect. + if (self.getConfig().autoUpgradeAnonymousUsers() && + // No user signed in on internal instance. + !result['user'] && + // Current non anonymous user available on external instance. + self.currentUser_ && + !self.currentUser_['isAnonymous']) { + // Return redirect result from external instance. + return self.getExternalAuth().getRedirectResult(); + } + // Return redirect result from internal instance. + return result; + })); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + this.getRedirectResult_ = this.initializeForAutoUpgrade_(cb); } return this.getRedirectResult_; }; @@ -331,9 +394,22 @@ firebaseui.auth.AuthUI.prototype.isPending = function() { }; +/** + * Returns true if there is any pending redirect operations to be resolved by + * the widget. + * @return {boolean} Whether the app has pending redirect operations to be + * performed. + */ +firebaseui.auth.AuthUI.prototype.isPendingRedirect = function() { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + return firebaseui.auth.storage.hasPendingRedirectStatus(this.getAppId()); +}; + + /** * Handles the FirebaseUI operation. - * An {@code Error} is thrown if the the developer tries to run this operation + * An `Error` is thrown if the developer tries to run this operation * more than once. * * @param {string|!Element} element The container element or the query selector. @@ -359,17 +435,36 @@ firebaseui.auth.AuthUI.prototype.start = function(element, config) { // These changes will be ignored as only the first accountchooser.com related // config will be applied. this.setConfig(config); - + // Removes pending status of previous redirect operations including redirect + // back from accountchooser.com and federated sign in. + firebaseui.auth.storage.removePendingRedirectStatus(this.getAppId()); + // Checks if there is pending internal Auth signOut promise. If yes, wait + // until it resolved and then initElement. var doc = goog.global.document; - // Wrap it in a onload callback to wait for the DOM element is rendered. - // If document already loaded, render immediately. - if (doc.readyState == 'complete') { - this.initElement_(element); + if (this.pendingInternalAuthSignOut_) { + this.pendingInternalAuthSignOut_.then(function() { + // Wrap it in a onload callback to wait for the DOM element is rendered. + // If document already loaded, render immediately. + if (doc.readyState == 'complete') { + self.initElement_(element); + } else { + // Document not ready, wait for load before rendering. + goog.events.listenOnce(window, goog.events.EventType.LOAD, function() { + self.initElement_(element); + }); + } + }); } else { - // Document not ready, wait for load before rendering. - goog.events.listenOnce(window, goog.events.EventType.LOAD, function() { + // Wrap it in a onload callback to wait for the DOM element is rendered. + // If document already loaded, render immediately. + if (doc.readyState == 'complete') { self.initElement_(element); - }); + } else { + // Document not ready, wait for load before rendering. + goog.events.listenOnce(window, goog.events.EventType.LOAD, function() { + self.initElement_(element); + }); + } } }; @@ -418,6 +513,77 @@ firebaseui.auth.AuthUI.prototype.initElement_ = function(element) { }; +/** + * Initializes state needed for processing anonymous user upgrade if enabled, + * before running the specified callback function and returning its result. + * @param {function(?firebase.User):T} cb The callback to trigger when ready. + * @return {T|goog.Promise} The callback result. + * @template T + * @private + */ +firebaseui.auth.AuthUI.prototype.initializeForAutoUpgrade_ = function(cb) { + // This routine could be called anytime, different Auth logic may need to be + // applied depending on the autoUpgradeAnonymousUsers flag. This keeps the + // anonymous user check only when needed minimizing the foot print size on the + // non-anonymous upgrade flow. + var self = this; + // If initial state already determined, run callback with the current + // upgradeable user and return its result. + if (this.initialStateReady_) { + return cb(this.getUpgradableUser_()); + } + // On reset, clear initial state as Auth state listener will be unsubscribed. + this.registerPending(function() { + // This ensures onAuthStateChanged re-subscribed to after reset. + self.initialStateReady_ = false; + }); + // onAuthStateChanged can be slow. Use it only when needed. + if (this.getConfig().autoUpgradeAnonymousUsers()) { + var p = new goog.Promise(function(resolve, reject) { + self.registerPending(self.auth_.onAuthStateChanged(function(user) { + // Update currentUser reference whenever there is a state change. + self.currentUser_ = user; + // Resolve promise only initially with initial upgradeable user. + if (!self.initialStateReady_) { + self.initialStateReady_ = true; + resolve(cb(self.getUpgradableUser_())); + } + })); + }); + // Register pending promise. + this.registerPending(p); + return p; + } else { + // Auto anonymous user upgrade disabled. + // By keeping this synchronous, no additional changes are needed for the + // no upgrade flow. + this.initialStateReady_ = true; + return cb(null); + } +}; + + +/** + * @return {?firebase.User} The current anonymous user if eligible for upgrade, + * null otherwise. + * @private + */ +firebaseui.auth.AuthUI.prototype.getUpgradableUser_ = function() { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + // This is typically run after initial onAuthStateChanged listener is + // triggered. + // Auto upgrade must be enabled and the current external user must be + // anonymous. + if (this.getConfig().autoUpgradeAnonymousUsers() && + this.currentUser_ && + this.currentUser_['isAnonymous']) { + return this.currentUser_; + } + return null; +}; + + /** * Registers a pending promise or reset function. * @param {?goog.Promise|?firebase.Promise|?function()} p The pending promise. @@ -479,14 +645,22 @@ firebaseui.auth.AuthUI.prototype.getAuthUiGetter = function() { firebaseui.auth.AuthUI.prototype.reset = function() { // Check if instance is already destroyed. this.checkIfDestroyed_(); + var self = this; // Remove the "lang" attribute that we set in start(). if (this.widgetElement_) { this.widgetElement_.removeAttribute('lang'); } + // Unregister previous listener. + if (this.widgetEventDispatcher_) { + this.widgetEventDispatcher_.unregister(); + } // Change back the languageCode of external Auth instance. if (typeof this.auth_.languageCode !== 'undefined') { this.auth_.languageCode = this.originalAuthLanguageCode_; } + // Removes pending status of previous redirect operations including redirect + // back from accountchooser.com and federated sign in. + firebaseui.auth.storage.removePendingRedirectStatus(this.getAppId()); // Cancel One-Tap last operation. this.cancelOneTapSignIn(); @@ -526,6 +700,15 @@ firebaseui.auth.AuthUI.prototype.reset = function() { } // Reset current page id. this.currentPageId_ = null; + // Signs Out internal Auth instance to avoid dangling Auth state. + if (this.tempAuth_) { + this.pendingInternalAuthSignOut_ = this.clearTempAuthState() + .then(function() { + self.pendingInternalAuthSignOut_ = null; + }, function(error) { + self.pendingInternalAuthSignOut_ = null; + }); + } }; @@ -538,7 +721,7 @@ firebaseui.auth.AuthUI.prototype.reset = function() { */ firebaseui.auth.AuthUI.prototype.initPageChangeListener_ = function(element) { var self = this; - /** @private {?string} Current page id. */ + /** @private {?string} Current page ID. */ this.currentPageId_ = null; // Initialize the event dispatcher on the widget element. this.widgetEventDispatcher_ = new firebaseui.auth.EventDispatcher(element); @@ -549,8 +732,8 @@ firebaseui.auth.AuthUI.prototype.initPageChangeListener_ = function(element) { this.widgetEventDispatcher_, 'pageEnter', function(event) { - // Get new page id. - var newPageId = event && event.pageId; + // Get new page ID. + var newPageId = event && event['pageId']; // If page change detected. if (self.currentPageId_ != newPageId) { // Get UI changed callback. @@ -680,3 +863,400 @@ firebaseui.auth.AuthUI.prototype.showOneTapSignIn = function(handler) { // Ignore the error when the One-Tap API is not supported. } }; + + +/** + * Sign in using an email and password. + * @param {string} email The email to sign in with. + * @param {string} password The password to sign in with. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.startSignInWithEmailAndPassword = + function(email, password) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + // Start sign in with existing email and password. This always runs on the + // internal Auth instance as an existing email/password account linking will + // always fail when upgrading an anonymous user. For the non-anonymous upgrade + // flow, the same credential will be used to complete sign in on the + // external Auth instance. + return /** @type {!firebase.Promise} */ ( + this.getAuth().signInWithEmailAndPassword(email, password) + .then(function(result) { + var cb = function(user) { + if (user) { + // signOut the user. + return self.clearTempAuthState().then(function() { + // Eligible anonymous user upgrade. + // On anonymous user upgrade with existing email/password, merge + // conflict will always occur, trigger signInFailure as soon as + // password is confirmed. + return self.onUpgradeError( + {'code': 'auth/email-already-in-use'}, + firebase.auth.EmailAuthProvider.credential(email, password)); + }); + } else { + return result; + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return self.initializeForAutoUpgrade_(cb); + })); +}; + + +/** + * Create a new email and password account. + * @param {string} email The email to sign up with. + * @param {string} password The password to sign up with. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.startCreateUserWithEmailAndPassword = + function(email, password) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + var cb = function(user) { + if (user) { + var credential = + firebase.auth.EmailAuthProvider.credential(email, password); + // For anonymous user upgrade, call link with credential on the external + // Auth user. Otherwise merge conflict will always occur when linking the + // credential to the external anonymous user after creation. + return /** @type {!firebase.Promise} */ ( + user.linkAndRetrieveDataWithCredential(credential) + .then(function(result) { + return result['user']; + })); + } else { + // Start create user with email and password. This runs on the internal + // Auth instance as finish sign in will sign in with that same credential + // to developer Auth instance. + return /** @type {!firebase.Promise} */ ( + self.getAuth().createUserWithEmailAndPassword(email, password)); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return this.initializeForAutoUpgrade_(cb); +}; + + +/** + * Logs into Firebase with the given 3rd party credentials. + * @param {!firebase.auth.AuthCredential} credential The Auth credential. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.startSignInWithCredential = + function(credential) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + var cb = function(user) { + if (user) { + // For anonymous user upgrade, call link with credential on the external + // Auth user. Otherwise merge conflict will always occur when linking the + // credential to the external anonymous user after creation. + return /** @type {!firebase.Promise} */ ( + user.linkAndRetrieveDataWithCredential(credential) + .then(function(result) { + return result; + }, function(error) { + // Fail directly when email already in use error thrown. This will + // trigger account linking flow. + // When email already exists for a new federated credential, user + // must sign in to existing account and then link this credential to + // that account. + if (error && + error['code'] == 'auth/email-already-in-use' && + error['email'] && error['credential']) { + throw error; + } + // Pass the same credential back in the error. This assumes the + // credential used is not a one-time credential. + return self.onUpgradeError(error, credential); + })); + } else { + // Starts sign in with a Firebase Auth credential, typically an OAuth + // credential. This runs on the internal Auth instance as finish sign in + // will sign in with that same credential to developer Auth instance. + return self.getAuth().signInAndRetrieveDataWithCredential(credential); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return this.initializeForAutoUpgrade_(cb); +}; + + +/** + * Signs in to Auth provider via popup. + * @param {!firebase.auth.AuthProvider} provider The Auth provider to sign in + * with. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.startSignInWithPopup = function(provider) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + var cb = function(user) { + if (user && + // If email already exists, user must first be signed in to the + // existing account on the internal Auth instance before the credential + // is linked and merge conflict is triggered. + !firebaseui.auth.storage.getPendingEmailCredential(self.getAppId())) { + // For anonymous user upgrade, call link with popup on the external Auth + // user. Otherwise merge conflict will always occur when linking the + // credential to the external anonymous user after creation. + return /** @type {!firebase.Promise} */ ( + user.linkWithPopup(provider) + .then(function(result) { + return result; + }, function(error) { + // Fail directly when email already in use error thrown. This will + // trigger account linking flow. + // When email already exists for a new federated credential, user + // must sign to existing account and then link this credential to + // that account. + if (error && + error['code'] == 'auth/email-already-in-use' && + error['email'] && error['credential']) { + throw error; + } + // For all other errors, run onUpgrade check. + return self.onUpgradeError(error); + })); + } else { + // Starts sign in with popup. This runs on the internal Auth instance as + // finish sign in will sign in with the final credential to developer Auth + // instance. + return self.getAuth().signInWithPopup(provider); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return this.initializeForAutoUpgrade_(cb); +}; + + +/** + * Signs in to Auth provider via redirect. + * @param {!firebase.auth.AuthProvider} provider The Auth provider to sign in + * with. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.startSignInWithRedirect = function(provider) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + // Save cached redirect result. + var cachedRedirectResult = this.getRedirectResult_; + // Each time a redirect operation is triggered, clear cached redirect result. + // This is important for Cordova apps where no page redirect may occur and + // getRedirectResult will update with the result after the redirect operation + // resolves. + this.getRedirectResult_ = null; + var cb = function(user) { + if (user && + // If email already exists, user must first be signed in to the + // existing account on the internal Auth instance before the credential + // is linked and merge conflict is triggered. + !firebaseui.auth.storage.getPendingEmailCredential(self.getAppId())) { + // For anonymous user upgrade, call link with redirect on the external + // user. Otherwise merge conflict will always occur when linking the + // credential to the external anonymous user after creation. + return user.linkWithRedirect(provider); + } else { + // Starts sign in with redirect. This runs on the internal Auth instance + // as finish sign in will sign in with the final credential to developer + // Auth instance. + return self.getAuth().signInWithRedirect(provider); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return this.initializeForAutoUpgrade_(cb).then( + function() { + // Redirect succeeded. + // Browser case: page navigation occurs. + // Cordova case: either activity destroyed or getRedirectResult is + // updated since it was nullified. + }, + function(error) { + // Error occurred, restore cached redirect result. + // It is useful to keep the cached result in case the UI was previously + // reset. This ensures that {'user': null, 'credential': null} is + // maintained and not some previous result from a redirect operation. + self.getRedirectResult_ = cachedRedirectResult; + throw error; + }); +}; + + +/** + * Signs in with a phone number using the app verifier instance and returns a + * promise that resolves with the confirmation result which on confirmation + * will resolve with the UserCredential object. + * @param {string} phoneNumber The phone number to authenticate with. + * @param {!firebase.auth.ApplicationVerifier} appVerifier The application + * verifier. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.startSignInWithPhoneNumber = + function(phoneNumber, appVerifier) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + var cb = function(user) { + if (user) { + // For anonymous user upgrade, call link with phone number on the external + // user. + return user.linkWithPhoneNumber(phoneNumber, appVerifier) + .then(function(confirmationResult) { + return new firebaseui.auth.PhoneAuthResult(confirmationResult, + function(error) { + if (error.code == 'auth/credential-already-in-use') { + return self.onUpgradeError(error); + } + throw error; + }); + }); + } else { + // Starts sign in with phone number. This runs on the external Auth + // instance. + return self.getExternalAuth().signInWithPhoneNumber( + phoneNumber, appVerifier).then(function(confirmationResult) { + return new firebaseui.auth.PhoneAuthResult(confirmationResult); + }); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return this.initializeForAutoUpgrade_(cb); +}; + + +/** + * Finishes FirebaseUI login with the given 3rd party credentials. + * @param {!firebase.auth.AuthCredential} credential The auth credential. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.finishSignInWithCredential = + function(credential) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + var cb = function(user) { + // Anonymous user upgrade successful, resolve immediately with the user. + // No need to sign in again with the same credential on the external Auth + // instance. + if (self.currentUser_ && + !self.currentUser_['isAnonymous'] && + self.getConfig().autoUpgradeAnonymousUsers()) { + return (firebase.Promise || goog.Promise).resolve(self.currentUser_); + } else if (user) { + // TODO: optimize and fail directly as this will fail in most cases + // with error credential already in use. + // There are cases where this is required. For example, when email + // mismatch occurs and the user continues with the new account. + return /** @type {!firebase.Promise} */ ( + user.linkWithCredential(credential) + .then(function() { + return user; + }, function(error) { + // Rethrow email already in use error so it can trigger the account + // linking flow. + if (error && + error['code'] == 'auth/email-already-in-use' && + error['email'] && error['credential']) { + throw error; + } + // For all other errors, run onUpgrade check. + return self.onUpgradeError(error, credential); + })); + } else { + // Finishes sign in with the supplied credential on the developer provided + // Auth instance. On completion, this will redirect to signInSuccessUrl or + // trigger the signInSuccess callback. + return self.getExternalAuth().signInWithCredential(credential); + } + }; + // Initialize current user if auto upgrade is enabled beforing running + // callback and returning result. + return this.initializeForAutoUpgrade_(cb); +}; + + +/** + * Sign in using an email and password on existing account for linking to + * recover from an existing email error. + * @param {string} email The email to sign in with. + * @param {string} password The password to sign in with. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.signInWithExistingEmailAndPasswordForLinking = + function(email, password) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + // Starts sign in with email/password on the internal Auth instance for + // linking purposes. This is needed to avoid triggering the onAuthStateChanged + // callbacks interrupting the linking flow. + return this.getAuth().signInWithEmailAndPassword(email, password); +}; + + +/** + * Clears all temporary Auth states. + * @return {!firebase.Promise} + */ +firebaseui.auth.AuthUI.prototype.clearTempAuthState = function() { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + return this.getAuth().signOut(); +}; + + +/** + * Handles Firebase Auth upgrade errors. + * @param {*} error The error thrown from a Firebase operation. + * @param {?firebase.auth.AuthCredential=} opt_credential The optional + * credential to provide to developer on merge conflicts. + * @return {!goog.Promise} A promise that resolves on completion. + */ +firebaseui.auth.AuthUI.prototype.onUpgradeError = + function(error, opt_credential) { + // Check if instance is already destroyed. + this.checkIfDestroyed_(); + var self = this; + if (error && error['code'] && + // Password sign in. + (error['code'] == 'auth/email-already-in-use' || + // Federated or phone number sign in. + error['code'] == 'auth/credential-already-in-use')) { + // Get signInFailure callback. + var signInFailureCallback = this.getConfig().getSignInFailureCallback(); + // Wrap in promise in case no promise returned by callback. + return goog.Promise.resolve().then(function() { + // Pass merge conflict error to callback and wait for resolution. + return signInFailureCallback(new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + opt_credential || error['credential'])); + }).then(function() { + // Clear UI after the developer finishes handling the error. + // This is helpful in case the developer forgets to reset UI + // after handling signInFailure. + if (self.currentComponent_) { + self.currentComponent_.dispose(); + self.currentComponent_ = null; + } + // Rethrow the original error. + throw error; + }); + } else { + // Rethrow all other non-merge conflict related errors. + return goog.Promise.reject(error); + } +}; diff --git a/javascript/widgets/authui_test.js b/javascript/widgets/authui_test.js index 0d30395d..82d9f67e 100644 --- a/javascript/widgets/authui_test.js +++ b/javascript/widgets/authui_test.js @@ -19,6 +19,7 @@ goog.provide('firebaseui.auth.AuthUITest'); goog.require('firebaseui.auth.AuthUI'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.CredentialHelper'); goog.require('firebaseui.auth.GoogleYolo'); goog.require('firebaseui.auth.PendingEmailCredential'); @@ -110,6 +111,51 @@ var googleYoloIdTokenCredential = { }; var mockControl; var ignoreArgument; +var expectedUser = { + uid: '1234567890', + email: 'user@example.com', + displayName: 'Federated User', + providerData: [{ + 'uid': 'FED_ID', + 'email': 'user@example.com', + 'displayName': 'Federated User', + 'providerId': 'google.com' + }, { + 'uid': 'user@example.com', + 'email': 'user@example.com', + 'providerId': 'password' + }] +}; +var expectedCredential = + {'accessToken': 'googleAccessToken', 'providerId': 'google.com'}; +var expectedAdditionalUserInfo = { + 'profile': { + 'kind': 'plus#person', + 'displayName': 'John Doe', + 'name': { + 'givenName': 'John', + 'familyName': 'Doe' + } + }, + 'providerId': 'google.com', + 'isNewUser': false +}; +var expectedUserCredential = { + 'user': expectedUser, + 'credential': expectedCredential, + 'operationType': 'signIn', + 'additionalUserInfo': expectedAdditionalUserInfo +}; +var pendingCredential = + {'accessToken': 'fbAccessToken', 'providerId': 'facebook.com'}; +var pendingEmailCredential = new firebaseui.auth.PendingEmailCredential( + expectedUser.email, pendingCredential); +var expectedProvider = null; +var anonymousUpgradeConfig = null; +var anonymousUser = { + uid: '1234567890', + isAnonymous: true +}; /** @@ -199,6 +245,62 @@ function setUp() { testUtil = new firebaseui.auth.testing.FakeUtil().install(); ignoreArgument = goog.testing.mockmatchers.ignoreArgument; mockControl = new goog.testing.MockControl(); + + // Build mock auth providers. + for (var key in firebaseui.auth.idp.AuthProviders) { + firebase['auth'][firebaseui.auth.idp.AuthProviders[key]] = function() { + this.scopes = []; + this.customParameters = {}; + }; + firebase['auth'][firebaseui.auth.idp.AuthProviders[key]].PROVIDER_ID = key; + if (key != 'twitter.com' && key != 'password') { + firebase['auth'][firebaseui.auth.idp.AuthProviders[key]] + .prototype.addScope = function(scope) { + this.scopes.push(scope); + return this; + }; + } + if (key != 'password') { + // Record setCustomParameters for all OAuth providers. + firebase['auth'][firebaseui.auth.idp.AuthProviders[key]] + .prototype.setCustomParameters = function(customParameters) { + this.customParameters = customParameters; + return this; + }; + } + if (key == 'password') { + // Mock credential initializer for Email/password credentials. + firebase['auth'][firebaseui.auth.idp.AuthProviders[key]]['credential'] = + function(email, password) { + return { + 'email': email, + 'password': password, + 'providerId': 'password' + }; + }; + } else if (key == 'facebook.com') { + // Mock credential initializer for Facebook credentials. + firebase['auth'][firebaseui.auth.idp.AuthProviders[key]]['credential'] = + function(accessToken) { + return { + 'accessToken': accessToken, + 'providerId': 'facebook.com' + }; + }; + } + } + expectedProvider = new firebase.auth.GoogleAuthProvider(); + anonymousUpgradeConfig = { + 'autoUpgradeAnonymousUsers': true, + 'callbacks': { + 'signInSuccess': goog.testing.recordFunction(function() { + return false; + }), + 'signInFailure': goog.testing.recordFunction(function() { + return goog.Promise.resolve(); + }) + } + }; } @@ -210,18 +312,22 @@ function tearDown() { // Delete all application instances. // Uninstall internal and external Auth instances. if (app1) { + app1.getAuth().assertSignOut([]); app1.getAuth().uninstall(); app1.getExternalAuth().uninstall(); app1.reset(); } app1 = null; if (app2) { + app2.getAuth().assertSignOut([]); app2.getAuth().uninstall(); app2.getExternalAuth().uninstall(); app2.reset(); } app2 = null; if (app3) { + + app3.getAuth().assertSignOut([]); app3.getAuth().uninstall(); app3.getExternalAuth().uninstall(); app3.reset(); @@ -250,6 +356,7 @@ function tearDown() { testAuth3.uninstall(); } if (app) { + app.getAuth().assertSignOut([]); app.getAuth().uninstall(); app.getExternalAuth().uninstall(); app.reset(); @@ -420,8 +527,13 @@ function testStart() { firebaseui.auth.storage.setPendingEmailCredential( pendingEmailCredential, app1.getAppId()); assertNull(app1.getCurrentComponent()); + firebaseui.auth.storage.setPendingRedirectStatus(app1.getAppId()); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app1.getAppId())); // Start widget for app1, override configuration for that. app1.start(container1, config4); + app1.getExternalAuth().runAuthChangeHandler(); + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app1.getAppId())); // Confirm getCurrentComponent returns the expected callback component. assertTrue( app1.getCurrentComponent() instanceof firebaseui.auth.ui.page.Callback); @@ -450,8 +562,14 @@ function testStart() { // Callback page rendered in first app container1. assertHasCssClass(container1, 'firebaseui-id-page-callback'); + firebaseui.auth.storage.setPendingRedirectStatus(app2.getAppId()); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app2.getAppId())); // Try to render another widget. This should reset first app widget. app2.start(container2, config2); + app2.getExternalAuth().runAuthChangeHandler(); + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app2.getAppId())); + app1.getAuth().assertSignOut([]); // App1 pending creds cleared. assertFalse( firebaseui.auth.storage.hasPendingEmailCredential(app1.getAppId())); @@ -492,6 +610,7 @@ function testStart() { 'user': null, 'credential': null }); + app2.getAuth().process().then(function() { // Provider sign-in rendered at this stage. assertHasCssClass(container2, 'firebaseui-id-page-provider-sign-in'); @@ -504,6 +623,8 @@ function testStart() { // getRedirectResult anymore since if there is a pending redirect, it will // process it and not display the widget. app2.start(container2, config2); + app2.getExternalAuth().runAuthChangeHandler(); + app2.getAuth().assertSignOut([]); // No additional Automatic reset warning is logged. /** @suppress {missingRequire} */ assertEquals(1, firebaseui.auth.log.warning.getCallCount()); @@ -511,6 +632,7 @@ function testStart() { app2.getRedirectResult().then(function(result) { assertHasCssClass(container2, 'firebaseui-id-page-provider-sign-in'); // After reset, currentComponent is set to null. + app2.getAuth().assertSignOut([]); app2.reset(); // Confirm current component is null after reset. assertNull(app2.getCurrentComponent()); @@ -526,9 +648,11 @@ function testSetLang() { // Replace goog.LOCALE and then install instance. createAndInstallTestInstances(); app1.start(container1, config1); + app1.getExternalAuth().runAuthChangeHandler(); assertEquals('de', container1.getAttribute('lang')); assertEquals('de', app1.getAuth().languageCode); assertEquals('de', app1.getExternalAuth().languageCode); + app1.getAuth().assertSignOut([]); app1.reset(); assertFalse(container1.hasAttribute('lang')); } @@ -539,9 +663,11 @@ function testSetLang_codeWithdash() { // Replace goog.LOCALE and then install instance. createAndInstallTestInstances(); app1.start(container1, config1); + app1.getExternalAuth().runAuthChangeHandler(); assertEquals('zh-CN', container1.getAttribute('lang')); assertEquals('zh-CN', app1.getAuth().languageCode); assertEquals('zh-CN', app1.getExternalAuth().languageCode); + app1.getAuth().assertSignOut([]); app1.reset(); assertFalse(container1.hasAttribute('lang')); } @@ -552,10 +678,12 @@ function testSetLang_codeWithUnderscore() { // Replace goog.LOCALE and then install instance. createAndInstallTestInstances(); app1.start(container1, config1); + app1.getExternalAuth().runAuthChangeHandler(); // The lang should have a dash instead of an underscore. assertEquals('zh-CN', container1.getAttribute('lang')); assertEquals('zh-CN', app1.getAuth().languageCode); assertEquals('zh-CN', app1.getExternalAuth().languageCode); + app1.getAuth().assertSignOut([]); app1.reset(); assertFalse(container1.hasAttribute('lang')); } @@ -571,7 +699,9 @@ function testStart_overrideLanguageCode() { testAuth.languageCode = 'de'; // Override language code of auth to zh-CN. app.start(container1, config1); + app.getExternalAuth().runAuthChangeHandler(); assertEquals('zh-CN', app.getExternalAuth().languageCode); + app.getAuth().assertSignOut([]); app.reset(); // Confirm language code of auth changed back to de. assertEquals('de', testAuth.languageCode); @@ -584,6 +714,7 @@ function testStart_elementNotFound() { createAndInstallTestInstances(); try { app1.start('#notFound', config4, 'POST_BODY'); + app1.getExternalAuth().runAuthChangeHandler(); firebaseui.auth.widget.dispatcher.dispatchOperation(app, '#notFound'); fails('Should have thrown an error!'); } catch (e) { @@ -644,6 +775,7 @@ function testUiChangedCallback() { assertEquals(uiChangedCallback, app.getConfig().getUiChangedCallback()); // Start widget mode. app.start(container1, config); + app.getExternalAuth().runAuthChangeHandler(); // UI changed from null to sign in on widget rendering. assertTrue(uiChangedCallbackCalled); assertEquals(null, fromPage); @@ -706,8 +838,12 @@ function testAuthUi_reset() { // No calls should be made to cancelOneTapSignIn at this point. assertEquals( 0, firebaseui.auth.AuthUI.prototype.cancelOneTapSignIn.getCallCount()); + firebaseui.auth.storage.setPendingRedirectStatus(app.getAppId()); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); // Trigger reset. app.reset(); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + app.getAuth().assertSignOut([]); // Reset functions should be called and pending promises cancelled. assertEquals(1, reset1.getCallCount()); assertEquals(1, reset2.getCallCount()); @@ -879,7 +1015,17 @@ function testAuthUi_delete() { 'cancelOneTapSignIn', 'showOneTapSignIn', 'disableAutoSignIn', - 'isAutoSignInDisabled' + 'isAutoSignInDisabled', + 'startSignInWithEmailAndPassword', + 'startCreateUserWithEmailAndPassword', + 'startSignInWithCredential', + 'startSignInWithPopup', + 'startSignInWithRedirect', + 'startSignInWithPhoneNumber', + 'finishSignInWithCredential', + 'signInWithExistingEmailAndPasswordForLinking', + 'clearTempAuthState', + 'onUpgradeError' ]; // Call all public methods and confirm expected error. for (var i = 0; i < methods.length; i++) { @@ -1183,3 +1329,2472 @@ function testAuthUi_oneTapSignIn_autoSignInEnabled() { app.showOneTapSignIn(handler); app.cancelOneTapSignIn(); } + + +function testStartSignInWithEmailAndPassword_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithEmailAndPassword_error() { + var expectedError = { + 'code': 'auth/wrong-password', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithEmailAndPassword('user@example.com', 'password') + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithEmailAndPassword_upgradeAnon_isAnonymous_success() { + // Record calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + var expectedError = {'code': 'auth/email-already-in-use'}; + var cred = firebase.auth.EmailAuthProvider.credential( + 'user@example.com', 'password'); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithEmailAndPassword('user@example.com', 'password') + .then(fail, function(error) { + // onUpgradeError should be called. + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertObjectEquals( + expectedError, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + // Expected credential passed as second parameter to onUpgradeError. + assertObjectEquals( + cred, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertObjectEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process().then(function() { + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signOut the user on internal instance. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testStartSignInWithEmailAndPassword_upgradeAnon_isAnon_error() { + // Record calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + var expectedError = { + 'code': 'auth/wrong-password', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + app.startSignInWithEmailAndPassword('user@example.com', 'password') + .then(fail, function(error) { + // onUpgradeError should not be called even though email-already-in-use + // thrown. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate wrong password error on sign-in. + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithEmailAndPassword_upgradeAnon_nonAnon_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate non-anonymous user logged in on external instance. + testAuth.setUser(expectedUser); + app.startSignInWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process().then(function() { + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getAuth().process(); + app.getExternalAuth().process(); + }); +} + + +function testStartSignInWithEmailAndPassword_upgradeAnonymous_noUser_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate no user logged in on external instance. + testAuth.setUser(null); + app.startSignInWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process().then(function() { + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getAuth().process(); + app.getExternalAuth().process(); + }); +} + + +function testStartCreateUserWithEmailAndPassword_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startCreateUserWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + app.getAuth().assertCreateUserWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartCreateUserWithEmailAndPassword_error() { + var expectedError = { + 'code': 'auth/weak-password', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startCreateUserWithEmailAndPassword('user@example.com', 'password') + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertCreateUserWithEmailAndPassword( + ['user@example.com', 'password'], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartCreateUserWithEmailAndPassword__upgradeAnon_isAnon_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startCreateUserWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + var emailCredential = { + 'email': 'user@example.com', + 'password': 'password', + 'providerId': 'password' + }; + app.getExternalAuth().currentUser.assertLinkAndRetrieveDataWithCredential( + [emailCredential], + function() { + app.getExternalAuth().setUser(expectedUser); + return { + 'user': app.getExternalAuth().currentUser, + 'credential': null + }; + }); + app.getExternalAuth().process(); +} + + +function testStartCreateUserWithEmailAndPassword__upgradeAnon_noUser_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate no user logged in on external instance. + testAuth.setUser(null); + app.startCreateUserWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // createUserWithEmailAndPassword called on internal Auth instance as no user + // available. + app.getAuth().assertCreateUserWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartCreateUserWithEmailAndPassword_upgradeAnon_emailInUseError() { + var emailCredential = { + 'email': 'user@example.com', + 'password': 'password', + 'providerId': 'password' + }; + // Expected email already in use error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'message': 'MESSAGE' + }; + // Record calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startCreateUserWithEmailAndPassword('user@example.com', 'password') + .then(fail, function(error) { + // onUpgradeError should not be called. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential called on external anonymous user and + // throws expected error. + app.getExternalAuth().currentUser.assertLinkAndRetrieveDataWithCredential( + [emailCredential], + null, + expectedError); + app.getExternalAuth().process(); +} + + +function testStartCreateUserWithEmailAndPassword_upgradeAnon_nonAnon_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate non-anonymous user logged in on external instance. + testAuth.setUser(expectedUser); + app.startCreateUserWithEmailAndPassword('user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getAuth().assertCreateUserWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInAndRetrieveDataWithCredential( + [expectedCredential], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_error() { + var expectedError = { + 'code': 'auth/network-request-failed', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_upgradeAnonymous_isAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential called on external anonymous user. + app.getExternalAuth().currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + function() { + app.getExternalAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_upgradeAnonymous_nonAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Simulate non-anonymous user logged in on external instance. + testAuth.setUser(expectedUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInAndRetrieveDataWithCredential called on internal Auth instance as no + // anonymous user available. + app.getAuth().assertSignInAndRetrieveDataWithCredential( + [expectedCredential], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_upgradeAnonymous_noUser_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Simulate no user logged in on external instance. + testAuth.setUser(null); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInAndRetrieveDataWithCredential called on internal Auth instance as no + // user available. + app.getAuth().assertSignInAndRetrieveDataWithCredential( + [expectedCredential], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_upgradeAnonymous_emailInUseError() { + // Expected email already in use error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + throw error; + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(fail, function(error) { + // onUpgradeError should not be called. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential called on external anonymous user and + // throws expected error. + app.getExternalAuth().currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithCredential_upgradeAnonymous_credentialInUseError() { + // Expected credential already in use error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record call to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + throw error; + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithCredential(expectedCredential) + .then(fail, function(error) { + // onUpgradeError should be called. + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals( + error, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + // Expected credential passed as second parameter to onUpgradeError. + assertEquals( + expectedCredential, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential called on external anonymous user and + // expected error thrown. + app.getExternalAuth().currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithPopup( + [expectedProvider], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_error() { + var expectedError = { + 'code': 'auth/user-cancelled', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithPopup( + [expectedProvider], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_upgradeAnonymous_isAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithPopup called on external anonymous user. + app.getExternalAuth().currentUser.assertLinkWithPopup( + [expectedProvider], + function() { + app.getExternalAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_upgradeAnon_isAnon_pendingCred_success() { + // Simulate eligible anonymous user upgrade with pending email credential + // after email already exists error. + // Typical flow: + // 1. eligible anonymous user + // 2. linkWithPopup -> email exists + // 3. Save pending credential + // 4. sign in with popup to existing account on internal instance. + // 5. Get result from internal instance and clear credential after + // linking. + // 6. sign out and pass credential for merge conflict handling. + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + // Save pending email credential. + firebaseui.auth.storage.setPendingEmailCredential( + pendingEmailCredential, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + // Set anonymous user on external instance. + testAuth.setUser(anonymousUser); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // As pending credential already exists, signInWithPopup triggered on internal + // Auth instance. + app.getAuth().assertSignInWithPopup( + [expectedProvider], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_upgradeAnonymous_nonAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Simulate non-anonymous user logged in on external instance. + testAuth.setUser(expectedUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInWithPopup called on internal Auth instance as no anonymous user + // available. + app.getAuth().assertSignInWithPopup( + [expectedProvider], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_upgradeAnonymous_noUser_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(function(userCredential) { + assertEquals(expectedUserCredential, userCredential); + asyncTestCase.signal(); + }); + // Simulate no user logged in on external instance. + testAuth.setUser(null); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInWithPopup called on internal Auth instance as no user available. + app.getAuth().assertSignInWithPopup( + [expectedProvider], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_upgradeAnonymous_emailAlreadyInUseError() { + // Expected email already in use error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(fail, function(error) { + // onUpgradeError should not be called. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithPopup called on external anonymous user and throws expected error. + app.getExternalAuth().currentUser.assertLinkWithPopup( + [expectedProvider], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPopup_upgradeAnonymous_credentialInUseError() { + // Expected credential already in use error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record call to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPopup(expectedProvider) + .then(fail, function(error) { + // onUpgradeError should be called. + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals( + error, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + assertUndefined( + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithPopup called on external anonymous user and expected error thrown. + app.getExternalAuth().currentUser.assertLinkWithPopup( + [expectedProvider], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(function() { + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithRedirect( + [expectedProvider]); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_error() { + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithRedirect( + [expectedProvider], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_upgradeAnonymous_isAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(function() { + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithRedirect called on external anonymous user. + app.getExternalAuth().currentUser.assertLinkWithRedirect( + [expectedProvider]); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_upgradeAnon_isAnon_pendingCred_success() { + // Simulate eligible anonymous user upgrade with pending email credential + // after email already exists error. + // Typical flow: + // 1. eligible anonymous user + // 2. linkWithRedirect -> email exists + // 3. Save pending credential + // 4. sign in with redirect to existing account on internal instance. + // 5. Get result from internal instance and clear credential after + // linking. + // 6. sign out and pass credential for merge conflict handling. + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + // Save pending email credential. + firebaseui.auth.storage.setPendingEmailCredential( + pendingEmailCredential, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + // Set anonymous user on external instance. + testAuth.setUser(anonymousUser); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(function() { + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // As a pending credential exists, signInWithRedirect is triggered on the + // internal Auth instance to sign in to the existing user before linking. + app.getAuth().assertSignInWithRedirect( + [expectedProvider]); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_upgradeAnonymous_nonAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(function() { + asyncTestCase.signal(); + }); + // Simulate non-anonymous user logged in on external instance. + testAuth.setUser(expectedUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInWithRedirect called on internal Auth instance as no anonymous user + // is available. + app.getAuth().assertSignInWithRedirect( + [expectedProvider]); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_upgradeAnonymous_noUser_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(function() { + asyncTestCase.signal(); + }); + // Simulate no user logged in on external instance. + testAuth.setUser(null); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInWithRedirect called on internal Auth instance as no user is + // available. + app.getAuth().assertSignInWithRedirect( + [expectedProvider]); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithRedirect_upgradeAnonymous_error() { + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + // Record all calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithRedirect(expectedProvider) + .then(fail, function(error) { + // onUpgradeError not called. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithRedirect called on external anonymous user and expected error + // thrown. + app.getExternalAuth().currentUser.assertLinkWithRedirect( + [expectedProvider], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPhoneNumber_success() { + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + var expectedConfirmationResult = { + 'verificationId': '1234567890', + 'confirm': function(code) {} + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(function(phoneAuthResult) { + assertEquals(expectedConfirmationResult, + phoneAuthResult.getConfirmationResult()); + asyncTestCase.signal(); + }); + app.getExternalAuth().assertSignInWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + expectedConfirmationResult); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testStartSignInWithPhoneNumber_error() { + var expectedError = { + 'code': 'auth/invalid-phone-number', + 'message': 'MESSAGE' + }; + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getExternalAuth().assertSignInWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + null, + expectedError); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testStartSignInWithPhoneNumber_upgradeAnon_isAnonymous_success() { + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + var expectedConfirmationResult = { + 'verificationId': '1234567890', + 'confirm': function(code) {} + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(function(phoneAuthResult) { + assertEquals(expectedConfirmationResult, + phoneAuthResult.getConfirmationResult()); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().currentUser.assertLinkWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + expectedConfirmationResult); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPhoneNumber_upgradeAnon_isAnon_credInUseError() { + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + var cred = { + 'providerId': 'phone', + 'verificationId': '123456abc', + 'verificationCode': '123456' + }; + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'phoneNumber': '+11234567890', + 'credential': cred + }; + var expectedConfirmationResult = { + 'verificationId': '1234567890', + 'confirm': function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + } + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(function(phoneAuthResult) { + assertEquals(expectedConfirmationResult, + phoneAuthResult.getConfirmationResult()); + return phoneAuthResult.confirm('123456').thenCatch(function(error) { + assertEquals(1, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals( + error, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + assertUndefined( + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().currentUser.assertLinkWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + expectedConfirmationResult); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPhoneNumber_upgradeAnon_isAnon_invalidCodeError() { + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + var expectedError = { + 'code': 'auth/invalid-verification-code', + 'message': 'MESSAGE', + }; + var expectedConfirmationResult = { + 'verificationId': '1234567890', + 'confirm': function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + } + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(function(phoneAuthResult) { + assertEquals(expectedConfirmationResult, + phoneAuthResult.getConfirmationResult()); + return phoneAuthResult.confirm('123456').thenCatch(function(error) { + assertEquals(0, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().currentUser.assertLinkWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + expectedConfirmationResult); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPhoneNumber_upgradeAnon_isAnon_invalidNumError() { + var expectedError = { + 'code': 'auth/invalid-phone-number', + 'message': 'MESSAGE' + }; + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + // Record calls to onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(fail, function(error) { + // onUpgradeError should not be called even though invalid phone number + // error thrown. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().currentUser.assertLinkWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + null, + expectedError); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPhoneNumber_upgradeAnon_nonAnon_success() { + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + var expectedConfirmationResult = { + 'verificationId': '1234567890', + 'confirm': function(code) {} + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(function(phoneAuthResult) { + assertEquals(expectedConfirmationResult, + phoneAuthResult.getConfirmationResult()); + asyncTestCase.signal(); + }); + // Simulate non-anonymous user logged in on external instance. + testAuth.setUser(expectedUser); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().assertSignInWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + expectedConfirmationResult); + app.getExternalAuth().process(); +} + + +function testStartSignInWithPhoneNumber_upgradeAnonymous_noUser_success() { + var expectedPhoneNumber = '+11234567890'; + var expectedAppVerifier = { + 'type': 'recaptcha', + 'verify': function() {}, + 'clear': function() {}, + 'render': function() {} + }; + var expectedConfirmationResult = { + 'verificationId': '1234567890', + 'confirm': function(code) {} + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.startSignInWithPhoneNumber(expectedPhoneNumber, expectedAppVerifier) + .then(function(phoneAuthResult) { + assertEquals(expectedConfirmationResult, + phoneAuthResult.getConfirmationResult()); + asyncTestCase.signal(); + }); + // Simulate no user logged in on external instance. + testAuth.setUser(null); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().assertSignInWithPhoneNumber( + [expectedPhoneNumber, expectedAppVerifier], + expectedConfirmationResult); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testFinishSignInWithCredential_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.finishSignInWithCredential(expectedCredential) + .then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + asyncTestCase.signal(); + }); + app.getExternalAuth().assertSignInWithCredential( + [expectedCredential], + function() { + app.getExternalAuth().setUser(expectedUser); + return app.getExternalAuth().currentUser; + }); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testFinishSignInWithCredential_error() { + var expectedError = { + 'code': 'auth/network-request-failed', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.finishSignInWithCredential(expectedCredential) + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getExternalAuth().assertSignInWithCredential( + [expectedCredential], + null, + expectedError); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testFinishSignInWithCredential_upgradeAnonymous_nonAnonymous() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate non-anonymous user already logged in on external instance. + testAuth.setUser(expectedUser); + // No underlying Auth call needed. + app.finishSignInWithCredential(expectedCredential) + .then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + // Call again and confirm no onAuthStateChanged listener set again. + // This should resolve with triggering runAuthChangeHandler. + app.finishSignInWithCredential(expectedCredential).then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + // Reset app. + app.reset(); + // Reset should cause onAuthStateChanged to unsubscribe. + app.finishSignInWithCredential(expectedCredential) + .then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + // Reset app. + app.reset(); + // This should fail due to reset call and onAuthStateChanged + // should unsubscribe. + app.finishSignInWithCredential(expectedCredential) + .then(fail, function(error) { + assertEquals('cancel', error['name']); + asyncTestCase.signal(); + }); + app.reset(); + }); + // Trigger initial onAuthStateChanged listener for new listener to + // resolve after reset. + app.getExternalAuth().runAuthChangeHandler(); + }); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getExternalAuth().process(); + app.getAuth().process().then(function() { + // Assert signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }).then(function() { + // Second signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }).then(function() { + // Third signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testFinishSignInWithCredential_upgradeAnonymous_noUser() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // No user signed in. + app.getExternalAuth().setUser(null); + app.finishSignInWithCredential(expectedCredential) + .then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // signInWithCredential called as no user available. + app.getExternalAuth().assertSignInWithCredential( + [expectedCredential], + function() { + app.getExternalAuth().setUser(expectedUser); + return app.getExternalAuth().currentUser; + }); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testFinishSignInWithCredential_upgradeAnonymous_anonymousUser() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user signed in on external instance. + app.getExternalAuth().setUser(anonymousUser); + app.finishSignInWithCredential(expectedCredential) + .then(function(user) { + assertEquals(app.getExternalAuth().currentUser, user); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithCredential called on external user since an anonymous user is + // available and autoUpgradeAnonymousUsers is set to true. + app.getExternalAuth().currentUser.assertLinkWithCredential( + [expectedCredential], + function() { + app.getExternalAuth().setUser(expectedUser); + return app.getExternalAuth().currentUser; + }); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testFinishSignInWithCredential_upgradeAnon_anonUser_credInUse() { + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user signed in on external instance. + app.getExternalAuth().setUser(anonymousUser); + app.finishSignInWithCredential(expectedCredential) + .then(fail, function(error) { + // onUpgradeError should be called with expected arguments. + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals( + error, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + assertEquals( + expectedCredential, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linknWithCredential called since an eligible anonymous user is available + // on external Auth instance. + app.getExternalAuth().currentUser.assertLinkWithCredential( + [expectedCredential], + null, + expectedError); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testFinishSignInWithCredential_upgradeAnon_anonUser_emailInUse() { + var expectedError = { + 'code': 'auth/email-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user signed in on external instance. + app.getExternalAuth().setUser(anonymousUser); + app.finishSignInWithCredential(expectedCredential) + .then(fail, function(error) { + // onUpgradeError should not be called. email-already-in-use error + // should be processed in handlers to trigger the account linking flow. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // linkWithCredential called since an anonymous user is available + // on external Auth instance. + app.getExternalAuth().currentUser.assertLinkWithCredential( + [expectedCredential], + null, + expectedError); + app.getExternalAuth().process(); + app.getAuth().process(); +} + + +function testSignInWithExistingEmailAndPasswordForLinking_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.signInWithExistingEmailAndPasswordForLinking( + 'user@example.com', 'password') + .then(function(user) { + assertEquals(app.getAuth().currentUser, user); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + function() { + app.getAuth().setUser(expectedUser); + return app.getAuth().currentUser; + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testSignInWithExistingEmailAndPasswordForLinking_error() { + var expectedError = { + 'code': 'auth/wrong-password', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.signInWithExistingEmailAndPasswordForLinking( + 'user@example.com', 'password') + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertSignInWithEmailAndPassword( + ['user@example.com', 'password'], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testClearTempAuthState_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.clearTempAuthState() + .then(function() { + asyncTestCase.signal(); + }); + app.getAuth().assertSignOut( + []); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testClearTempAuthState_error() { + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.clearTempAuthState() + .then(fail, function(error) { + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + app.getAuth().assertSignOut( + [], + null, + expectedError); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testGetRedirectResult_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.getRedirectResult().then(function(result) { + assertEquals(expectedUserCredential, result); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + app.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + }); + // getRedirectResult called on internal instance. + app.getAuth().assertGetRedirectResult( + [], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process().then(function() { + // signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_caching_signInFlow() { + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // First call to getRedirectResult resolves with underlying + // auth.getRedirectResult. + app.getRedirectResult().then(function(result) { + assertEquals(expectedUserCredential, result); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + return app.getRedirectResult(); + }).then(function(result) { + assertNull(result.user); + // Simulate error on signInWithRedirect. + return app.startSignInWithRedirect(expectedProvider) + .then(fail, function(error) { + assertEquals(expectedError, error); + // This should still resolve to null since an error occurred on + // signInWithRedirect. + return app.getRedirectResult(); + }); + }).then(function(result) { + assertNull(result.user); + // Simulate successful signInWithRedirect. + return app.startSignInWithRedirect(expectedProvider); + }).then(function() { + // getRedirectResult should no longer return the cached result. + return app.getRedirectResult(); + }).then(function(result) { + assertEquals(expectedUserCredential, result); + asyncTestCase.signal(); + }); + + // getRedirectResult called on internal instance. + app.getAuth().assertGetRedirectResult( + [], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process().then(function() { + // signOut on reset. + app.getAuth().assertSignOut( + [], + function() { + // User would be set to null on temp Auth. + app.getAuth().setUser(null); + }); + return app.getAuth().process(); + }).then(function() { + // First call to signInWithRedirect throws an error. + app.getAuth().assertSignInWithRedirect( + [expectedProvider], + null, + expectedError); + return app.getAuth().process(); + }).then(function() { + // Second call to signInWithRedirect succeeds. + app.getAuth().assertSignInWithRedirect([expectedProvider]); + return app.getAuth().process(); + }).then(function() { + // Underlying auth.getRedirectResult should overwrite cached app + // getRedirectResult. + app.getAuth().assertGetRedirectResult( + [], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_caching_anonymousUpgradeFlow() { + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user logged in on external instance. + testAuth.setUser(anonymousUser); + // First call to getRedirectResult resolves with underlying + // auth.getRedirectResult. + app.getRedirectResult().then(function(result) { + assertEquals(expectedUserCredential, result); + app.reset(); + // Simulate old user signed out and anonymous user signed in. + testAuth.setUser(anonymousUser); + // Null result should be set after reset and no underlying Auth call should + // be made. + return app.getRedirectResult(); + }).then(function(result) { + assertNull(result.user); + // Simulate error on signInWithRedirect. + var p = app.startSignInWithRedirect(expectedProvider); + // Trigger onAuthStateChanged listener again since reset was called. + // This is needed for underlying linkWithRedirect to run. + app.getExternalAuth().runAuthChangeHandler(); + return p.then(fail, function(error) { + assertEquals(expectedError, error); + // This should still resolve to null since an error occurred on + // linkWithRedirect. + return app.getRedirectResult(); + }); + }).then(function(result) { + assertNull(result.user); + // Simulate successful signInWithRedirect. + return app.startSignInWithRedirect(expectedProvider); + }).then(function() { + // getRedirectResult should no longer return the cached result. + return app.getRedirectResult(); + }).then(function(result) { + assertEquals(expectedUserCredential, result); + asyncTestCase.signal(); + }); + + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // getRedirectResult called on internal instance. + app.getExternalAuth().assertGetRedirectResult( + [], + function() { + app.getExternalAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getExternalAuth().process().then(function() { + // signOut on reset. + app.getAuth().assertSignOut( + [], + function() { + // User would be set to null on temp Auth. + app.getAuth().setUser(null); + }); + return app.getAuth().process(); + }).then(function() { + // First call to linkWithRedirect fails. + app.getExternalAuth().currentUser.assertLinkWithRedirect( + [expectedProvider], + null, + expectedError); + return app.getExternalAuth().process(); + }).then(function() { + return goog.Promise.resolve(); + }).then(function() { + // Second call to linkWithRedirect succeeds. + app.getExternalAuth().currentUser.assertLinkWithRedirect( + [expectedProvider]); + return app.getExternalAuth().process(); + }).then(function() { + // Underlying auth.getRedirectResult should overwrite cached app + // getRedirectResult. + app.getExternalAuth().assertGetRedirectResult( + [], + function() { + app.getExternalAuth().setUser(expectedUser); + return expectedUserCredential; + }); + return app.getExternalAuth().process(); + }); +} + + +function testGetRedirectResult_error() { + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + app.getRedirectResult().then(fail, function(error) { + assertEquals(expectedError, error); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + app.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + }); + // getRedirectResult called on internal instance and expected error thrown. + app.getAuth().assertGetRedirectResult( + [], + null, + expectedError); + app.getAuth().process().then(function() { + // signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_upgradeAnonymous_success() { + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user upgraded successfully. + testAuth.setUser(expectedUser); + app.getRedirectResult().then(function(result) { + assertEquals(expectedUserCredential, result); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + app.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // getRedirectResult called on internal instance first. + app.getAuth().assertGetRedirectResult( + [], + { + 'user': null, + 'credential': null + }); + app.getAuth().process().then(function() { + // getRedirectResult called on external instance after no result found. + app.getExternalAuth().assertGetRedirectResult( + [], + function() { + app.getExternalAuth().setUser(expectedUser); + return expectedUserCredential; + }); + return app.getExternalAuth().process(); + }).then(function() { + // signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_upgradeAnonymous_isAnon_pendingCred_success() { + // Simulate eligible anonymous user upgrade with pending email credential + // after email already exists error. + // Typical flow: + // 1. eligible anonymous user + // 2. linkWithRedirect -> email exists + // 3. Save pending credential + // 4. sign in with redirect to existing account on internal instance. + // 5. Get redirect result from internal instance and clear credential after + // linking. + // 6. sign out and pass credential for merge conflict handling. + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + // Save pending email credential. + firebaseui.auth.storage.setPendingEmailCredential( + pendingEmailCredential, 'id0'); + app.getAuth().install(); + app.getExternalAuth().install(); + // Set anonymous user on external instance. + testAuth.setUser(anonymousUser); + asyncTestCase.waitForSignals(1); + app.getRedirectResult().then(function(result) { + assertEquals(expectedUserCredential, result); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + app.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // getRedirectResult called on internal instance as a pending credential is + // detected. + app.getAuth().assertGetRedirectResult( + [], + function() { + app.getAuth().setUser(expectedUser); + return expectedUserCredential; + }); + app.getAuth().process().then(function() { + // signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_upgradeAnonymous_emailAlreadyInUseError() { + // Tests anonymous user upgrade linkWithRedirect flow that fails with email + // already in use error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user failed to upgrade. + testAuth.setUser(anonymousUser); + app.getRedirectResult().then(fail, function(error) { + // onUpgradeError should not be called as the credential should be first + // linked with the existing user before the upgrade error is thrown. + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals(expectedError, error); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + app.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // getRedirectResult called on external instance since an anonymous user + // that is eligible for upgrade is detected (no pending credential is + // detected). + app.getExternalAuth().assertGetRedirectResult( + [], + null, + expectedError); + app.getExternalAuth().process().then(function() { + // signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_upgradeAnonymous_credentialAlreadyInUseError() { + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user failed to upgrade. + testAuth.setUser(anonymousUser); + app.getRedirectResult().then(fail, function(error) { + // onUpgradeError should be called with expected arguments. + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals( + error, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + assertUndefined( + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertEquals(expectedError, error); + app.reset(); + // Null result should be set after reset and no underlying Auth call should + // be made. + app.getRedirectResult().then(function(result) { + assertNull(result.user); + asyncTestCase.signal(); + }); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getAuth().process().then(function() { + // getRedirectResult called on external instance since an anonymous user + // that is eligible for upgrade is detected. + app.getExternalAuth().assertGetRedirectResult( + [], + null, + expectedError); + return app.getExternalAuth().process(); + }).then(function() { + // signOut on reset. + app.getAuth().assertSignOut([]); + return app.getAuth().process(); + }); +} + + +function testGetRedirectResult_upgradeAnonymous_otherError() { + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + // Record calls on onUpgradeError. + testStubs.replace( + firebaseui.auth.AuthUI.prototype, + 'onUpgradeError', + goog.testing.recordFunction(function(error) { + return goog.Promise.reject(error); + })); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user failed to upgrade. + testAuth.setUser(anonymousUser); + app.getRedirectResult().then(fail, function(error) { + // onUpgradeError should be called. + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.onUpgradeError.getCallCount()); + assertEquals( + error, + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(0)); + assertUndefined( + firebaseui.auth.AuthUI.prototype.onUpgradeError.getLastCall() + .getArgument(1)); + assertEquals(expectedError, error); + asyncTestCase.signal(); + }); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + app.getAuth().process().then(function() { + // getRedirectResult called on external instance since anonymous user + // that is eligible for upgrade is detected. + app.getExternalAuth().assertGetRedirectResult( + [], + null, + expectedError); + app.getExternalAuth().process(); + }); +} + + +function testOnUpgradeError_credentialAlreadyInUseError() { + // User tries to sign in with an existing federated/phone number account which + // will fail to upgrade. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'email': expectedUser['email'], + 'credential': expectedCredential + }; + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + expectedCredential); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + // Render some UI for testing. + firebaseui.auth.widget.handler.handlePasswordSignIn(app, container1); + + asyncTestCase.waitForSignals(1); + app.onUpgradeError(expectedError).thenCatch(function(error) { + // Error funnelled through. + assertEquals(expectedError, error); + // Confirm current component is null. + assertNull(app.getCurrentComponent()); + // Confirm UI cleared. + assertEquals(0, container1.children.length); + // Confirm signInFailure called with expected UI error. + assertEquals( + 1, anonymousUpgradeConfig.callbacks.signInFailure.getCallCount()); + assertObjectEquals( + expectedMergeError, + anonymousUpgradeConfig.callbacks.signInFailure.getLastCall() + .getArgument(0)); + asyncTestCase.signal(); + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testOnUpgradeError_emailAlreadyInUseError() { + // User tries to sign in with an existing email/password account which will + // fail to upgrade. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'message': 'MESSAGE' + }; + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + expectedCredential); + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + // Render some UI for testing. + firebaseui.auth.widget.handler.handlePasswordSignIn(app, container1); + + asyncTestCase.waitForSignals(1); + app.onUpgradeError(expectedError, expectedCredential) + .thenCatch(function(error) { + // Error funnelled through. + assertEquals(expectedError, error); + // Confirm current component is null. + assertNull(app.getCurrentComponent()); + // Confirm UI cleared. + assertEquals(0, container1.children.length); + // Confirm signInFailure called with expected UI error. + assertEquals( + 1, anonymousUpgradeConfig.callbacks.signInFailure.getCallCount()); + assertObjectEquals( + expectedMergeError, + anonymousUpgradeConfig.callbacks.signInFailure.getLastCall() + .getArgument(0)); + asyncTestCase.signal(); + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} + + +function testOnUpgradeError_otherError() { + // All other errors should just pass through. + var expectedError = { + 'code': 'auth/internal-error', + 'message': 'MESSAGE' + }; + testApp = new firebaseui.auth.testing.FakeAppClient(options); + testAuth = testApp.auth(); + app = new firebaseui.auth.AuthUI(testAuth, 'id0'); + // Simulate autoUpgradeAnonymousUsers set to true. + app.setConfig(anonymousUpgradeConfig); + app.getAuth().install(); + app.getExternalAuth().install(); + // Render some UI for testing. + firebaseui.auth.widget.handler.handlePasswordSignIn(app, container1); + + asyncTestCase.waitForSignals(1); + app.onUpgradeError(expectedError).thenCatch(function(error) { + // Error funnelled through. + assertEquals(expectedError, error); + // Confirm current component is not null. + assertNotNull(app.getCurrentComponent()); + // Confirm UI not cleared. + assertEquals(1, container1.children.length); + // Confirm signInFailure not called. + assertEquals( + 0, anonymousUpgradeConfig.callbacks.signInFailure.getCallCount()); + asyncTestCase.signal(); + }); + app.getAuth().process(); + app.getExternalAuth().process(); +} diff --git a/javascript/widgets/authuierror.js b/javascript/widgets/authuierror.js new file mode 100644 index 00000000..c7225c53 --- /dev/null +++ b/javascript/widgets/authuierror.js @@ -0,0 +1,87 @@ +/* + * Copyright 2018 Google Inc. 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. + */ + +/** + * @fileoverview FirebaseUI error. + */ + +goog.provide('firebaseui.auth.AuthUIError'); + +goog.require('firebaseui.auth.soy2.strings'); + + +/** + * Error that can be returned to the developer. + * @param {!firebaseui.auth.AuthUIError.Error} code The short error code. + * @param {?string=} opt_message The human-readable message. + * @param {?firebase.auth.AuthCredential=} opt_credential The Auth credential + * that failed to link to the anonymous user. + * @constructor + * @extends {Error} + */ +firebaseui.auth.AuthUIError = function(code, opt_message, opt_credential) { + this['code'] = firebaseui.auth.AuthUIError.ERROR_CODE_PREFIX + code; + this['message'] = opt_message || + firebaseui.auth.AuthUIError.getDefaultErrorMessage_(this['code']) || ''; + this['credential'] = opt_credential || null; +}; +goog.inherits(firebaseui.auth.AuthUIError, Error); + + +/** + * @return {!Object} The plain object form of the error. + */ +firebaseui.auth.AuthUIError.prototype.toPlainObject = function() { + return { + 'code': this['code'], + 'message': this['message'], + }; +}; + + +/** + * @return {!Object} The plain object form of the error. This is used by + * JSON.stringify() to return the stringified representation of the error. + * @override + */ +firebaseui.auth.AuthUIError.prototype.toJSON = function() { + return this.toPlainObject(); +}; + + +/** + * The error prefix for firebaseui.auth.AuthUIError. + * @protected {string} + */ +firebaseui.auth.AuthUIError.ERROR_CODE_PREFIX = 'firebaseui/'; + + +/** + * Developer facing FirebaseUI error codes. + * @enum {string} + */ +firebaseui.auth.AuthUIError.Error = { + MERGE_CONFLICT: 'anonymous-upgrade-merge-conflict' +}; + + +/** + * Maps the error code to the default error message. + * @param {string} code The error code. + * @return {string} The display error message. + * @private + */ +firebaseui.auth.AuthUIError.getDefaultErrorMessage_ = function(code) { + return firebaseui.auth.soy2.strings.errorAuthUI({code: code}).toString(); +}; diff --git a/javascript/widgets/authuierror_test.js b/javascript/widgets/authuierror_test.js new file mode 100644 index 00000000..bcb4f6e6 --- /dev/null +++ b/javascript/widgets/authuierror_test.js @@ -0,0 +1,64 @@ +/* + * Copyright 2018 Google Inc. 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. + */ + +/** + * @fileoverview Tests for authuierror.js + */ + +goog.provide('firebaseui.auth.AuthUIErrorTest'); + +goog.require('firebaseui.auth.AuthUIError'); +goog.require('firebaseui.auth.soy2.strings'); +goog.require('goog.testing.jsunit'); + +goog.setTestOnly(); + + +function testAuthUIError() { + var authCredential = {'accessToken': 'googleAccessToken', + 'providerId': 'google.com'}; + var error = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, undefined, + authCredential); + assertEquals('firebaseui/anonymous-upgrade-merge-conflict', error['code']); + assertEquals( + firebaseui.auth.soy2.strings.errorAuthUI( + {code: error['code']}).toString(), + error['message']); + // Test toJSON(). Do not expose credential in JSON object. + assertObjectEquals({ + code: error['code'], + message: error['message'], + }, error.toJSON()); + // Make sure JSON.stringify works and uses underlying toJSON. + assertEquals(JSON.stringify(error), JSON.stringify(error.toJSON())); +} + + +function testAuthUIError_customMessage() { + var authCredential = {'accessToken': 'googleAccessToken', + 'providerId': 'google.com'}; + var error = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, 'merge conflict error', + authCredential); + assertEquals('firebaseui/anonymous-upgrade-merge-conflict', error['code']); + assertEquals('merge conflict error', error['message']); + // Test toJSON(). Do not expose credential in JSON object. + assertObjectEquals({ + code: error['code'], + message: error['message'], + }, error.toJSON()); + // Make sure JSON.stringify works and uses underlying toJSON. + assertEquals(JSON.stringify(error), JSON.stringify(error.toJSON())); +} diff --git a/javascript/widgets/config.js b/javascript/widgets/config.js index d899a6c4..d71ab678 100644 --- a/javascript/widgets/config.js +++ b/javascript/widgets/config.js @@ -17,9 +17,11 @@ */ goog.provide('firebaseui.auth.CredentialHelper'); +goog.provide('firebaseui.auth.callback.signInFailure'); goog.provide('firebaseui.auth.callback.signInSuccess'); goog.provide('firebaseui.auth.widget.Config'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.Config'); goog.require('firebaseui.auth.PhoneNumber'); goog.require('firebaseui.auth.data.country'); @@ -41,6 +43,7 @@ firebaseui.auth.widget.Config = function() { this.config_ = new firebaseui.auth.Config(); // Define FirebaseUI widget configurations and convenient getters. this.config_.define('acUiConfig'); + this.config_.define('autoUpgradeAnonymousUsers'); this.config_.define('callbacks'); /** * Determines which credential helper to use. Currently, only @@ -95,6 +98,13 @@ firebaseui.auth.CredentialHelper = { firebaseui.auth.callback.signInSuccess; +/** + * The configuration sign-in failure callback. + * @typedef {function(!firebaseui.auth.AuthUIError): (!Promise|void)} + */ +firebaseui.auth.callback.signInFailure; + + /** * The accountchooser.com result codes. * @@ -212,6 +222,22 @@ firebaseui.auth.widget.Config.prototype.getSignInSuccessUrl = function() { }; +/** @return {boolean} Whether to auto upgrade anonymous users. */ +firebaseui.auth.widget.Config.prototype.autoUpgradeAnonymousUsers = function() { + var autoUpgradeAnonymousUsers = + !!this.config_.get('autoUpgradeAnonymousUsers'); + // Confirm signInFailure callback is provided when anonymous upgrade is + // enabled. This is required to provide a means of recovery for merge conflict + // flows. + if (autoUpgradeAnonymousUsers && !this.getSignInFailureCallback()) { + firebaseui.auth.log.error('Missing "signInFailure" callback: ' + + '"signInFailure" callback needs to be provided when ' + + '"autoUpgradeAnonymousUsers" is set to true.'); + } + return autoUpgradeAnonymousUsers; +}; + + /** * Returns the normalized list of valid user-enabled IdPs. * @@ -596,8 +622,8 @@ firebaseui.auth.widget.Config.prototype.getAccountChooserResultCallback = * into the callback. A second parameter, the Auth credential is also * returned if available from the sign in with redirect response. * An optional third parameter, the redirect URL, is also returned if that - * value is set in storage. If it returns {@code true}, the widget will - * continue to redirect the page to {@code signInSuccessUrl}. Otherwise, the + * value is set in storage. If it returns `true`, the widget will + * continue to redirect the page to `signInSuccessUrl`. Otherwise, the * widget stops after it returns. */ firebaseui.auth.widget.Config.prototype.getSignInSuccessCallback = function() { @@ -606,6 +632,16 @@ firebaseui.auth.widget.Config.prototype.getSignInSuccessCallback = function() { }; +/** + * @return {?firebaseui.auth.callback.signInFailure} The callback to invoke when + * the user fails to sign in. + */ +firebaseui.auth.widget.Config.prototype.getSignInFailureCallback = function() { + return /** @type {?firebaseui.auth.callback.signInFailure} */ ( + this.getCallbacks_()['signInFailure'] || null); +}; + + /** * @return {!Object} The callback configuration. * @private diff --git a/javascript/widgets/config_test.js b/javascript/widgets/config_test.js index 32dd1941..0445a2a1 100644 --- a/javascript/widgets/config_test.js +++ b/javascript/widgets/config_test.js @@ -859,17 +859,20 @@ function testGetCallbacks() { var uiChangedCallback = function() {}; var accountChooserInvokedCallback = function() {}; var accountChooserResultCallback = function() {}; + var signInFailureCallback = function() {}; assertNull(config.getUiShownCallback()); assertNull(config.getSignInSuccessCallback()); assertNull(config.getUiChangedCallback()); assertNull(config.getAccountChooserInvokedCallback()); assertNull(config.getAccountChooserResultCallback()); + assertNull(config.getAccountChooserResultCallback()); config.update('callbacks', { 'uiShown': uiShownCallback, 'signInSuccess': signInSuccessCallback, 'uiChanged': uiChangedCallback, 'accountChooserInvoked': accountChooserInvokedCallback, - 'accountChooserResult': accountChooserResultCallback + 'accountChooserResult': accountChooserResultCallback, + 'signInFailure': signInFailureCallback }); assertEquals(uiShownCallback, config.getUiShownCallback()); assertEquals( @@ -881,6 +884,49 @@ function testGetCallbacks() { assertEquals( accountChooserResultCallback, config.getAccountChooserResultCallback()); + assertEquals( + signInFailureCallback, config.getSignInFailureCallback()); +} + + +function testAutoUpgradeAnonymousUsers() { + var expectedErrorLogMessage = 'Missing "signInFailure" callback: ' + + '"signInFailure" callback needs to be provided when ' + + '"autoUpgradeAnonymousUsers" is set to true.'; + assertFalse(config.autoUpgradeAnonymousUsers()); + + config.update('autoUpgradeAnonymousUsers', ''); + assertFalse(config.autoUpgradeAnonymousUsers()); + + config.update('autoUpgradeAnonymousUsers', null); + assertFalse(config.autoUpgradeAnonymousUsers()); + + config.update('autoUpgradeAnonymousUsers', false); + assertFalse(config.autoUpgradeAnonymousUsers()); + + // No error or warning should be logged. + assertArrayEquals([], errorLogMessages); + assertArrayEquals([], warningLogMessages); + + // Set autoUpgradeAnonymousUsers to true without providing a signInFailure + // callback. + config.update('autoUpgradeAnonymousUsers', true); + assertTrue(config.autoUpgradeAnonymousUsers()); + // Error should be logged. + assertArrayEquals([expectedErrorLogMessage], errorLogMessages); + assertArrayEquals([], warningLogMessages); + + // Provide the signInFailure callback. + config.update('callbacks', { + 'signInFailure': goog.nullFunction + }); + config.update('autoUpgradeAnonymousUsers', 'true'); + assertTrue(config.autoUpgradeAnonymousUsers()); + config.update('autoUpgradeAnonymousUsers', 1); + assertTrue(config.autoUpgradeAnonymousUsers()); + // No additional error logged. + assertArrayEquals([expectedErrorLogMessage], errorLogMessages); + assertArrayEquals([], warningLogMessages); } diff --git a/javascript/widgets/exports_app.js b/javascript/widgets/exports_app.js index 5fae8b89..430f5bb6 100644 --- a/javascript/widgets/exports_app.js +++ b/javascript/widgets/exports_app.js @@ -14,6 +14,7 @@ goog.provide('firebaseui.auth.exports'); goog.require('firebaseui.auth.AuthUI'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.CredentialHelper'); goog.exportSymbol('firebaseui.auth.AuthUI', firebaseui.auth.AuthUI); @@ -38,6 +39,13 @@ goog.exportSymbol( goog.exportSymbol( 'firebaseui.auth.AuthUI.prototype.delete', firebaseui.auth.AuthUI.prototype.delete); +goog.exportSymbol( + 'firebaseui.auth.AuthUI.prototype.isPendingRedirect', + firebaseui.auth.AuthUI.prototype.isPendingRedirect); +goog.exportSymbol('firebaseui.auth.AuthUIError', firebaseui.auth.AuthUIError); +goog.exportSymbol( + 'firebaseui.auth.AuthUIError.prototype.toJSON', + firebaseui.auth.AuthUIError.prototype.toJSON); goog.exportSymbol( 'firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM', firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM); diff --git a/javascript/widgets/handler/actioncode_test.js b/javascript/widgets/handler/actioncode_test.js index 8541b232..e194aad2 100644 --- a/javascript/widgets/handler/actioncode_test.js +++ b/javascript/widgets/handler/actioncode_test.js @@ -96,6 +96,7 @@ function testHandlePasswordReset_reset() { app.getAuth().process().then(function() { assertPasswordResetPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -128,6 +129,7 @@ function testHandlePasswordReset_inProcessing() { // Password reset success page should show. assertPasswordResetSuccessPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -150,6 +152,7 @@ function testHandlePasswordReset_failToCheckActionCode() { // Password reset failure page should show. assertPasswordResetFailurePage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -231,6 +234,7 @@ function testHandleEmailChangeRevocation_success() { // Successful revocation. assertEmailChangeRevokeSuccessPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -246,6 +250,7 @@ function testHandleEmailChangeRevocation_reset() { firebaseui.auth.widget.handler.handleEmailChangeRevocation( app, container, 'EMAIL_CHANGE_REVOKE_ACTION_CODE'); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -281,6 +286,7 @@ function testHandleEmailChangeRevocation_resetPassword_success() { // We should notify the user that the recovery email was sent. assertPasswordRecoveryEmailSentPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -320,6 +326,7 @@ function testHandleEmailChangeRevocation_resetPassword_failure() { assertInfoBarMessage(firebaseui.auth.soy2.strings.errorSendPasswordReset() .toString()); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -342,6 +349,7 @@ function testHandleEmailChangeRevocation_checkActionCodefailure() { // Email change revocation failure page should show. assertEmailChangeRevokeFailurePage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -385,6 +393,7 @@ function testHandleEmailVerification_success() { // No continue button should be displayed. assertNull(getSubmitButton()); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -426,6 +435,7 @@ function testHandleEmailVerification_reset() { firebaseui.auth.widget.handler.handleEmailVerification( app, container, 'EMAIL_VERIFICATION_ACTION_CODE'); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -451,6 +461,7 @@ function testHandleEmailVerification_failure() { // Email verification failure page should show. assertEmailVerificationFailurePage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); diff --git a/javascript/widgets/handler/callback.js b/javascript/widgets/handler/callback.js index 31e71504..e1e5d32a 100644 --- a/javascript/widgets/handler/callback.js +++ b/javascript/widgets/handler/callback.js @@ -50,14 +50,15 @@ firebaseui.auth.widget.handler.handleCallback = firebaseui.auth.widget.handler.handleCallbackResult_(app, component, result); }, function(error) { - // A previous redirect operation was triggered and some error occured. + // A previous redirect operation was triggered and some error occurred. // Test for need confirmation error and handle appropriately. // For all other errors, display info bar and show sign in screen. if (error && // Single out need confirmation error as email-already-in-use and // credential-already-in-use will also return email and credential // and need to be handled differently. - error['code'] == 'auth/account-exists-with-different-credential' && + (error['code'] == 'auth/account-exists-with-different-credential' || + error['code'] == 'auth/email-already-in-use') && error['email'] && error['credential']) { // Save pending email credential. @@ -92,6 +93,9 @@ firebaseui.auth.widget.handler.handleCallback = firebaseui.auth.widget.handler.handleCallbackFailure_( app, component, /** @type {!Error} */ (error)); } + } else if (error && error['code'] == 'auth/credential-already-in-use') { + // Do nothing and keep callback UI while onUpgradeError catches and + // handles this error. } else if (error && error['code'] == 'auth/operation-not-supported-in-this-environment' && firebaseui.auth.widget.handler.common.isPasswordProviderOnly(app)) { @@ -295,7 +299,7 @@ firebaseui.auth.widget.handler.handleCallbackEmailMismatch_ = var container = component.getContainer(); // On email mismatch, sign out the temporary user to avoid leaking this // temp auth session if the user decides to close the window. - app.registerPending(app.getAuth().signOut().then(function() { + app.registerPending(app.clearTempAuthState().then(function() { component.dispose(); firebaseui.auth.widget.handler.handle( firebaseui.auth.widget.HandlerName.EMAIL_MISMATCH, diff --git a/javascript/widgets/handler/callback_test.js b/javascript/widgets/handler/callback_test.js index d3a8762c..f0846caf 100644 --- a/javascript/widgets/handler/callback_test.js +++ b/javascript/widgets/handler/callback_test.js @@ -19,6 +19,7 @@ goog.provide('firebaseui.auth.widget.handler.CallbackTest'); goog.setTestOnly('firebaseui.auth.widget.handler.CallbackTest'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.CredentialHelper'); goog.require('firebaseui.auth.PendingEmailCredential'); goog.require('firebaseui.auth.idp'); @@ -153,6 +154,7 @@ function testHandleCallback_reset() { firebaseui.auth.widget.handler.handleCallback(app, container); assertCallbackPage(); // Reset current rendered widget. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -439,6 +441,7 @@ function testHandleCallback_redirectUser_noPendingCredential_signInCallback() { [cred], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // Pending credential should be cleared from storage. assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( app.getAppId())); @@ -610,6 +613,7 @@ function testHandleCallback_redirectUser_pendingCredential_signInCallback() { [cred], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // Pending credential should be cleared from storage. assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( app.getAppId())); @@ -1979,3 +1983,274 @@ function testHandleCallback_operationNotSupported_passwordOnly_acEnabled() { }); } + +function testHandleCallback_anonymousUpgrade_redirect_success() { + // Test successful anonymous user upgrade. + asyncTestCase.waitForSignals(1); + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // User should be signed in. + externalAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + // Callback rendered. + firebaseui.auth.widget.handler.handleCallback(app, container); + assertCallbackPage(); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // getRedirectResult called on internal instance first. + app.getAuth().assertGetRedirectResult( + [], + { + 'user': null, + 'credential': null + }); + app.getAuth().process().then(function() { + // getRedirectResult called on external instance after no result found. + app.getExternalAuth().assertGetRedirectResult( + [], + { + 'user': externalAuth.currentUser, + 'credential': cred + }); + return app.getExternalAuth().process(); + }).then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // User should be redirected to success URL. + testUtil.assertGoTo('http://localhost/home'); + asyncTestCase.signal(); + }); +} + + +function testHandleCallback_anonymousUpgrade_redirect_error() { + // Test anonymous user upgrade with merge conflict error. + asyncTestCase.waitForSignals(1); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // Expected getRedirectResult error on merge conflict. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected signInFailure FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + cred); + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user on external Auth instance. + externalAuth.setUser(anonymousUser); + // Callback rendered. + firebaseui.auth.widget.handler.handleCallback(app, container); + assertCallbackPage(); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // External getRedirectResult called with expected error. + app.getExternalAuth().assertGetRedirectResult( + [], + null, + expectedError); + app.getExternalAuth().process().then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // signInFailure callback triggered with expected FirebaseUI error. + assertSignInFailure(expectedMergeError); + asyncTestCase.signal(); + }); +} + + +function testHandleCallback_anonymousUpgrade_emailAlreadyInUse_fedLinking() { + // Test anonymous user upgrade linkWithRedirect throwing email already in use + // error where the existing email belongs to a federated account. + asyncTestCase.waitForSignals(1); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), cred); + // Expected linkWithRedirect error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + // Render callback handler. + firebaseui.auth.widget.handler.handleCallback(app, container); + assertCallbackPage(); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // Assert getRedirectResult called on external instance and expected email + // already in use error thrown. + app.getExternalAuth().assertGetRedirectResult( + [], + null, + expectedError); + app.getExternalAuth().process().then(function() { + // As account already exists, user must sign in to existing account. + // In this case, the existing account is a google account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['google.com']); + return testAuth.process(); + }).then(function() { + // The pending credential should be saved here. + assertObjectEquals( + pendingEmailCred, + firebaseui.auth.storage.getPendingEmailCredential(app.getAppId())); + // Federated linking flow should be triggered. + assertFederatedLinkingPage(federatedAccount.getEmail()); + asyncTestCase.signal(); + }); +} + + +function testHandleCallback_anonymousUpgrade_emailAlreadyInUse_passLinking() { + // Test anonymous user upgrade linkWithRedirect throwing email already in use + // error where the existing email belongs to a password account. + asyncTestCase.waitForSignals(1); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // Expected linkWithRedirect error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + // Render callback handler. + firebaseui.auth.widget.handler.handleCallback(app, container); + assertCallbackPage(); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // Assert getRedirectResult called on external instance and expected email + // already in use error thrown. + app.getExternalAuth().assertGetRedirectResult( + [], + null, + expectedError); + app.getExternalAuth().process().then(function() { + // Simulate existing account is a password account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['password']); + return testAuth.process(); + }).then(function() { + // The pending email credential should be cleared at this point. + // Password linking does not require a redirect so no need to save it + // anyway. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // Password linking page rendered. + assertPasswordLinkingPage(federatedAccount.getEmail()); + asyncTestCase.signal(); + }); +} + + +function testHandleCallback_anonymousUpgrade_pendingCredential_success() { + // Test successful return from regular sign in operation with pending + // credentials requiring linking. + asyncTestCase.waitForSignals(1); + // Enabled anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // The new credential to link. + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // The existing credential to sign in to. + var cred2 = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'facebook.com', + 'accessToken': 'ACCESS_TOKEN' + }); + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), cred); + // Expected linkAndRetrieveDataWithCredential error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected signInFailure FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + cred); + // Simulate previous linking required (pending credentials should be saved). + firebaseui.auth.storage.setPendingEmailCredential( + pendingEmailCred, app.getAppId()); + // Anonymous user signed in externally. + externalAuth.setUser(anonymousUser); + // Callback rendered. + firebaseui.auth.widget.handler.handleCallback(app, container); + assertCallbackPage(); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // Assert get redirect result called on internal instance to complete sign in + // to existing credential. + testAuth.assertGetRedirectResult( + [], + function() { + // User should be signed in at this point. + testAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + return { + 'user': testAuth.currentUser, + 'credential': cred2 + }; + }); + testAuth.process().then(function() { + // Linking should be triggered with pending credential. + testAuth.currentUser.assertLinkWithCredential([cred], testAuth.currentUser); + return testAuth.process(); + // Sign out from internal instance and then sign in with passed credential + // to external instance. + }).then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Linking existing credential to anonymous user should fail with expected + // error. + externalAuth.currentUser.assertLinkWithCredential( + [cred], + null, + expectedError); + return externalAuth.process(); + }).then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // signInFailure callback triggered with expected FirebaseUI error. + assertSignInFailure(expectedMergeError); + asyncTestCase.signal(); + }); +} diff --git a/javascript/widgets/handler/common.js b/javascript/widgets/handler/common.js index b9e69d57..57250daf 100644 --- a/javascript/widgets/handler/common.js +++ b/javascript/widgets/handler/common.js @@ -199,8 +199,14 @@ firebaseui.auth.widget.handler.common.handleAcEmptyResponse_ = function( // in continue callback function to be passed to accountchooser.com invoked // handler. var continueCallback = function() { + // Sets pending redirect status before redirect to + // accountchooser.com. + firebaseui.auth.storage.setPendingRedirectStatus(app.getAppId()); firebaseui.auth.acClient.trySelectAccount( function(isAvailable) { + // Removes the pending redirect status if does not get + // redirected to accountchooser.com. + firebaseui.auth.storage.removePendingRedirectStatus(app.getAppId()); // On empty response, post accountchooser.com result (either empty // or unavailable). firebaseui.auth.widget.handler.common.accountChooserResult( @@ -392,6 +398,7 @@ firebaseui.auth.widget.handler.common.selectFromAccountChooser = function( * the user was already signed out from the temporary auth instance. * @param {boolean=} opt_alreadySignedIn Whether user already signed in on * external auth instance. + * @return {!goog.Promise} A promise that resolves on login completion. * @package */ firebaseui.auth.widget.handler.common.setLoggedIn = @@ -403,7 +410,7 @@ firebaseui.auth.widget.handler.common.setLoggedIn = component, /** @type {!firebase.User} */ (app.getExternalAuth().currentUser), credential); - return; + return goog.Promise.resolve(); } // This should not occur. if (!credential) { @@ -451,14 +458,17 @@ firebaseui.auth.widget.handler.common.setLoggedIn = }; // In some cases like email mismatch, the temporary user may be signed out. // In that case, get the current temporary user directly. - var tempUser = app.getAuth().currentUser || opt_user; + var tempUser = app.getAuth().currentUser || opt_user || + app.getExternalAuth().currentUser; if (!tempUser) { // Shouldn't happen as we're only calling this method internally. throw new Error('User not logged in.'); } // Sign out from internal auth instance before signing in to external // instance. - app.registerPending(app.getAuth().signOut().then(function() { + // Wrap in a promise to ensure the progress bar remains visible until the + // underlying signInWithCredential resolves. + var signOutAndSignInPromise = app.clearTempAuthState().then(function() { // Save before signing in to developer's auth instance to make sure account // is saved without risking interruption from onAuthStateChanged. var account = new firebaseui.auth.Account( @@ -476,14 +486,18 @@ firebaseui.auth.widget.handler.common.setLoggedIn = firebaseui.auth.storage.removeRememberAccount(app.getAppId()); // After successful sign out from internal instance, sign in with credential // to the developer provided auth instance. Use the credential passed. - app.registerPending(app.getExternalAuth().signInWithCredential( + var finishSignInPromise = app.finishSignInWithCredential( /** @type {!firebase.auth.AuthCredential} */ (credential)) .then(function(user) { firebaseui.auth.widget.handler.common.setUserLoggedInExternal_( app, component, user, outputCred); // Catch error when signInSuccessUrl is required and not provided. - }, onError).then(function() {}, onError)); - }, onError)); + }, onError).then(function() {}, onError); + app.registerPending(finishSignInPromise); + return finishSignInPromise; + }, onError); + app.registerPending(signOutAndSignInPromise); + return goog.Promise.resolve(signOutAndSignInPromise); }; @@ -706,6 +720,9 @@ firebaseui.auth.widget.handler.common.federatedSignIn = function( app, component, providerId, opt_email) { var container = component.getContainer(); var providerSigninFailedCallback = function(error) { + // Removes the pending redirect status being set previously + // if sign-in with redirect fails. + firebaseui.auth.storage.removePendingRedirectStatus(app.getAppId()); // TODO: align redirect and popup flow error handling for similar errors. // Ignore error if cancelled by the client. if (error['name'] && error['name'] == 'cancel') { @@ -716,15 +733,57 @@ firebaseui.auth.widget.handler.common.federatedSignIn = function( error); component.showInfoBar(errorMessage); }; - + // Error handler for signInWithPopup and getRedirectResult on Cordova. + var signInResultErrorCallback = function(error) { + // Clear pending redirect status if redirect on Cordova fails. + firebaseui.auth.storage.removePendingRedirectStatus(app.getAppId()); + // Ignore error if cancelled by the client. + if (error['name'] && error['name'] == 'cancel') { + return; + } + switch (error['code']) { + case 'auth/popup-blocked': + // Popup blocked, switch to redirect flow as fallback. + processRedirect(); + break; + case 'auth/popup-closed-by-user': + case 'auth/cancelled-popup-request': + // When popup is closed or when the user clicks another button, + // do nothing. + break; + case 'auth/credential-already-in-use': + // Do nothing when anonymous user is getting updated. + // Developer should handle this in signInFailure callback. + break; + case 'auth/network-request-failed': + case 'auth/too-many-requests': + case 'auth/user-cancelled': + // For no action errors like network error, just display in info + // bar in current component. A second attempt could still work. + component.showInfoBar( + firebaseui.auth.widget.handler.common.getErrorMessage(error)); + break; + default: + // Either linking required errors or errors that are + // unrecoverable. + component.dispose(); + firebaseui.auth.widget.handler.handle( + firebaseui.auth.widget.HandlerName.CALLBACK, + app, + container, + goog.Promise.reject(error)); + break; + } + }; // Initialize the corresponding provider. var provider = firebaseui.auth.widget.handler.common.getAuthProvider_( app, providerId, opt_email); // Redirect processor. var processRedirect = function() { + firebaseui.auth.storage.setPendingRedirectStatus(app.getAppId()); app.registerPending(component.executePromiseRequest( /** @type {function (): !goog.Promise} */ ( - goog.bind(app.getAuth().signInWithRedirect, app.getAuth())), + goog.bind(app.startSignInWithRedirect, app)), [provider], function() { // Only run below logic if the environment is potentially a Cordova @@ -736,16 +795,20 @@ firebaseui.auth.widget.handler.common.federatedSignIn = function( // This will resolve in a Cordova environment. Result should be // obtained from getRedirectResult and then treated like a // signInWithPopup operation. - return app.registerPending(app.getAuth().getRedirectResult() + return app.registerPending(app.getRedirectResult() .then(function(result) { // Pass result in promise to callback handler. component.dispose(); + // Removes pending redirect status if sign-in with redirect + // resolves in Cordova environment. + firebaseui.auth.storage.removePendingRedirectStatus( + app.getAppId()); firebaseui.auth.widget.handler.handle( firebaseui.auth.widget.HandlerName.CALLBACK, app, container, goog.Promise.resolve(result)); - }, providerSigninFailedCallback)); + }, signInResultErrorCallback)); }, providerSigninFailedCallback)); }; @@ -758,7 +821,7 @@ firebaseui.auth.widget.handler.common.federatedSignIn = function( } else { // Popup flow. // During rpc, no progress bar should be displayed. - app.registerPending(app.getAuth().signInWithPopup(provider).then( + app.registerPending(app.startSignInWithPopup(provider).then( function(result) { // Pass result in promise to callback handler. component.dispose(); @@ -767,42 +830,7 @@ firebaseui.auth.widget.handler.common.federatedSignIn = function( app, container, goog.Promise.resolve(result)); - }, - function(error) { - // Ignore error if cancelled by the client. - if (error['name'] && error['name'] == 'cancel') { - return; - } - switch (error['code']) { - case 'auth/popup-blocked': - // Popup blocked, switch to redirect flow as fallback. - processRedirect(); - break; - case 'auth/popup-closed-by-user': - case 'auth/cancelled-popup-request': - // When popup is closed or when the user clicks another button, - // do nothing. - break; - case 'auth/network-request-failed': - case 'auth/too-many-requests': - case 'auth/user-cancelled': - // For no action errors like network error, just display in info - // bar in current component. A second attempt could still work. - component.showInfoBar( - firebaseui.auth.widget.handler.common.getErrorMessage(error)); - break; - default: - // Either linking required errors or errors that are - // unrecoverable. - component.dispose(); - firebaseui.auth.widget.handler.handle( - firebaseui.auth.widget.HandlerName.CALLBACK, - app, - container, - goog.Promise.reject(error)); - break; - } - })); + }, signInResultErrorCallback)); } }; @@ -830,9 +858,8 @@ firebaseui.auth.widget.handler.common.handleGoogleYoloCredential = var signInWithCredential = function(firebaseCredential) { var status = false; var p = component.executePromiseRequest( - /** @type {function (): !goog.Promise} */ (goog.bind( - app.getAuth().signInAndRetrieveDataWithCredential, - app.getAuth())), + /** @type {function (): !goog.Promise} */ ( + goog.bind(app.startSignInWithCredential, app)), [firebaseCredential], function(result) { var container = component.getContainer(); @@ -847,6 +874,24 @@ firebaseui.auth.widget.handler.common.handleGoogleYoloCredential = function(error) { if (error['name'] && error['name'] == 'cancel') { return; + } else if (error && + error['code'] == 'auth/credential-already-in-use') { + // Do nothing when anonymous user is getting updated. + // Developer should handle this in signInFailure callback. + return; + } else if (error && + error['code'] == 'auth/email-already-in-use' && + error['email'] && error['credential']) { + // Email already in use error should trigger account linking flow. + // Pass error to callback handler to trigger that flow. + var container = component.getContainer(); + component.dispose(); + firebaseui.auth.widget.handler.handle( + firebaseui.auth.widget.HandlerName.CALLBACK, + app, + container, + goog.Promise.reject(error)); + return; } var errorMessage = firebaseui.auth.widget.handler.common.getErrorMessage(error); @@ -953,12 +998,12 @@ firebaseui.auth.widget.handler.common.verifyPassword = app.registerPending(component.executePromiseRequest( /** @type {function (): !goog.Promise} */ ( - goog.bind(app.getAuth().signInWithEmailAndPassword, app.getAuth())), + goog.bind(app.startSignInWithEmailAndPassword, app)), [email, password], function(user) { // Pass password credential to complete sign-in to the original auth // instance. - firebaseui.auth.widget.handler.common.setLoggedIn( + return firebaseui.auth.widget.handler.common.setLoggedIn( app, component, emailPassCred); }, function(error) { @@ -1229,8 +1274,15 @@ firebaseui.auth.widget.handler.common.handleSignInWithEmail = // routine in continue callback function to be passed to // accountchooser.com invoked handler. var continueCallback = function() { + // Sets pending redirect status before redirect to + // accountchooser.com. + firebaseui.auth.storage.setPendingRedirectStatus(app.getAppId()); firebaseui.auth.acClient.trySelectAccount( function(isAvailable) { + // Removes the pending redirect status if does not get + // redirected to accountchooser.com. + firebaseui.auth.storage.removePendingRedirectStatus( + app.getAppId()); // On empty response, post accountchooser.com result (either // empty or unavailable). var AccountChooserResult = diff --git a/javascript/widgets/handler/common_test.js b/javascript/widgets/handler/common_test.js index 71692c0a..368c9c35 100644 --- a/javascript/widgets/handler/common_test.js +++ b/javascript/widgets/handler/common_test.js @@ -19,7 +19,9 @@ goog.provide('firebaseui.auth.widget.handler.CommonTest'); goog.setTestOnly('firebaseui.auth.widget.handler.CommonTest'); goog.require('firebaseui.auth.Account'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.CredentialHelper'); +goog.require('firebaseui.auth.PendingEmailCredential'); goog.require('firebaseui.auth.idp'); goog.require('firebaseui.auth.log'); goog.require('firebaseui.auth.soy2.strings'); @@ -57,10 +59,12 @@ var federatedAccountWithProvider = new firebaseui.auth.Account( // TODO: Update all the tests when accountchooser.com handlers change. function testSelectFromAccountChooser_noResponse() { firebaseui.auth.storage.rememberAccount(passwordAccount, app.getAppId()); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); firebaseui.auth.widget.handler.common.selectFromAccountChooser(getApp, container); testAc.assertTrySelectAccount([passwordAccount]); assertFalse(firebaseui.auth.storage.hasRememberAccount(app.getAppId())); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); } @@ -71,10 +75,13 @@ function testSelectFromAccountChooser_noResponse_uiShown() { } }); testAc.setSkipSelect(true); + firebaseui.auth.storage.setPendingRedirectStatus(app.getAppId()); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); firebaseui.auth.storage.rememberAccount(passwordAccount, app.getAppId()); firebaseui.auth.widget.handler.common.selectFromAccountChooser(getApp, container); assertUiShownCallbackInvoked(); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); testAc.assertTrySelectAccount([passwordAccount]); assertFalse(firebaseui.auth.storage.hasRememberAccount(app.getAppId())); } @@ -307,6 +314,7 @@ function testSetLoggedIn_falseSignInCallback() { externalAuth.assertSignInWithCredential([cred], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); assertEquals(1, firebaseui.auth.storage.getRememberedAccounts( app.getAppId()).length); assertObjectEquals( @@ -449,6 +457,7 @@ function testSetLoggedIn_signInSuccessCallback_noRedirect() { [federatedCredential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. assertSignInSuccessCallbackInvoked( externalAuth.currentUser, @@ -532,6 +541,7 @@ function testSetLoggedIn_signInSuccessCallback_storageNoRedirect() { [federatedCredential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. // redirectUrl passed to callback, developer has to manually redirect. assertSignInSuccessCallbackInvoked( @@ -619,6 +629,7 @@ function testSetLoggedIn_signInSuccessCallback_storageManualRedirect() { [federatedCredential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. // redirectUrl passed to callback, developer has to manually redirect. assertSignInSuccessCallbackInvoked( @@ -1004,6 +1015,7 @@ function testSetLoggedIn_popup_signInSuccessCallback_noRedirect() { [federatedCredential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. assertSignInSuccessCallbackInvoked( externalAuth.currentUser, @@ -1095,6 +1107,7 @@ function testSetLoggedIn_popup_signInSuccessCallback_storageNoRedirect() { [federatedCredential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. // redirectUrl passed to callback, developer has to manually redirect. assertSignInSuccessCallbackInvoked( @@ -1144,6 +1157,7 @@ function testSetLoggedIn_popup_signInSuccessCallback_storageManualRedirect() { [federatedCredential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. // redirectUrl passed to callback, developer has to manually redirect. assertSignInSuccessCallbackInvoked( @@ -1188,6 +1202,7 @@ function testSetLoggedIn_alreadySignedIn_falseSignInCallback() { } }); externalAuth.setUser(passwordUser); + app.getAuth().assertSignOut([]); firebaseui.auth.widget.handler.common.setLoggedIn( app, testComponent, cred, null, true); assertSignInSuccessCallbackInvoked( @@ -1205,6 +1220,7 @@ function testHandleUnrecoverableError() { // Assert unrecoverable error message page with correct message. assertUnrecoverableErrorPage(errorMessage); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -1530,6 +1546,46 @@ function testHandleSignInFetchProvidersForEmail_registeredFederatedAccount() { } +function testHandleSignInWithEmail_acInitialized() { + var onPreSkip = goog.testing.recordFunction(function() { + assertTrue( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + }); + testAc.setSkipSelect(true, onPreSkip); + firebaseui.auth.widget.handler.common.handleSignInWithEmail(app, container); + assertEquals(1, onPreSkip.getCallCount()); + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertFalse(firebaseui.auth.storage.hasRememberAccount(app.getAppId())); + // Accountchooser client is already initialized. + firebaseui.auth.widget.handler.common.handleSignInWithEmail(app, container); + testAc.assertTrySelectAccount( + firebaseui.auth.storage.getRememberedAccounts(app.getAppId()), + 'http://localhost/firebaseui-widget?mode=select'); + assertEquals(2, onPreSkip.getCallCount()); + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); +} + + +function testHandleSignInWithEmail_acNotEnabled() { + testStubs.replace( + firebaseui.auth.storage, + 'setPendingRedirectStatus', + goog.testing.recordFunction()); + app.setConfig({ + 'credentialHelper': firebaseui.auth.CredentialHelper.NONE + }); + firebaseui.auth.widget.handler.common.acForceUiShown_ = true; + firebaseui.auth.widget.handler.common.handleSignInWithEmail(app, container); + assertSignInPage(); + /** @suppress {missingRequire} */ + assertEquals(0, + firebaseui.auth.storage.setPendingRedirectStatus.getCallCount()); + assertFalse(firebaseui.auth.storage.hasRememberAccount(app.getAppId())); + assertFalse(firebaseui.auth.widget.handler.common.acForceUiShown_); +} + function testLoadAccountchooserJs_externallyLoaded() { // Test accountchooser.com client loading when already loaded. // Reset loadAccountchooserJs stubs. @@ -1700,6 +1756,397 @@ function testIsPhoneProviderOnly_multipleProviders() { } +function testFederatedSignIn_success_redirectMode() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a signInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + // Confirm signInWithRedirect called underneath. + testAuth.assertSignInWithRedirect([expectedProvider]); + testAuth.process(); + +} + + +function testFederatedSignIn_error_redirectMode() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a signInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + // Confirm signInWithRedirect called underneath. + testAuth.assertSignInWithRedirect([expectedProvider], null, internalError); + testAuth.process().then(function() { + // Error in signInWithRedirect, cancel the pending redirect status. + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + asyncTestCase.signal(); + }); +} + + +function testFederatedSignIn_success_cordova() { + simulateCordovaEnvironment(); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a signInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + // Confirm signInWithRedirect called underneath. + testAuth.assertSignInWithRedirect([expectedProvider]); + return testAuth.process().then(function() { + assertTrue( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + testAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + testAuth.assertGetRedirectResult( + [], + { + 'user': testAuth.currentUser, + 'credential': cred + }); + return testAuth.process(); + }).then(function() { + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertCallbackPage(); + return testAuth.process(); + }).then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + externalAuth.setUser(testAuth.currentUser); + externalAuth.assertSignInWithCredential( + [cred], externalAuth.currentUser); + return externalAuth.process(); + }).then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // User should be redirected to success URL. + testUtil.assertGoTo('http://localhost/home'); + asyncTestCase.signal(); + }); + +} + + +function testFederatedSignIn_federatedLinkingRequiredError_cordova() { + simulateCordovaEnvironment(); + var expectedError = { + 'code': 'auth/account-exists-with-different-credential', + 'credential': federatedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), federatedCredential); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a signInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + // Confirm signInWithRedirect called underneath. + testAuth.assertSignInWithRedirect([expectedProvider]); + return testAuth.process().then(function() { + testAuth.assertGetRedirectResult( + [], + null, + expectedError); + return testAuth.process(); + }).then(function() { + assertNoInfoBarMessage(); + assertCallbackPage(); + // Simulate existing email belongs to a Facebook account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['facebook.com']); + return testAuth.process(); + }).then(function() { + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertFederatedLinkingPage(); + assertObjectEquals( + pendingEmailCred, + firebaseui.auth.storage.getPendingEmailCredential(app.getAppId())); + asyncTestCase.signal(); + }); +} + + +function testFederatedSignIn_error_cordova() { + simulateCordovaEnvironment(); + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a signInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + // Confirm signInWithRedirect called underneath. + testAuth.assertSignInWithRedirect([expectedProvider]); + return testAuth.process().then(function() { + testAuth.assertGetRedirectResult( + [], + null, + internalError); + return testAuth.process(); + }).then(function() { + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // Provider sign in page should remain displayed. + assertProviderSignInPage(); + // Confirm error message shown in info bar. + assertInfoBarMessage( + firebaseui.auth.widget.handler.common.getErrorMessage(internalError)); + asyncTestCase.signal(); + }); +} + + +function testFederatedSignIn_anonymousUpgrade_success_redirectMode() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + app.updateConfig('autoUpgradeAnonymousUsers', true); + externalAuth.setUser(anonymousUser); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a linkWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + externalAuth.runAuthChangeHandler(); + // Confirm linkWithRedirect called underneath. + externalAuth.currentUser.assertLinkWithRedirect([expectedProvider]); + externalAuth.process().then(function() { + asyncTestCase.signal(); + }); +} + + +function testFederatedSignIn_anonymousUpgrade_error_redirectMode() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + app.updateConfig('autoUpgradeAnonymousUsers', true); + externalAuth.setUser(anonymousUser); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a linkWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + externalAuth.runAuthChangeHandler(); + // Confirm linkWithRedirect called underneath. + externalAuth.currentUser.assertLinkWithRedirect( + [expectedProvider], null, internalError); + externalAuth.process().then(function() { + // Error in linkWithRedirect, cancel the pending redirect status. + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + asyncTestCase.signal(); + }); +} + +function testFederatedSignIn_anonymousUpgrade_success_cordova() { + simulateCordovaEnvironment(); + app.updateConfig('autoUpgradeAnonymousUsers', true); + externalAuth.setUser(anonymousUser); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a linkInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + externalAuth.runAuthChangeHandler(); + // Confirm linkWithRedirect called underneath. + externalAuth.currentUser.assertLinkWithRedirect([expectedProvider]); + return externalAuth.process().then(function() { + assertTrue( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + externalAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + externalAuth.assertGetRedirectResult( + [], + { + 'user': externalAuth.currentUser, + 'credential': cred + }); + return externalAuth.process(); + }).then(function() { + assertFalse( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertCallbackPage(); + }).then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // User should be redirected to success URL. + testUtil.assertGoTo('http://localhost/home'); + asyncTestCase.signal(); + }); +} + + +function testFederatedSignIn_anonymousUpgrade_credInUse_error_cordova() { + simulateCordovaEnvironment(); + app.updateConfig('autoUpgradeAnonymousUsers', true); + externalAuth.setUser(anonymousUser); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + cred); + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a linkInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + externalAuth.runAuthChangeHandler(); + // Confirm linkWithRedirect called underneath. + externalAuth.currentUser.assertLinkWithRedirect([expectedProvider]); + return externalAuth.process().then(function() { + assertTrue( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + externalAuth.assertGetRedirectResult( + [], + null, + expectedError); + return externalAuth.process(); + }).then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // signInFailure triggered with expected error. + assertSignInFailure(expectedMergeError); + asyncTestCase.signal(); + }); +} + + +function testFederatedSignIn_anonymousUpgrade_emailInUse_error_cordova() { + simulateCordovaEnvironment(); + app.updateConfig('autoUpgradeAnonymousUsers', true); + externalAuth.setUser(anonymousUser); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), cred); + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + assertFalse(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + // This will trigger a linkInWithRedirect using the expected provider. + firebaseui.auth.widget.handler.common.federatedSignIn( + app, component, 'google.com'); + assertTrue(firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + assertProviderSignInPage(); + externalAuth.runAuthChangeHandler(); + // Confirm linkWithRedirect called underneath. + externalAuth.currentUser.assertLinkWithRedirect([expectedProvider]); + return externalAuth.process().then(function() { + assertTrue( + firebaseui.auth.storage.hasPendingRedirectStatus(app.getAppId())); + externalAuth.assertGetRedirectResult( + [], + null, + expectedError); + return externalAuth.process(); + }).then(function() { + assertCallbackPage(); + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['facebook.com']); + return testAuth.process(); + }).then(function() { + // The pending credential should be saved here. + assertObjectEquals( + pendingEmailCred, + firebaseui.auth.storage.getPendingEmailCredential(app.getAppId())); + // Federated linking flow should be triggered. + assertFederatedLinkingPage(federatedAccount.getEmail()); + asyncTestCase.signal(); + }); +} + + function testHandleGoogleYoloCredential_handledSuccessfully_withScopes() { var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); expectedProvider.addScope('googl1'); @@ -1869,6 +2316,7 @@ function testHandleGoogleYoloCredential_cancelled_withoutScopes() { asyncTestCase.signal(); }); // Reset will cancel underlying pending promises. + app.getAuth().assertSignOut([]); app.reset(); } @@ -1902,3 +2350,300 @@ function testHandleGoogleYoloCredential_unsupportedCredential() { asyncTestCase.signal(); }); } + + +function testHandleGoogleYoloCredential_upgradeAnonymous_noScopes() { + // Enable googleyolo with Google provider and no additional scopes. + app.setConfig({ + // Set anonymous user upgrade to true. + 'autoUpgradeAnonymousUsers': true, + 'signInSuccessUrl': 'http://localhost/home', + 'signInOptions': [{ + 'provider': 'google.com', + 'authMethod': 'https://accounts.google.com', + 'clientId': '1234567890.apps.googleusercontent.com' + }, 'facebook.com', 'password', 'phone'], + 'credentialHelper': firebaseui.auth.CredentialHelper.GOOGLE_YOLO + }); + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user initially signed in on external instance. + externalAuth.setUser(anonymousUser); + // This will succeed while callback page will be rendered as the result + // gets processed before the redirect to success URL occurs. + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential( + app, component, googleYoloIdTokenCredential) + .then(function(status) { + // Renders callback page while results are processed. + assertCallbackPage(); + assertTrue(status); + asyncTestCase.signal(); + }); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Confirm linkAndRetrieveDataWithCredential called underneath with + // successful response. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + function() { + // User should be signed in. + externalAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + return { + 'user': externalAuth.currentUser, + 'credential': expectedCredential + }; + }); + // Confirm successful flow completes. + externalAuth.process().then(function() { + assertCallbackPage(); + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // User should be redirected to success URL. + testUtil.assertGoTo('http://localhost/home'); + }); +} + + +function testHandleGoogleYoloCredential_upgradeAnonymous_credentialInUse() { + // Enable googleyolo with Google provider and no additional scopes. + app.setConfig({ + // Enable anonymous user upgrade. + 'autoUpgradeAnonymousUsers': true, + 'signInSuccessUrl': 'http://localhost/home', + 'signInOptions': [{ + 'provider': 'google.com', + 'authMethod': 'https://accounts.google.com', + 'clientId': '1234567890.apps.googleusercontent.com' + }, 'facebook.com', 'password', 'phone'], + 'credentialHelper': firebaseui.auth.CredentialHelper.GOOGLE_YOLO + }); + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Expected linkAndRetrieveDataWithCredential error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': expectedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + expectedCredential); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user initially signed in on external Auth instance. + externalAuth.setUser(anonymousUser); + // This will resolve with false due to the failing Auth call underneath. + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential( + app, component, googleYoloIdTokenCredential) + .then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Confirm linkAndRetrieveDataWithCredential called underneath with + // expected error thrown. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + // Confirm signInFailure called with expected error on processing. + externalAuth.process().then(function() { + assertNoInfoBarMessage(); + assertSignInFailure(expectedMergeError); + }); +} + + +function testHandleGoogleYoloCredential_upgradeAnonymous_fedEmailInUse() { + // Enable googleyolo with Google provider and no additional scopes. + app.setConfig({ + // Enable anonymous user upgrade. + 'autoUpgradeAnonymousUsers': true, + 'signInSuccessUrl': 'http://localhost/home', + 'signInOptions': [{ + 'provider': 'google.com', + 'authMethod': 'https://accounts.google.com', + 'clientId': '1234567890.apps.googleusercontent.com' + }, 'facebook.com', 'password', 'phone'], + 'credentialHelper': firebaseui.auth.CredentialHelper.GOOGLE_YOLO + }); + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Expected linkAndRetrieveDataWithCredential error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': expectedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), + firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken, null)); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user initially signed in on external Auth instance. + externalAuth.setUser(anonymousUser); + // This will resolve with false due to the failing Auth call underneath. + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential( + app, component, googleYoloIdTokenCredential) + .then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Confirm linkAndRetrieveDataWithCredential called underneath with + // expected error thrown. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + externalAuth.process().then(function() { + // No info bar message should be shown and the callback page should be + // rendered for linking flow. + assertNoInfoBarMessage(); + assertCallbackPage(); + // Simulate existing email belongs to a Facebook account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['facebook.com']); + return testAuth.process(); + }).then(function() { + // The pending credential should be saved here. + assertObjectEquals( + pendingEmailCred, + firebaseui.auth.storage.getPendingEmailCredential(app.getAppId())); + // Federated linking flow should be triggered. + assertFederatedLinkingPage(federatedAccount.getEmail()); + }); +} + + +function testHandleGoogleYoloCredential_upgradeAnonymous_passEmailInUse() { + // Enable googleyolo with Google provider and no additional scopes. + app.setConfig({ + // Enable anonymous user upgrade. + 'autoUpgradeAnonymousUsers': true, + 'signInSuccessUrl': 'http://localhost/home', + 'signInOptions': [{ + 'provider': 'google.com', + 'authMethod': 'https://accounts.google.com', + 'clientId': '1234567890.apps.googleusercontent.com' + }, 'facebook.com', 'password', 'phone'], + 'credentialHelper': firebaseui.auth.CredentialHelper.GOOGLE_YOLO + }); + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Expected linkAndRetrieveDataWithCredential error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': expectedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), + firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken, null)); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user initially signed in on external Auth instance. + externalAuth.setUser(anonymousUser); + // This will resolve with false due to the failing Auth call underneath. + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential( + app, component, googleYoloIdTokenCredential) + .then(function(status) { + assertFalse(status); + asyncTestCase.signal(); + }); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Confirm linkAndRetrieveDataWithCredential called underneath with + // expected error thrown. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + externalAuth.process().then(function() { + // No info bar message shown and callback page rendered to complete account + // linking. + assertNoInfoBarMessage(); + assertCallbackPage(); + // Simulate email belongs to an existing password account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['password']); + return testAuth.process(); + }).then(function() { + // The pending email credential should be cleared at this point. + // Password linking does not require a redirect so no need to save it + // anyway. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // Password linking page rendered. + assertPasswordLinkingPage(federatedAccount.getEmail()); + }); +} + + +function testHandleGoogleYoloCredential_upgradeAnonymous_withScopes() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({ + 'prompt': 'select_account', + 'login_hint': federatedAccount.getEmail() + }); + // Enable googleyolo with Google provider and additional scopes. + app.setConfig({ + // Enable anonymous user upgrade. + 'autoUpgradeAnonymousUsers': true, + 'signInSuccessUrl': 'http://localhost/home', + 'signInOptions': [{ + 'provider': 'google.com', + 'scopes': ['googl1', 'googl2'], + 'customParameters': {'prompt': 'select_account'}, + 'authMethod': 'https://accounts.google.com', + 'clientId': '1234567890.apps.googleusercontent.com' + }, 'facebook.com', 'password', 'phone'], + 'credentialHelper': firebaseui.auth.CredentialHelper.GOOGLE_YOLO + }); + var component = new firebaseui.auth.ui.page.ProviderSignIn( + goog.nullFunction(), []); + component.render(container); + asyncTestCase.waitForSignals(1); + // Simulate anonymous user initially signed in on the external Auth instance. + externalAuth.setUser(anonymousUser); + // This will trigger a linkWithRedirect using the expected provider since + // additional scopes are requested. + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential( + app, component, googleYoloIdTokenCredential) + .then(function(status) { + // Remains on same page until redirect completes. + assertProviderSignInPage(); + assertTrue(status); + asyncTestCase.signal(); + }); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Confirm linkWithRedirect called on the external Auth instance user. + externalAuth.currentUser.assertLinkWithRedirect([expectedProvider]); + testAuth.process(); +} diff --git a/javascript/widgets/handler/emailmismatch_test.js b/javascript/widgets/handler/emailmismatch_test.js index 212d6b0e..391b70d1 100644 --- a/javascript/widgets/handler/emailmismatch_test.js +++ b/javascript/widgets/handler/emailmismatch_test.js @@ -135,6 +135,52 @@ function testHandleEmailMismatch_linking_continue() { } +function testHandleEmailMismatch_linking_continue_upgradeAnonymous() { + // Test handleEmailMismatch when continue button is clicked and the user was + // doing the linking flow with an eligible anonymous user available. + + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + // The credentials returned from the provider. + var credential = firebaseui.auth.idp.getAuthCredential({ + 'idToken': 'googleIdToken', + 'providerId': 'google.com' + }); + // Store pending email and pending credential. + setPendingCredentials('other@example.com'); + var currentUser = {email: federatedAccount.getEmail()}; + firebaseui.auth.widget.handler.handleEmailMismatch( + app, container, currentUser, credential); + assertEmailMismatchPage(federatedAccount.getEmail(), 'other@example.com'); + // Click continue. + submitForm(); + // Sign out from internal instance and then sign in with passed credential to + // external instance. + return testAuth.process().then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Trigger initial onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Linking the credential to continue with to the existing anonymous user. + externalAuth.currentUser.assertLinkWithCredential( + [credential], + function() { + externalAuth.setUser(testAuth.currentUser); + return externalAuth.currentUser; + }); + return externalAuth.process(); + }).then(function() { + testUtil.assertGoTo('http://localhost/home'); + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + }); +} + + function testHandleEmailMismatch_signIn_continue() { // Test handleEmailMismatch when continue button is clicked and the user was // doing the sign-in flow. @@ -195,6 +241,35 @@ function testHandleEmailMismatch_linking_cancel() { } +function testHandleEmailMismatch_linking_cancel_upgradeAnonymous() { + // Test handlEmailMismatch when cancel button is clicked and the user was + // doing the linking flow with an eligible anonymous user available. + + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + // The credentials returned from the provider. + var credential = firebaseui.auth.idp.getAuthCredential({ + 'idToken': 'googleIdToken', + 'providerId': 'google.com' + }); + // Store pending email and pending credential. + setPendingCredentials('other@example.com'); + var currentUser = {email: federatedAccount.getEmail()}; + firebaseui.auth.widget.handler.handleEmailMismatch( + app, container, currentUser, credential); + assertEmailMismatchPage(federatedAccount.getEmail(), 'other@example.com'); + // Click cancel. + clickSecondaryLink(); + // User should be redirect back to federated linking page with the originally + // intended email to sign in with. + assertFederatedLinkingPage('other@example.com'); + // Pending email credential should still be available. + assertTrue(firebaseui.auth.storage.hasPendingEmailCredential(app.getAppId())); +} + + function testHandleEmailMismatch_signIn_cancel() { // Test handlEmailMismatch when cancel button is clicked and the user was // doing the sign-in flow. diff --git a/javascript/widgets/handler/federatedlinking_test.js b/javascript/widgets/handler/federatedlinking_test.js index 93dc5e8c..e7beeabf 100644 --- a/javascript/widgets/handler/federatedlinking_test.js +++ b/javascript/widgets/handler/federatedlinking_test.js @@ -19,6 +19,7 @@ goog.provide('firebaseui.auth.widget.handler.FederatedLinkingTest'); goog.setTestOnly('firebaseui.auth.widget.handler.FederatedLinkingTest'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.PendingEmailCredential'); goog.require('firebaseui.auth.idp'); goog.require('firebaseui.auth.storage'); @@ -86,6 +87,31 @@ function testHandleFederatedLinking_noLoginHint() { } +function testHandleFederatedLinking_noLoginHint_upgradeAnonymous() { + // Add additional scopes to test they are properly passed to the sign-in + // method. + // As this is not google.com, no customParameters will be set. + var expectedProvider = + getExpectedProviderWithCustomParameters('github.com'); + // Simulate pending email credentials. + setPendingEmailCredentials(); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handleFederatedLinking( + app, container, federatedAccount.getEmail(), 'github.com'); + assertFederatedLinkingPage(federatedAccount.getEmail()); + submitForm(); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // Assert signInWithRedirect called on internal Auth instance with expected + // provider. + testAuth.assertSignInWithRedirect([expectedProvider]); + return testAuth.process(); +} + + function testHandleFederatedLinking_noLoginHint_cordova() { // Test federated linking successful flow in a Cordova environment. // Simulate a Cordova environment. @@ -166,11 +192,11 @@ function testHandleFederatedLinking_noLoginHint_error_cordova() { internalError); return testAuth.process(); }).then(function() { - // Pending credential and email should be not cleared from storage. - assertTrue(firebaseui.auth.storage.hasPendingEmailCredential( + // Pending credential and email should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( app.getAppId())); - // Federated linking page should remain displayed. - assertFederatedLinkingPage(federatedAccount.getEmail()); + // Navigate to provider sign in page and display the error in info bar. + assertProviderSignInPage(); // Confirm error message shown in info bar. assertInfoBarMessage( firebaseui.auth.widget.handler.common.getErrorMessage(internalError)); @@ -233,6 +259,89 @@ function testHandleFederatedLinking_popup_success() { } +function testHandleFederatedLinking_popup_upgradeAnonymous() { + // Test successful federated linking in popup flow when an eligible anonymous + // user is available for upgrade. + app.updateConfig('signInFlow', 'popup'); + // Add additional scopes to test they are properly passed to the sign-in + // method. + var expectedProvider = getExpectedProviderWithScopes({ + 'login_hint': federatedAccount.getEmail(), + 'prompt': 'select_account' + }); + setPendingEmailCredentials(); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handleFederatedLinking( + app, container, federatedAccount.getEmail(), 'google.com'); + assertFederatedLinkingPage(federatedAccount.getEmail()); + submitForm(); + // Existing account credential. + var cred = { + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }; + // Expected linkWithCredential error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': credential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + credential); + // Trigger initial onAuthStateChanged listener. + app.getExternalAuth().runAuthChangeHandler(); + // Sign in with popup should be called on internal Auth instance. + testAuth.assertSignInWithPopup( + [expectedProvider], + function() { + // User should be signed in. + testAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + return { + 'user': testAuth.currentUser, + 'credential': cred + }; + }); + return testAuth.process().then(function() { + // Linking should be triggered with pending credential on internal Auth + // instance user. + testAuth.currentUser.assertLinkWithCredential( + [credential], testAuth.currentUser); + return testAuth.process(); + // Sign out from internal instance and then sign in with passed credential + // to external instance. + }).then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Existing credential linking to anonymous user should fail with expected + // error. + externalAuth.currentUser.assertLinkWithCredential( + [credential], + null, + expectedError); + return externalAuth.process(); + }).then(function() { + // Pending credential and email should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // No info bar message shown. + assertNoInfoBarMessage(); + // signInFailure triggered with expected error. + assertSignInFailure(expectedMergeError); + }); +} + + function testHandleFederatedLinking_popup_success_multipleClicks() { // Test successful federated linking in popup flow when multiple clicks are // triggered. @@ -303,6 +412,7 @@ function testHandleFederatedLinking_reset() { firebaseui.auth.widget.handler.handleFederatedLinking( app, container, federatedAccount.getEmail(), 'google.com'); assertFederatedLinkingPage(federatedAccount.getEmail()); + app.getAuth().assertSignOut([]); // Reset current rendered widget page. app.reset(); // Container should be cleared. @@ -584,4 +694,3 @@ function testHandleFederatedLinking_popup_cancelled() { assertFederatedLinkingPage(); }); } - diff --git a/javascript/widgets/handler/federatedsignin_test.js b/javascript/widgets/handler/federatedsignin_test.js index 3ae92a3c..39c771c4 100644 --- a/javascript/widgets/handler/federatedsignin_test.js +++ b/javascript/widgets/handler/federatedsignin_test.js @@ -124,8 +124,8 @@ function testHandleFederatedSignIn_error_cordova() { internalError); return testAuth.process(); }).then(function() { - // Federated linking page should remain displayed. - assertFederatedLinkingPage(); + // Navigate to provider sign in page and display the error in info bar. + assertProviderSignInPage(); // Confirm error message shown in info bar. assertInfoBarMessage( firebaseui.auth.widget.handler.common.getErrorMessage(internalError)); @@ -273,6 +273,7 @@ function testHandleFederatedSignIn_reset() { firebaseui.auth.widget.handler.handleFederatedSignIn( app, container, 'user@gmail.com', 'google.com'); assertFederatedLinkingPage(); + app.getAuth().assertSignOut([]); // Reset current rendered widget page. app.reset(); // Container should be cleared. diff --git a/javascript/widgets/handler/passwordlinking.js b/javascript/widgets/handler/passwordlinking.js index a9acde48..040dc0fd 100644 --- a/javascript/widgets/handler/passwordlinking.js +++ b/javascript/widgets/handler/passwordlinking.js @@ -105,14 +105,18 @@ firebaseui.auth.widget.handler.onPasswordLinkingSubmit_ = // Tries to sign in with the email and the password entered by the user. app.registerPending(component.executePromiseRequest( /** @type {function (): !goog.Promise} */ ( - goog.bind(app.getAuth().signInWithEmailAndPassword, app.getAuth())), + goog.bind(app.signInWithExistingEmailAndPasswordForLinking, app)), [email, password], function(user) { - return app.registerPending(user.linkWithCredential(pendingCredential) + var p = user.linkWithCredential(pendingCredential) .then(function(linkedUser) { - firebaseui.auth.widget.handler.common.setLoggedIn( + // Wait for setLoggedIn promise to resolve before hiding progress + // bar. + return firebaseui.auth.widget.handler.common.setLoggedIn( app, component, pendingCredential); - })); + }); + app.registerPending(p); + return p; }, function(error) { // Ignore error if cancelled by the client. if (error['name'] && error['name'] == 'cancel') { diff --git a/javascript/widgets/handler/passwordlinking_test.js b/javascript/widgets/handler/passwordlinking_test.js index 428c397f..2491f3d2 100644 --- a/javascript/widgets/handler/passwordlinking_test.js +++ b/javascript/widgets/handler/passwordlinking_test.js @@ -19,6 +19,7 @@ goog.provide('firebaseui.auth.widget.handler.PasswordLinkingTest'); goog.setTestOnly('firebaseui.auth.widget.handler.PasswordLinkingTest'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.PendingEmailCredential'); goog.require('firebaseui.auth.idp'); goog.require('firebaseui.auth.storage'); @@ -104,6 +105,58 @@ function testHandlePasswordLinking() { } +function testHandlePasswordLinking_upgradeAnonymous() { + setPendingEmailCredentials(); + // Expected linkWithCredential error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': credential, + 'email': passwordAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + credential); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handlePasswordLinking( + app, container, passwordAccount.getEmail()); + assertPasswordLinkingPage(passwordAccount.getEmail()); + goog.dom.forms.setValue(getPasswordElement(), '123'); + submitForm(); + // Assert successful password linking flow on internal Auth instance. + assertSuccessfulPasswordLinking(credential); + // Sign out from internal instance and then sign in with passed credential to + // external instance. + return testAuth.process().then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Trigger initial onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Assert existing credential linking triggered expected error on external + // anonymous user. + externalAuth.currentUser.assertLinkWithCredential( + [credential], + null, + expectedError); + return externalAuth.process(); + }).then(function() { + // Pending credential and email should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // No info bar message shown. + assertNoInfoBarMessage(); + // signInFailure triggered with expected error. + assertSignInFailure(expectedMergeError); + }); +} + + function testHandlePasswordLinking_reset() { // Test reset after password linking handler called. setPendingEmailCredentials(); @@ -111,6 +164,7 @@ function testHandlePasswordLinking_reset() { app, container, passwordAccount.getEmail()); assertPasswordLinkingPage(passwordAccount.getEmail()); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -148,6 +202,7 @@ function testHandlePasswordLinking_signInCallback() { [credential], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // Pending credential should be cleared from storage. assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( app.getAppId())); diff --git a/javascript/widgets/handler/passwordrecovery_test.js b/javascript/widgets/handler/passwordrecovery_test.js index 3038fe25..001b4ac4 100644 --- a/javascript/widgets/handler/passwordrecovery_test.js +++ b/javascript/widgets/handler/passwordrecovery_test.js @@ -72,6 +72,7 @@ function testHandlePasswordRecovery_reset() { app, container, passwordAccount.getEmail()); assertPasswordRecoveryPage(); // Reset the current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); diff --git a/javascript/widgets/handler/passwordsignin_test.js b/javascript/widgets/handler/passwordsignin_test.js index 1c697e7b..ed274e1f 100644 --- a/javascript/widgets/handler/passwordsignin_test.js +++ b/javascript/widgets/handler/passwordsignin_test.js @@ -19,6 +19,7 @@ goog.provide('firebaseui.auth.widget.handler.PasswordSignInTest'); goog.setTestOnly('firebaseui.auth.widget.handler.PasswordSignInTest'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.widget.handler.common'); goog.require('firebaseui.auth.widget.handler.handlePasswordRecovery'); goog.require('firebaseui.auth.widget.handler.handlePasswordSignIn'); @@ -37,7 +38,8 @@ function testHandlePasswordSignIn() { goog.dom.forms.setValue(getPasswordElement(), '123'); submitForm(); testAuth.assertSignInWithEmailAndPassword( - [passwordAccount.getEmail(), '123'], function(){ + [passwordAccount.getEmail(), '123'], + function() { testAuth.setUser({ 'email': passwordAccount.getEmail(), 'displayName': passwordAccount.getDisplayName() @@ -62,11 +64,114 @@ function testHandlePasswordSignIn() { } +function testHandlePasswordSignIn_upgradeAnonymous_successfulSignIn() { + var expectedCredential = firebase.auth.EmailAuthProvider.credential( + passwordAccount.getEmail(), '123'); + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + expectedCredential); + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user externally signed in. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handlePasswordSignIn( + app, container, passwordAccount.getEmail()); + assertPasswordSignInPage(); + goog.dom.forms.setValue(getPasswordElement(), '123'); + submitForm(); + testAuth.assertSignInWithEmailAndPassword( + [passwordAccount.getEmail(), '123'], + function() { + // Set non-anonymous user on internal Auth instance. + testAuth.setUser({ + 'email': passwordAccount.getEmail(), + 'displayName': passwordAccount.getDisplayName() + }); + return testAuth.currentUser; + }); + // Sign out from internal instance and then sign in with passed credential to + // external instance. + return testAuth.process().then(function() { + externalAuth.runAuthChangeHandler(); + // signOut user on temp instance. + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // No info bar should be displayed. + assertNoInfoBarMessage(); + // UI should be disposed. + assertComponentDisposed(); + // signInFailure should be triggered with expected FirebaseUI error. + assertSignInFailure(expectedMergeError); + }); +} + + +function testHandlePasswordSignIn_upgradeAnonymous_wrongPassword() { + var error = {'code': 'auth/wrong-password'}; + var expectedCredential = firebase.auth.EmailAuthProvider.credential( + passwordAccount.getEmail(), '123'); + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + expectedCredential); + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user externally signed in. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handlePasswordSignIn( + app, container, passwordAccount.getEmail()); + assertPasswordSignInPage(); + goog.dom.forms.setValue(getPasswordElement(), '321'); + submitForm(); + // Simulate wrong password on sign-in. + testAuth.assertSignInWithEmailAndPassword( + [passwordAccount.getEmail(), '321'], + null, + error); + return testAuth.process().then(function() { + // Password sign-in page should be displayed with info bar message. + assertPasswordSignInPage(); + assertEquals( + firebaseui.auth.widget.handler.common.getErrorMessage(error), + getPasswordErrorMessage()); + // Try the correct password. + goog.dom.forms.setValue(getPasswordElement(), '123'); + submitForm(); + testAuth.assertSignInWithEmailAndPassword( + [passwordAccount.getEmail(), '123'], + function() { + // Set non-anonymous user on internal Auth instance. + testAuth.setUser({ + 'email': passwordAccount.getEmail(), + 'displayName': passwordAccount.getDisplayName() + }); + return testAuth.currentUser; + }); + return testAuth.process(); + }).then(function() { + externalAuth.runAuthChangeHandler(); + // signOut user on temp instance. + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // No info bar should be displayed. + assertNoInfoBarMessage(); + // UI should be disposed. + assertComponentDisposed(); + // signInFailure should be triggered with expected FirebaseUI error. + assertSignInFailure(expectedMergeError); + }); +} + + function testHandlePasswordSignIn_reset() { firebaseui.auth.widget.handler.handlePasswordSignIn( app, container, passwordAccount.getEmail()); assertPasswordSignInPage(); // Reset the current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); @@ -86,7 +191,8 @@ function testHandlePasswordSignIn_signInCallback() { goog.dom.forms.setValue(getPasswordElement(), '123'); submitForm(); testAuth.assertSignInWithEmailAndPassword( - [passwordAccount.getEmail(), '123'], function(){ + [passwordAccount.getEmail(), '123'], + function() { testAuth.setUser({ 'email': passwordAccount.getEmail(), 'displayName': passwordAccount.getDisplayName() @@ -106,6 +212,7 @@ function testHandlePasswordSignIn_signInCallback() { externalAuth.assertSignInWithCredential([cred], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. No password credential is passed. assertSignInSuccessCallbackInvoked( externalAuth.currentUser, null, undefined); @@ -150,7 +257,8 @@ function testHandlePasswordSignIn_wrongPassword() { goog.dom.forms.setValue(getPasswordElement(), '123'); submitForm(); testAuth.assertSignInWithEmailAndPassword( - [passwordAccount.getEmail(), '123'], function(){ + [passwordAccount.getEmail(), '123'], + function() { testAuth.setUser({ 'email': passwordAccount.getEmail(), 'displayName': passwordAccount.getDisplayName() @@ -244,7 +352,8 @@ function testHandlePasswordSignIn_inProcessing() { // Submit again. submitForm(); testAuth.assertSignInWithEmailAndPassword( - [passwordAccount.getEmail(), '123'], function(){ + [passwordAccount.getEmail(), '123'], + function() { testAuth.setUser({ 'email': passwordAccount.getEmail(), 'displayName': passwordAccount.getDisplayName() diff --git a/javascript/widgets/handler/passwordsignup.js b/javascript/widgets/handler/passwordsignup.js index 3248d8cb..7b42d549 100644 --- a/javascript/widgets/handler/passwordsignup.js +++ b/javascript/widgets/handler/passwordsignup.js @@ -26,6 +26,7 @@ goog.require('firebaseui.auth.widget.Handler'); goog.require('firebaseui.auth.widget.HandlerName'); goog.require('firebaseui.auth.widget.handler'); goog.require('firebaseui.auth.widget.handler.common'); +goog.require('goog.string'); /** @@ -85,9 +86,13 @@ firebaseui.auth.widget.handler.onSignUpSubmit_ = function(app, component) { component.getEmailElement().focus(); return; } - if (requireDisplayName && !name) { - component.getNameElement().focus(); - return; + if (requireDisplayName) { + if (name) { + name = goog.string.htmlEscape(name); + } else { + component.getNameElement().focus(); + return; + } } if (!password) { component.getNewPasswordElement().focus(); @@ -101,19 +106,21 @@ firebaseui.auth.widget.handler.onSignUpSubmit_ = function(app, component) { // Sign up new account. app.registerPending(component.executePromiseRequest( /** @type {function (): !goog.Promise} */ ( - goog.bind(app.getAuth().createUserWithEmailAndPassword, app.getAuth()) + goog.bind(app.startCreateUserWithEmailAndPassword, app) ), [email, password], function(user) { if (requireDisplayName) { // Sign up successful. We can now set the name. - return app.registerPending(user.updateProfile({'displayName': name}) + var p = user.updateProfile({'displayName': name}) .then(function() { // Pass password credential to complete the sign-in to original // auth instance. - firebaseui.auth.widget.handler.common.setLoggedIn( + return firebaseui.auth.widget.handler.common.setLoggedIn( app, component, emailPassCred); - })); + }); + app.registerPending(p); + return p; } else { return firebaseui.auth.widget.handler.common.setLoggedIn( app, component, emailPassCred); diff --git a/javascript/widgets/handler/passwordsignup_test.js b/javascript/widgets/handler/passwordsignup_test.js index 237a8bb6..5d766cec 100644 --- a/javascript/widgets/handler/passwordsignup_test.js +++ b/javascript/widgets/handler/passwordsignup_test.js @@ -20,6 +20,7 @@ goog.provide('firebaseui.auth.widget.handler.PasswordSignUpTest'); goog.setTestOnly('firebaseui.auth.widget.handler.PasswordSignUpTest'); goog.require('firebaseui.auth.soy2.strings'); +goog.require('firebaseui.auth.widget.handler.common'); goog.require('firebaseui.auth.widget.handler.handlePasswordSignUp'); goog.require('firebaseui.auth.widget.handler.handleProviderSignIn'); goog.require('firebaseui.auth.widget.handler.handleSignIn'); @@ -77,18 +78,124 @@ function testHandlePasswordSignUp() { } +function testHandlePasswordSignUp_anonymousUpgrade_success() { + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous current user on external Auth instance. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handlePasswordSignUp( + app, container, passwordAccount.getEmail()); + assertPasswordSignUpPage(); + goog.dom.forms.setValue(getNameElement(), 'Password User'); + goog.dom.forms.setValue(getNewPasswordElement(), '123123'); + submitForm(); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + var cred = new firebase.auth.EmailAuthProvider.credential( + passwordAccount.getEmail(), '123123'); + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [cred], + function() { + // User should be signed in. + externalAuth.setUser({ + 'uid': '12345678' + }); + return { + 'user': externalAuth.currentUser, + 'credential': null + }; + }); + return externalAuth.process().then(function() { + externalAuth.currentUser.assertUpdateProfile([{ + 'displayName': 'Password User' + }]); + return externalAuth.process(); + }).then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + testUtil.assertGoTo('http://localhost/home'); + }); +} + + +function testHandlePasswordSignUp_anonymousUpgrade_emailInUse() { + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous current user on external Auth instance. + externalAuth.setUser(anonymousUser); + firebaseui.auth.widget.handler.handlePasswordSignUp( + app, container, passwordAccount.getEmail()); + assertPasswordSignUpPage(); + goog.dom.forms.setValue(getNameElement(), 'Password User'); + goog.dom.forms.setValue(getNewPasswordElement(), '123123'); + submitForm(); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + var cred = new firebase.auth.EmailAuthProvider.credential( + passwordAccount.getEmail(), '123123'); + var error = { + 'code': 'auth/email-already-in-use' + }; + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [cred], null, error); + return externalAuth.process().then(function() { + testAuth.assertFetchProvidersForEmail( + [passwordAccount.getEmail()], ['password']); + return testAuth.process(); + }).then(function() { + assertPasswordSignUpPage(); + assertEquals( + firebaseui.auth.widget.handler.common.getErrorMessage(error), + getEmailErrorMessage()); + }); +} + + function testHandlePasswordSignUp_reset() { // Test reset after password sign-up handler called. firebaseui.auth.widget.handler.handlePasswordSignUp( app, container, passwordAccount.getEmail()); assertPasswordSignUpPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); } +function testHandlePasswordSignUp_escapeDisplayName() { + firebaseui.auth.widget.handler.handlePasswordSignUp( + app, container, passwordAccount.getEmail()); + assertPasswordSignUpPage(); + goog.dom.forms.setValue(getNameElement(), ''); + goog.dom.forms.setValue(getNewPasswordElement(), '123123'); + submitForm(); + testAuth.assertCreateUserWithEmailAndPassword( + [passwordAccount.getEmail(), '123123'], function() { + testAuth.setUser({ + 'email': passwordAccount.getEmail() + }); + // Display name should be sanitized. + testAuth.currentUser.assertUpdateProfile([{ + 'displayName': '<script>doSthBad();</script>' + }]); + return testAuth.currentUser; + }); + return testAuth.process().then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + externalAuth.setUser(testAuth.currentUser); + var cred = new firebase.auth.EmailAuthProvider.credential( + passwordAccount.getEmail(), '123123'); + externalAuth.assertSignInWithCredential([cred], externalAuth.currentUser); + return externalAuth.process(); + }).then(function() { + testUtil.assertGoTo('http://localhost/home'); + }); +} + + function testHandlePasswordSignUp_withoutDisplayName() { app.setConfig({ 'signInOptions': [ @@ -157,6 +264,7 @@ function testHandlePasswordSignUp_signInCallback() { externalAuth.assertSignInWithCredential([cred], externalAuth.currentUser); return externalAuth.process(); }).then(function() { + testAuth.assertSignOut([]); // SignInCallback is called. No password credential is passed. assertSignInSuccessCallbackInvoked( externalAuth.currentUser, null, undefined); diff --git a/javascript/widgets/handler/phonesigninfinish.js b/javascript/widgets/handler/phonesigninfinish.js index f812f9dd..39f2fe59 100644 --- a/javascript/widgets/handler/phonesigninfinish.js +++ b/javascript/widgets/handler/phonesigninfinish.js @@ -42,12 +42,12 @@ goog.require('firebaseui.auth.widget.handler.common'); * @param {!firebaseui.auth.PhoneNumber} phoneNumberValue * The value of the phone number input. * @param {!number} resendDelay The resend delay. - * @param {!Object} confirmationResult The confirmation result used to verify + * @param {!Object} phoneAuthResult The phone Auth result used to verify * the code on. * @param {string=} opt_infoBarMessage The message to show on info bar. */ firebaseui.auth.widget.handler.handlePhoneSignInFinish = function( - app, container, phoneNumberValue, resendDelay, confirmationResult, + app, container, phoneNumberValue, resendDelay, phoneAuthResult, opt_infoBarMessage) { // This is a placeholder for now. // Render the phone sign in start page component. @@ -63,7 +63,7 @@ firebaseui.auth.widget.handler.handlePhoneSignInFinish = function( // On submit. function() { firebaseui.auth.widget.handler.onPhoneSignInFinishSubmit_( - app, component, phoneNumberValue, confirmationResult); + app, component, phoneNumberValue, phoneAuthResult); }, // On cancel. function() { @@ -105,12 +105,12 @@ firebaseui.auth.widget.handler.CODE_SUCCESS_DIALOG_DELAY = 1000; * component. * @param {!firebaseui.auth.PhoneNumber} phoneNumberValue * The value of the phone number input. - * @param {!Object} confirmationResult The confirmation result used to verify + * @param {!Object} phoneAuthResult The phone Auth result used to verify * the code on. * @private */ firebaseui.auth.widget.handler.onPhoneSignInFinishSubmit_ = function( - app, component, phoneNumberValue, confirmationResult) { + app, component, phoneNumberValue, phoneAuthResult) { var showInvalidCode = function(errorMessage) { // No code provided. component.getPhoneConfirmationCodeElement().focus(); @@ -134,7 +134,7 @@ firebaseui.auth.widget.handler.onPhoneSignInFinishSubmit_ = function( firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); app.registerPending(component.executePromiseRequest( /** @type {function (): !goog.Promise} */ ( - goog.bind(confirmationResult['confirm'], confirmationResult)), + goog.bind(phoneAuthResult['confirm'], phoneAuthResult)), [verificationCode], // On success a user credential is returned. function(userCredential) { @@ -163,9 +163,9 @@ firebaseui.auth.widget.handler.onPhoneSignInFinishSubmit_ = function( }, // On code verification failure. function(error) { - // Close dialog. - component.dismissDialog(); if (error['name'] && error['name'] == 'cancel') { + // Close dialog. + component.dismissDialog(); return; } // Get error message. @@ -173,11 +173,18 @@ firebaseui.auth.widget.handler.onPhoneSignInFinishSubmit_ = function( firebaseui.auth.widget.handler.common.getErrorMessage(error); // Some errors are recoverable while others require resending the code. switch (error['code']) { + case 'auth/credential-already-in-use': + // Do nothing when anonymous user is getting upgraded. + // Developer should handle this in signInFailure callback. + component.dismissDialog(); + break; case 'auth/code-expired': // Expired code requires sending another request. // Render previous phone sign in start page and display error in // the info bar. var container = component.getContainer(); + // Close dialog. + component.dismissDialog(); component.dispose(); firebaseui.auth.widget.handler.handle( firebaseui.auth.widget.HandlerName.PHONE_SIGN_IN_START, app, @@ -185,11 +192,15 @@ firebaseui.auth.widget.handler.onPhoneSignInFinishSubmit_ = function( break; case 'auth/missing-verification-code': case 'auth/invalid-verification-code': + // Close dialog. + component.dismissDialog(); // As these errors are related to the code provided, it is better // to display inline. showInvalidCode(errorMessage); break; default: + // Close dialog. + component.dismissDialog(); // Stay on the same page for all other errors and display error in // info bar. component.showInfoBar(errorMessage); diff --git a/javascript/widgets/handler/phonesigninfinish_test.js b/javascript/widgets/handler/phonesigninfinish_test.js index 6b6de981..18f1c34a 100644 --- a/javascript/widgets/handler/phonesigninfinish_test.js +++ b/javascript/widgets/handler/phonesigninfinish_test.js @@ -21,6 +21,7 @@ goog.provide('firebaseui.auth.widget.handler.PhoneSignInFinishTest'); goog.setTestOnly('firebaseui.auth.widget.handler.PhoneSignInFinishTest'); +goog.require('firebaseui.auth.PhoneAuthResult'); goog.require('firebaseui.auth.PhoneNumber'); goog.require('firebaseui.auth.soy2.strings'); goog.require('firebaseui.auth.widget.handler.common'); @@ -33,6 +34,7 @@ goog.require('firebaseui.auth.widget.handler.handleProviderSignIn'); goog.require('firebaseui.auth.widget.handler.testHelper'); goog.require('goog.Promise'); goog.require('goog.dom.forms'); +goog.require('goog.testing.recordFunction'); @@ -100,6 +102,149 @@ function testHandlePhoneSignInFinish_success_signInSuccessUrl() { } +function testHandlePhoneSignInFinish_anonymousUpgrade_success() { + externalAuth.setUser(anonymousUser); + app.updateConfig('autoUpgradeAnonymousUsers', true); + var mockPhoneAuthResult = { + 'confirm': function(code) { + assertEquals('123456', code); + return goog.Promise.resolve({ + 'user': {'uid': '1234567890'}, + 'credential': null, + 'operationType': 'link' + }); + } + }; + firebaseui.auth.widget.handler.handlePhoneSignInFinish( + app, container, phoneNumberValue, resendDelaySeconds, + mockPhoneAuthResult); + // Confirm expected page rendered. + assertPhoneSignInFinishPage(); + // Try to submit form without code being provided. + submitForm(); + // Inline error message shown. + assertEquals( + firebaseui.auth.soy2.strings.errorInvalidConfirmationCode().toString(), + getPhoneConfirmationCodeErrorMessage()); + // Simulate code provided. + goog.dom.forms.setValue(getPhoneConfirmationCodeElement(), '123456'); + // Submit form. + submitForm(); + // Loading dialog shown. + assertDialog( + firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); + return goog.Promise.resolve().then(function() { + // Code verified dialog shown on success. + assertDialog(firebaseui.auth.soy2.strings.dialogCodeVerified().toString()); + // Wait for one second. Sign in success URL redirect should occur after. + mockClock.tick(firebaseui.auth.widget.handler.CODE_SUCCESS_DIALOG_DELAY); + // No dialog shown. + assertNoDialog(); + // Successful sign in. + testUtil.assertGoTo('http://localhost/home'); + }); +} + + +function testHandlePhoneSignInFinish_anonymousUpgrade_credentialInUseError() { + externalAuth.setUser(anonymousUser); + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = { + 'providerId': 'phone', + 'verificationId': '123456abc', + 'verificationCode': '123456' + }; + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'phoneNumber': '+11234567890', + 'credential': cred + }; + var mockConfirmationResult = { + 'confirm': function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + } + }; + var errorHandler = goog.testing.recordFunction(function(error) { + assertEquals(expectedError, error); + throw error; + }); + var phoneAuthResult = new firebaseui.auth.PhoneAuthResult( + mockConfirmationResult, errorHandler); + firebaseui.auth.widget.handler.handlePhoneSignInFinish( + app, container, phoneNumberValue, resendDelaySeconds, + phoneAuthResult); + // Confirm expected page rendered. + assertPhoneSignInFinishPage(); + // Try to submit form without code being provided. + submitForm(); + // Inline error message shown. + assertEquals( + firebaseui.auth.soy2.strings.errorInvalidConfirmationCode().toString(), + getPhoneConfirmationCodeErrorMessage()); + // Simulate code provided. + goog.dom.forms.setValue(getPhoneConfirmationCodeElement(), '123456'); + // Submit form. + submitForm(); + // Loading dialog shown. + assertDialog( + firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); + return goog.Promise.resolve().then(function() { + // No info bar message. + assertNoInfoBarMessage(); + // Verifies that error handler got called. + assertEquals(1, errorHandler.getCallCount()); + }); +} + + +function testHandlePhoneSignInFinish_anonymousUpgrade_invalidCodeError() { + externalAuth.setUser(anonymousUser); + app.updateConfig('autoUpgradeAnonymousUsers', true); + var expectedError = { + 'code': 'auth/invalid-verification-code', + 'message': 'MESSAGE' + }; + var mockConfirmationResult = { + 'confirm': function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + } + }; + var errorHandler = goog.testing.recordFunction(function(error) { + assertEquals(expectedError, error); + throw error; + }); + var phoneAuthResult = new firebaseui.auth.PhoneAuthResult( + mockConfirmationResult, errorHandler); + firebaseui.auth.widget.handler.handlePhoneSignInFinish( + app, container, phoneNumberValue, resendDelaySeconds, + phoneAuthResult); + // Confirm expected page rendered. + assertPhoneSignInFinishPage(); + // Try to submit form without code being provided. + submitForm(); + // Inline error message shown. + assertEquals( + firebaseui.auth.soy2.strings.errorInvalidConfirmationCode().toString(), + getPhoneConfirmationCodeErrorMessage()); + // Simulate code provided. + goog.dom.forms.setValue(getPhoneConfirmationCodeElement(), '123456'); + // Submit form. + submitForm(); + // Loading dialog shown. + assertDialog( + firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); + return goog.Promise.resolve().then(function() { + // No info bar message. + assertNoInfoBarMessage(); + // Verifies that error handler got called. + assertEquals(1, errorHandler.getCallCount()); + }); +} + + function testHandlePhoneSignInFinish_success_signInSuccessCallback() { // Test successful code entry with signInSuccess callback provided. // Provide a sign in success callback. @@ -129,6 +274,7 @@ function testHandlePhoneSignInFinish_success_signInSuccessCallback() { // is logged in. assertSignInSuccessCallbackInvoked( externalAuth.currentUser, null, undefined); + app.getAuth().assertSignOut([]); // Container should be cleared. assertComponentDisposed(); }); @@ -155,6 +301,7 @@ function testHandlePhoneSignInFinish_success_resetBeforeCompletion() { // Code verified dialog shown on success. assertDialog(firebaseui.auth.soy2.strings.dialogCodeVerified().toString()); // Simulate app reset. + app.getAuth().assertSignOut([]); app.reset(); // Dialog should be dismissed, even though no time passed. assertNoDialog(); @@ -333,6 +480,7 @@ function testHandlePhoneSignInFinishStart_reset() { // Confirm expected page rendered. assertPhoneSignInFinishPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); diff --git a/javascript/widgets/handler/phonesigninstart.js b/javascript/widgets/handler/phonesigninstart.js index 4ad080a4..6f673631 100644 --- a/javascript/widgets/handler/phonesigninstart.js +++ b/javascript/widgets/handler/phonesigninstart.js @@ -236,12 +236,11 @@ firebaseui.auth.widget.handler.onPhoneSignInStartSubmit_ = // verifier. app.registerPending(component.executePromiseRequest( /** @type {function (): !goog.Promise} */ ( - goog.bind(app.getExternalAuth().signInWithPhoneNumber, - app.getExternalAuth()) + goog.bind(app.startSignInWithPhoneNumber, app) ), [phoneNumberValue.getPhoneNumber(), recaptchaVerifier], - // On success a confirmation result is returned. - function(confirmationResult) { + // On success a phone Auth result is returned. + function(phoneAuthResult) { // Display the dialog that the code was sent. var container = component.getContainer(); component.showProgressDialog( @@ -259,7 +258,7 @@ firebaseui.auth.widget.handler.onPhoneSignInStartSubmit_ = container, phoneNumberValue, firebaseui.auth.widget.handler.RESEND_DELAY_SECONDS, - confirmationResult); + phoneAuthResult); }, firebaseui.auth.widget.handler.SENDING_SUCCESS_DIALOG_DELAY); // On reset, clear timeout. app.registerPending(function() { diff --git a/javascript/widgets/handler/phonesigninstart_test.js b/javascript/widgets/handler/phonesigninstart_test.js index df76c4af..4bd0e9da 100644 --- a/javascript/widgets/handler/phonesigninstart_test.js +++ b/javascript/widgets/handler/phonesigninstart_test.js @@ -19,6 +19,7 @@ goog.provide('firebaseui.auth.widget.handler.PhoneSignInStartTest'); goog.setTestOnly('firebaseui.auth.widget.handler.PhoneSignInStartTest'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.PhoneNumber'); goog.require('firebaseui.auth.soy2.strings'); goog.require('firebaseui.auth.widget.handler.common'); @@ -136,6 +137,283 @@ function testHandlePhoneSignInStart_visible() { } +function testHandlePhoneSignInStart_anonymousUpgrade_success() { + externalAuth.setUser(anonymousUser); + // Enable visible reCAPTCHA and auto anonymous upgrade. + app.setConfig({ + 'signInOptions': [ + { + provider: 'password', + }, + { + provider: 'phone', + recaptchaParameters: {'type': 'image', 'size': 'compact'} + } + ], + 'autoUpgradeAnonymousUsers': true + }); + // Render phone sign in start UI. + firebaseui.auth.widget.handler.handlePhoneSignInStart( + app, container); + // Confirm expected page rendered. + assertPhoneSignInStartPage(); + // Confirm reCAPTCHA initialized with expected parameters. + recaptchaVerifierInstance.assertInitializedWithParameters( + getRecaptchaElement(), + {'type': 'image', 'size': 'compact'}, + app.getExternalAuth().app); + // reCAPTCHA should be rendering. + recaptchaVerifierInstance.assertRender([], function() { + // Simulate grecaptcha loaded. + simulateGrecaptchaLoaded(0); + // Return expected widget ID. + return 0; + }); + return recaptchaVerifierInstance.process().then(function() { + // Recaptcha rendered at this point. + // Try first without phone number. + submitForm(); + // Error should be shown that the phone number is missing. + assertEquals( + firebaseui.auth.soy2.strings.errorInvalidPhoneNumber().toString(), + getPhoneNumberErrorMessage()); + // Simulate phone number inputted. + goog.dom.forms.setValue(getPhoneNumberElement(), '1234567890'); + // Submit without solving reCAPTCHA. + submitForm(); + // reCAPTCHA error should show. + // Error should be shown that the reCAPTCHA response is missing. + assertEquals( + firebaseui.auth.soy2.strings.errorMissingRecaptchaResponse() + .toString(), + getRecaptchaErrorMessage()); + // Simulate reCAPTCHA solved. + var callback = recaptchaVerifierInstance.getParameters()['callback']; + callback('RECAPTCHA_TOKEN'); + // Submit again. This time, it wills succeed. + submitForm(); + // Loading dialog shown. + assertDialog( + firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Link with phone number triggered. + externalAuth.currentUser.assertLinkWithPhoneNumber( + ['+11234567890', recaptchaVerifierInstance], + mockConfirmationResult); + return externalAuth.process(); + }).then(function() { + // No grecaptcha reset called. + assertEquals(0, goog.global['grecaptcha'].reset.getCallCount()); + // Code sent dialog shown on success. + assertDialog(firebaseui.auth.soy2.strings.dialogCodeSent().toString()); + // Wait for one second. Confirm code entry page rendered. + mockClock.tick(firebaseui.auth.widget.handler.SENDING_SUCCESS_DIALOG_DELAY); + assertNoDialog(); + assertPhoneSignInFinishPage(); + // Assert countdown matches delay param. + assertResendCountdown('0:' + + firebaseui.auth.widget.handler.RESEND_DELAY_SECONDS); + // Simulate correct code provided. + goog.dom.forms.setValue(getPhoneConfirmationCodeElement(), '123456'); + // Submit form. + submitForm(); + // Give enough time for code to process. + return goog.Promise.resolve(); + }).then(function() { + // Code verified dialog shown on success. + assertDialog(firebaseui.auth.soy2.strings.dialogCodeVerified().toString()); + // Wait for one second. Sign in success URL redirect should occur after. + mockClock.tick(firebaseui.auth.widget.handler.CODE_SUCCESS_DIALOG_DELAY); + assertNoDialog(); + // Successful sign in. + testUtil.assertGoTo('http://localhost/home'); + }); +} + + +function testHandlePhoneSignInStart_anonymousUpgrade_credInUseError() { + externalAuth.setUser(anonymousUser); + // Enable visible reCAPTCHA and auto anonymous upgrade. + app.setConfig({ + 'signInOptions': [ + { + provider: 'password', + }, + { + provider: 'phone', + recaptchaParameters: {'type': 'image', 'size': 'compact'} + } + ], + 'autoUpgradeAnonymousUsers': true + }); + var cred = { + 'providerId': 'phone', + 'verificationId': '123456abc', + 'verificationCode': '123456' + }; + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'message': 'MESSAGE', + 'phoneNumber': '+11234567890', + 'credential': cred + }; + // Mock confirmation result which throws credential in use error. + var mockConfirmationResult = { + 'confirm': function(code) { + assertEquals('123456', code); + return goog.Promise.reject(expectedError); + } + }; + // Expected signInFailure FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + cred); + // Render phone sign in start UI. + firebaseui.auth.widget.handler.handlePhoneSignInStart( + app, container); + // Confirm expected page rendered. + assertPhoneSignInStartPage(); + // Confirm reCAPTCHA initialized with expected parameters. + recaptchaVerifierInstance.assertInitializedWithParameters( + getRecaptchaElement(), + {'type': 'image', 'size': 'compact'}, + app.getExternalAuth().app); + // reCAPTCHA should be rendering. + recaptchaVerifierInstance.assertRender([], function() { + // Simulate grecaptcha loaded. + simulateGrecaptchaLoaded(0); + // Return expected widget ID. + return 0; + }); + return recaptchaVerifierInstance.process().then(function() { + // Recaptcha rendered at this point. + // Try first without phone number. + submitForm(); + // Error should be shown that the phone number is missing. + assertEquals( + firebaseui.auth.soy2.strings.errorInvalidPhoneNumber().toString(), + getPhoneNumberErrorMessage()); + // Simulate phone number inputted. + goog.dom.forms.setValue(getPhoneNumberElement(), '1234567890'); + // Submit without solving reCAPTCHA. + submitForm(); + // reCAPTCHA error should show. + // Error should be shown that the reCAPTCHA response is missing. + assertEquals( + firebaseui.auth.soy2.strings.errorMissingRecaptchaResponse() + .toString(), + getRecaptchaErrorMessage()); + // Simulate reCAPTCHA solved. + var callback = recaptchaVerifierInstance.getParameters()['callback']; + callback('RECAPTCHA_TOKEN'); + // Submit again. This time, it wills succeed. + submitForm(); + // Loading dialog shown. + assertDialog( + firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Link with phone number triggered. + externalAuth.currentUser.assertLinkWithPhoneNumber( + ['+11234567890', recaptchaVerifierInstance], + mockConfirmationResult); + return externalAuth.process(); + }).then(function() { + // No grecaptcha reset called. + assertEquals(0, goog.global['grecaptcha'].reset.getCallCount()); + // Code sent dialog shown on success. + assertDialog(firebaseui.auth.soy2.strings.dialogCodeSent().toString()); + // Wait for one second. Confirm code entry page rendered. + mockClock.tick(firebaseui.auth.widget.handler.SENDING_SUCCESS_DIALOG_DELAY); + assertNoDialog(); + assertPhoneSignInFinishPage(); + // Assert countdown matches delay param. + assertResendCountdown('0:' + + firebaseui.auth.widget.handler.RESEND_DELAY_SECONDS); + // Simulate correct code provided. + goog.dom.forms.setValue(getPhoneConfirmationCodeElement(), '123456'); + // Submit form. + submitForm(); + // Give enough time for code to process. + return goog.Promise.resolve(); + }).then(function() { + // No info bar message. + assertNoInfoBarMessage(); + // signInFailure callback triggered with expected FirebaseUI error. + assertSignInFailure(expectedMergeError); + }); +} + + +function testHandlePhoneSignInStart_anonymousUpgrade_signInError() { + externalAuth.setUser(anonymousUser); + // Enable visible reCAPTCHA and auto anonymous upgrade. + app.setConfig({ + 'signInOptions': [ + { + provider: 'password', + }, + { + provider: 'phone', + recaptchaParameters: {'type': 'image', 'size': 'compact'} + } + ], + 'autoUpgradeAnonymousUsers': true + }); + // Render phone sign in start UI. + firebaseui.auth.widget.handler.handlePhoneSignInStart( + app, container); + // Confirm expected page rendered. + assertPhoneSignInStartPage(); + // Confirm reCAPTCHA initialized with expected parameters. + recaptchaVerifierInstance.assertInitializedWithParameters( + getRecaptchaElement(), + {'type': 'image', 'size': 'compact'}, + app.getExternalAuth().app); + // reCAPTCHA should be rendering. + recaptchaVerifierInstance.assertRender([], function() { + // Simulate grecaptcha loaded. + simulateGrecaptchaLoaded(5); + // Return expected widget ID. + return 5; + }); + return recaptchaVerifierInstance.process().then(function() { + // Recaptcha rendered at this point. + // Simulate phone number inputted. + goog.dom.forms.setValue(getPhoneNumberElement(), '1234567890'); + // Simulate reCAPTCHA solved. + var callback = recaptchaVerifierInstance.getParameters()['callback']; + callback('RECAPTCHA_TOKEN'); + submitForm(); + // Loading dialog shown. + assertDialog( + firebaseui.auth.soy2.strings.dialogVerifyingPhoneNumber().toString()); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Link with phone number triggered. Simulate an error thrown. + externalAuth.currentUser.assertLinkWithPhoneNumber( + ['+11234567890', recaptchaVerifierInstance], + null, + internalError); + return externalAuth.process(); + }).then(function() { + // reCAPTCHA should be reset as the token has already been used. + assertEquals(1, goog.global['grecaptcha'].reset.getCallCount()); + // Reset should be called with the expected widget ID. + assertEquals( + 5, goog.global['grecaptcha'].reset.getLastCall().getArgument(0)); + // No dialog shown. + assertNoDialog(); + // Should remain on the page and display the expected error. + assertPhoneSignInStartPage(); + assertInfoBarMessage( + firebaseui.auth.widget.handler.common.getErrorMessage(internalError)); + }); +} + + function testHandlePhoneSignInStart_resetBeforeCodeEntry() { // Tests successful visible reCAPTCHA flow and reset before code entry page is // rendered. @@ -191,6 +469,7 @@ function testHandlePhoneSignInStart_resetBeforeCodeEntry() { // Code sent dialog shown on success. assertDialog(firebaseui.auth.soy2.strings.dialogCodeSent().toString()); // Simulate app reset. + app.getAuth().assertSignOut([]); app.reset(); // Code entry page should not get rendered after delay. // Wait for one second. Confirm code entry page rendered. @@ -933,6 +1212,7 @@ function testHandlePhoneSignInStart_reset() { // This is called and will be cancelled. recaptchaVerifierInstance.assertRender([], 0); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); diff --git a/javascript/widgets/handler/providersignin_test.js b/javascript/widgets/handler/providersignin_test.js index f439dcef..b1507ae6 100644 --- a/javascript/widgets/handler/providersignin_test.js +++ b/javascript/widgets/handler/providersignin_test.js @@ -20,6 +20,7 @@ goog.provide('firebaseui.auth.widget.handler.ProviderSignInTest'); goog.setTestOnly('firebaseui.auth.widget.handler.ProviderSignInTest'); goog.require('firebaseui.auth.AuthUI'); +goog.require('firebaseui.auth.AuthUIError'); goog.require('firebaseui.auth.CredentialHelper'); goog.require('firebaseui.auth.PendingEmailCredential'); goog.require('firebaseui.auth.acClient'); @@ -163,6 +164,51 @@ function testHandleProviderSignIn_oneTap_handledSuccessfully_withScopes() { } +function testHandleProviderSignIn_oneTap_anonymousUpgrade_withScopes() { + // The expected Firebase Auth provider to linkWithRedirect with. + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({ + 'prompt': 'select_account', + 'login_hint': 'user@example.com' + }); + // Render the provider sign-in page with additional scopes and googleyolo + // enabled and confirm it was rendered correctly. + setupProviderSignInPage('redirect', false, true); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user initially signed in on the external Auth instance. + externalAuth.setUser(anonymousUser); + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.cancelOneTapSignIn.getCallCount()); + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getCallCount()); + // Get the One-Tap credential handler. + var handler = firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getLastCall() + .getArgument(0); + // Confirm expected handler. + assertEquals( + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential, + handler); + // Simulate successful credential provided by One-Tap. + var p = handler(app, app.getCurrentComponent(), googleYoloIdTokenCredential) + .then(function(status) { + assertTrue(status); + }); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkWithRedirect should be called with the expected provider. + externalAuth.currentUser.assertLinkWithRedirect([expectedProvider]); + externalAuth.process().then(function() { + // Any pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + }); + return p; +} + + function testHandleProviderSignIn_oneTap_unhandled_withoutScopes() { // Render the provider sign-in page with no additional scopes and googleyolo // enabled and confirm it was rendered correctly. @@ -317,6 +363,281 @@ function testHandleProviderSignIn_oneTap_handledSuccessfully_withoutScopes() { } +function testHandleProviderSignIn_oneTap_upgradeAnonymous_withoutScopes() { + // Render the provider sign-in page with no additional scopes and googleyolo + // enabled and confirm it was rendered correctly. + setupProviderSignInPage('redirect', false, true, true); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user initially signed in on external Auth instance. + externalAuth.setUser(anonymousUser); + var expectedUser = { + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }; + // Confirm provider sign in page rendered. + assertProviderSignInPage(); + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.cancelOneTapSignIn.getCallCount()); + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getCallCount()); + // Get googleyolo credential handler. + var handler = firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getLastCall() + .getArgument(0); + // Confirm expected handler. + assertEquals( + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential, + handler); + // Simulate successful credential provided by One-Tap. + var expectedHandlerStatus = false; + handler(app, app.getCurrentComponent(), googleYoloIdTokenCredential) + .then(function(status) { + expectedHandlerStatus = status; + }); + // Since no additional scopes are requested, + // linkAndRetrieveDataWithCredential should be called to handle the + // ID token returned by googleyolo. + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential should be called with the expected + // credential and simulate a successful sign in operation. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + function() { + // Simulate non-anonymous user successfully signed in. + externalAuth.setUser(expectedUser); + return { + 'user': expectedUser, + 'credential': expectedCredential + }; + }); + return externalAuth.process().then(function() { + // Callback page should be rendered while the result is being processed. + assertCallbackPage(); + // signOut should be called on the internal Auth instance. + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Confirm googleyolo handler successful. + assertTrue(expectedHandlerStatus); + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // User should be redirected to success URL. + testUtil.assertGoTo('http://localhost/home'); + }); +} + + +function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_credInUse() { + // Render the provider sign-in page with no additional scopes and googleyolo + // enabled and confirm it was rendered correctly. + setupProviderSignInPage('redirect', false, true, true); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user initially signed in on the external Auth instance. + externalAuth.setUser(anonymousUser); + // Confirm provider sign in page rendered. + assertProviderSignInPage(); + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.cancelOneTapSignIn.getCallCount()); + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getCallCount()); + // Get googleyolo credential handler. + var handler = firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getLastCall() + .getArgument(0); + // Confirm expected handler. + assertEquals( + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential, + handler); + // Simulate successful credential provided by One-Tap. + var expectedHandlerStatus = false; + handler(app, app.getCurrentComponent(), googleYoloIdTokenCredential) + .then(function(status) { + expectedHandlerStatus = status; + }); + // Since no additional scopes are requested, + // linkAndRetrieveDataWithCredential should be called to handle the + // ID token returned by googleyolo. + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Expected linkAndRetrieveDataWithCredential error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': expectedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + expectedCredential); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential should be called with the expected + // credential and simulate the expected error thrown. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + return externalAuth.process().then(function() { + // No info bar message shown. + assertNoInfoBarMessage(); + // signInFailure triggered with expected error. + assertSignInFailure(expectedMergeError); + // googleyolo handler should have resolved with false status. + assertFalse(expectedHandlerStatus); + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + }); +} + + +function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_fedEmailInUse() { + // Render the provider sign-in page with no additional scopes and googleyolo + // enabled and confirm it was rendered correctly. + setupProviderSignInPage('redirect', false, true, true); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user initially signed in on external Auth instance. + externalAuth.setUser(anonymousUser); + // Confirm provider sign in page rendered. + assertProviderSignInPage(); + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.cancelOneTapSignIn.getCallCount()); + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getCallCount()); + // Get googleyolo credential handler. + var handler = firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getLastCall() + .getArgument(0); + // Confirm expected handler. + assertEquals( + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential, + handler); + // Simulate successful credential provided by One-Tap. + var expectedHandlerStatus = false; + handler(app, app.getCurrentComponent(), googleYoloIdTokenCredential) + .then(function(status) { + expectedHandlerStatus = status; + }); + // Since no additional scopes are requested, + // linkAndRetrieveDataWithCredential should be called to handle the + // ID token returned by googleyolo. + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Expected linkWithRedirect error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': expectedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), + firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken, null)); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential should be called with the expected + // credential and simulate an email already in use error. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + return externalAuth.process().then(function() { + // Callback page should be rendered while the result is being processed. + assertCallbackPage(); + // Simulate existing email belongs to a federated Facebook account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['facebook.com']); + return testAuth.process(); + }).then(function() { + // The pending credential should be saved here. + assertObjectEquals( + pendingEmailCred, + firebaseui.auth.storage.getPendingEmailCredential(app.getAppId())); + // Federated linking page should be rendered with expected email. + assertFederatedLinkingPage(federatedAccount.getEmail()); + assertFalse(expectedHandlerStatus); + }); +} + + +function testHandleProviderSignIn_oneTap_upgradeAnon_noScopes_passEmailInUse() { + // Render the provider sign-in page with no additional scopes and googleyolo + // enabled and confirm it was rendered correctly. + setupProviderSignInPage('redirect', false, true, true); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user initially signed in on external Auth instance. + externalAuth.setUser(anonymousUser); + // Confirm provider sign in page rendered. + assertProviderSignInPage(); + assertEquals( + 0, firebaseui.auth.AuthUI.prototype.cancelOneTapSignIn.getCallCount()); + assertEquals( + 1, firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getCallCount()); + // Get googleyolo credential handler. + var handler = firebaseui.auth.AuthUI.prototype.showOneTapSignIn.getLastCall() + .getArgument(0); + // Confirm expected handler. + assertEquals( + firebaseui.auth.widget.handler.common.handleGoogleYoloCredential, + handler); + // Simulate successful credential provided by One-Tap. + var expectedHandlerStatus = false; + handler(app, app.getCurrentComponent(), googleYoloIdTokenCredential) + .then(function(status) { + expectedHandlerStatus = status; + }); + // Since no additional scopes are requested, + // linkAndRetrieveDataWithCredential should be called to handle the + // ID token returned by googleyolo. + var expectedCredential = firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken); + // Expected linkWithRedirect error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': expectedCredential, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), + firebase.auth.GoogleAuthProvider.credential( + googleYoloIdTokenCredential.idToken, null)); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkAndRetrieveDataWithCredential should be called with the expected + // credential and simulate an email already in user error thrown. + externalAuth.currentUser.assertLinkAndRetrieveDataWithCredential( + [expectedCredential], + null, + expectedError); + return externalAuth.process().then(function() { + // Callback page should be rendered while the result is being processed. + assertCallbackPage(); + // Simulate email belongs to existing password account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['password']); + return testAuth.process(); + }).then(function() { + // The pending email credential should be cleared at this point. + // Password linking does not require a redirect so no need to save it + // anyway. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // Password linking page rendered. + assertPasswordLinkingPage(federatedAccount.getEmail()); + assertFalse(expectedHandlerStatus); + }); +} + + function testHandleProviderSignIn_popup_success() { // Test successful provider sign-in with popup. // Add additional scopes to test that they are properly passed to the sign-in @@ -1356,6 +1677,7 @@ function testHandleProviderSignIn_accountChooserSelect_appChange() { 'tosUrl': 'http://localhost/tos', 'credentialHelper': firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM }); + app.getExternalAuth().runAuthChangeHandler(); // Callback page should be rendered. assertCallbackPage(); // accountchooser.com client initialized at this point. @@ -1363,6 +1685,7 @@ function testHandleProviderSignIn_accountChooserSelect_appChange() { // First app's AuthUI widget is now rendered. assertEquals(app, firebaseui.auth.AuthUI.getAuthUi()); // Reset app. + app.getAuth().assertSignOut([]); app.reset(); // Render second app. var signInOptions = ['google.com', 'password']; @@ -1370,6 +1693,7 @@ function testHandleProviderSignIn_accountChooserSelect_appChange() { 'widgetUrl': 'http://localhost/firebaseui-widget2', 'signInOptions': signInOptions }); + app2.getExternalAuth().runAuthChangeHandler(); // Second app's AuthUI widget is now rendered. assertEquals(app2, firebaseui.auth.AuthUI.getAuthUi()); // Since accountchooser.com client is already initialized, provider sign in @@ -1412,3 +1736,261 @@ function testHandleProviderSignIn_accountChooserSelect_appChange() { app2.getExternalAuth().uninstall(); }); } + + +function testHandleProviderSignIn_anonymousUpgrade_popup_success() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({'prompt': 'select_account'}); + // Render the provider sign-in page and confirm it was rendered correctly. + setupProviderSignInPage('popup'); + // Test successful anonymous upgrade with popup flow. + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // Simulate anonymous current user on external Auth instance. + externalAuth.setUser(anonymousUser); + // Click the first button, which is Google IdP. + goog.testing.events.fireClickSequence(buttons[0]); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkWithPopup on external Auth user should be triggered. + externalAuth.currentUser.assertLinkWithPopup( + [expectedProvider], + function() { + // Non-anonymous user should be signed in. + externalAuth.setUser({ + 'email': federatedAccount.getEmail(), + 'displayName': federatedAccount.getDisplayName() + }); + return { + 'user': externalAuth.currentUser, + 'credential': cred + }; + }); + return externalAuth.process().then(function() { + testAuth.assertSignOut([]); + return testAuth.process(); + }).then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // User should be redirected to success URL. + testUtil.assertGoTo('http://localhost/home'); + }); +} + + +function testHandleProviderSignIn_anonymousUpgrade_popup_error() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({'prompt': 'select_account'}); + // Render the provider sign-in page and confirm it was rendered correctly. + setupProviderSignInPage('popup'); + // Test upgrade failure with popup flow. + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // Expected linkWithPopup error. + var expectedError = { + 'code': 'auth/credential-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + // Expected FirebaseUI error. + var expectedMergeError = new firebaseui.auth.AuthUIError( + firebaseui.auth.AuthUIError.Error.MERGE_CONFLICT, + null, + cred); + // Simulate anonymous user on external instance. + externalAuth.setUser(anonymousUser); + // Click the first button, which is Google IdP. + goog.testing.events.fireClickSequence(buttons[0]); + // Trigger onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // linkWithPopup called on external user and error simulated. + externalAuth.currentUser.assertLinkWithPopup( + [expectedProvider], + null, + expectedError); + return externalAuth.process().then(function() { + // Pending credential should be cleared from storage. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // signInFailure triggered with expected error. + assertSignInFailure(expectedMergeError); + }); +} + + +function testHandleProviderSignIn_anonymousUpgrade_redirect_success() { + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({'prompt': 'select_account'}); + // Render the provider sign-in page and confirm it was rendered correctly. + setupProviderSignInPage('redirect'); + // Test successful sign in with redirect flow. + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + externalAuth.setUser(anonymousUser); + // Click the first button, which is Google IdP. + goog.testing.events.fireClickSequence(buttons[0]); + externalAuth.runAuthChangeHandler(); + externalAuth.currentUser.assertLinkWithRedirect( + [expectedProvider]); + return externalAuth.process(); +} + + +function testHandleProviderSignIn_anonymousUpgrade_redirect_error() { + var expectedError = {'code': 'auth/network-request-failed'}; + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({'prompt': 'select_account'}); + // Render the provider sign-in page and confirm it was rendered correctly. + setupProviderSignInPage('redirect'); + // Test successful sign in with redirect flow. + app.updateConfig('autoUpgradeAnonymousUsers', true); + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + externalAuth.setUser(anonymousUser); + // Click the first button, which is Google IdP. + goog.testing.events.fireClickSequence(buttons[0]); + externalAuth.runAuthChangeHandler(); + externalAuth.currentUser.assertLinkWithRedirect( + [expectedProvider], + null, + expectedError); + return externalAuth.process().then(function() { + // Remain on provider sign-in page. + assertProviderSignInPage(); + // Show error in info bar. + assertInfoBarMessage( + firebaseui.auth.widget.handler.common.getErrorMessage(expectedError)); + }); +} + + +function testHandleProviderSignIn_anonUpgrade_popup_emailInUse_fedLinking() { + // Test provider sign-in with popup when federated linking required and an + // eligible anonymous user is available for upgrade. Test when existing email + // belongs to a federated account. + // Add additional scopes to test that they are properly passed to the sign-in + // method. + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({'prompt': 'select_account'}); + // Render the provider sign-in page and confirm it was rendered correctly. + setupProviderSignInPage('popup'); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + // Click the first button, which is Google IdP. + goog.testing.events.fireClickSequence(buttons[0]); + + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // Expected linkWithPopup error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), cred); + // Trigger initial onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Link with popup on external anonymous user triggers linking flow. + externalAuth.currentUser.assertLinkWithPopup( + [expectedProvider], + null, + expectedError); + return externalAuth.process().then(function() { + // Simulate existing account is a federated Facebook account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['facebook.com']); + return testAuth.process(); + }).then(function() { + // The pending credential should be saved here. + assertObjectEquals( + pendingEmailCred, + firebaseui.auth.storage.getPendingEmailCredential(app.getAppId())); + // Federated linking triggered. + assertFederatedLinkingPage(federatedAccount.getEmail()); + }); +} + + +function testHandleProviderSignIn_anonUpgrade_popup_emailInUse_passLinking() { + // Test provider sign-in with popup when federated linking required and an + // eligible anonymous user is available for upgrade. Test when existing email + // belongs to a password account. + // Add additional scopes to test that they are properly passed to the sign-in + // method. + var expectedProvider = firebaseui.auth.idp.getAuthProvider('google.com'); + expectedProvider.addScope('googl1'); + expectedProvider.addScope('googl2'); + expectedProvider.setCustomParameters({'prompt': 'select_account'}); + // Render the provider sign-in page and confirm it was rendered correctly. + setupProviderSignInPage('popup'); + // Enable anonymous user upgrade. + app.updateConfig('autoUpgradeAnonymousUsers', true); + // Simulate anonymous user signed in. + externalAuth.setUser(anonymousUser); + // Click the first button, which is Google IdP. + goog.testing.events.fireClickSequence(buttons[0]); + + var cred = firebaseui.auth.idp.getAuthCredential({ + 'providerId': 'google.com', + 'accessToken': 'ACCESS_TOKEN' + }); + // Expected linkWithPopup error. + var expectedError = { + 'code': 'auth/email-already-in-use', + 'credential': cred, + 'email': federatedAccount.getEmail(), + 'message': 'MESSAGE' + }; + var pendingEmailCred = new firebaseui.auth.PendingEmailCredential( + federatedAccount.getEmail(), cred); + // Trigger initial onAuthStateChanged listener. + externalAuth.runAuthChangeHandler(); + // Link with popup on external anonymous user triggers linking flow. + externalAuth.currentUser.assertLinkWithPopup( + [expectedProvider], + null, + expectedError); + return externalAuth.process().then(function() { + // Simulate existing account is a password account. + testAuth.assertFetchProvidersForEmail( + [federatedAccount.getEmail()], ['password']); + return testAuth.process(); + }).then(function() { + // The pending email credential should be cleared at this point. + // Password linking does not require a redirect so no need to save it + // anyway. + assertFalse(firebaseui.auth.storage.hasPendingEmailCredential( + app.getAppId())); + // Password linking page rendered. + assertPasswordLinkingPage(federatedAccount.getEmail()); + }); +} diff --git a/javascript/widgets/handler/signin_test.js b/javascript/widgets/handler/signin_test.js index 905743f1..e257cf11 100644 --- a/javascript/widgets/handler/signin_test.js +++ b/javascript/widgets/handler/signin_test.js @@ -199,6 +199,7 @@ function testHandleSignIn_reset() { firebaseui.auth.widget.handler.handleSignIn(app, container); assertSignInPage(); // Reset current rendered widget page. + app.getAuth().assertSignOut([]); app.reset(); // Container should be cleared. assertComponentDisposed(); diff --git a/javascript/widgets/handler/testhelper.js b/javascript/widgets/handler/testhelper.js index e31a7153..9153f781 100644 --- a/javascript/widgets/handler/testhelper.js +++ b/javascript/widgets/handler/testhelper.js @@ -35,6 +35,7 @@ goog.require('firebaseui.auth.testing.RecaptchaVerifier'); goog.require('firebaseui.auth.ui.page.Base'); goog.require('firebaseui.auth.util'); goog.require('firebaseui.auth.widget.Config'); +goog.require('goog.Promise'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.dom.classlist'); @@ -101,6 +102,11 @@ var googleYoloOtherCredential = { 'id': federatedAccount.getEmail(), 'authMethod': 'https://accounts.google.com' }; +// Mock anonymous user. +var anonymousUser = { + uid: '1234567890', + isAnonymous: true +}; var container; var container2; @@ -115,6 +121,7 @@ var signInCallbackUser; var signInCallbackCredential; var signInCallbackRedirectUrl; var uiShownCallbackCount; +var signInFailureCallback; var callbackStub = new goog.testing.PropertyReplacer(); @@ -174,6 +181,10 @@ function setUp() { signInCallbackRedirectUrl = undefined; signInCallbackCredential = undefined; uiShownCallbackCount = 0; + // Define recorded signInFailure callback. + signInFailureCallback = goog.testing.recordFunction(function() { + return goog.Promise.resolve(); + }); app.setConfig({ 'signInSuccessUrl': 'http://localhost/home', 'widgetUrl': 'http://localhost/firebaseui-widget', @@ -181,7 +192,10 @@ function setUp() { 'siteName': 'Test Site', 'popupMode': false, 'tosUrl': 'http://localhost/tos', - 'credentialHelper': firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM + 'credentialHelper': firebaseui.auth.CredentialHelper.ACCOUNT_CHOOSER_COM, + 'callbacks': { + 'signInFailure': signInFailureCallback + } }); window.localStorage.clear(); window.sessionStorage.clear(); @@ -1020,3 +1034,18 @@ function assertResendCountdown(timeRemaining) { var actual = goog.dom.getTextContent(el); assertEquals(expected, actual); } + + +/** + * Asserts signInFailure callback called with expected error. + * @param {?Object|undefined} expectedError The expected error passed to + * signInFailure callback. + */ +function assertSignInFailure(expectedError) { + // Confirm signInFailure callback triggered with expected argument. + assertEquals(1, signInFailureCallback.getCallCount()); + assertObjectEquals( + expectedError, signInFailureCallback.getLastCall().getArgument(0)); + // Sign in success should not be called. + assertUndefined(signInCallbackUser); +} diff --git a/soy/pages.soy b/soy/pages.soy index 65fca40c..c3c0bc67 100644 --- a/soy/pages.soy +++ b/soy/pages.soy @@ -96,9 +96,7 @@ {if $requireDisplayName} {call firebaseui.auth.soy2.element.name data="all" /} {/if} - {call firebaseui.auth.soy2.element.newPassword} - {param choose: true /} - {/call} + {call firebaseui.auth.soy2.element.newPassword /} {if $tosUrl}{call firebaseui.auth.soy2.element.tos data="all" /}{/if}
diff --git a/soy/strings.soy b/soy/strings.soy index 19ad6bcd..d2cdfa6f 100644 --- a/soy/strings.soy +++ b/soy/strings.soy @@ -229,6 +229,23 @@ {/template} +/** Translates an FirebaseUI Auth error code to a user-displayable string. */ +{template .errorAuthUI kind="text"} + {@param code: string} /** The error code. */ + {switch $code} + {case 'firebaseui/merge-conflict'} + {msg desc="Error message when an existing anonymous user is unable to upgrade to a + non-anonymous account (Google, Password, etc.) because the associated credential for + the non-anonymous account already exists."} + The current anonymous user failed to upgrade. The non-anonymous credential is already + associated with a different user account. + {/msg} + {default} + {call .internalError /} + {/switch} +{/template} + + /** Resend countdown. */ {template .resendCountdown kind="text"} {@param timeRemaining:string} /** The time remaining. */ diff --git a/translations/ar-XB.xtb b/translations/ar-XB.xtb index cf2c4b16..80330301 100644 --- a/translations/ar-XB.xtb +++ b/translations/ar-XB.xtb @@ -69,6 +69,7 @@ ‏‮New‬‏ ‏‮Caledonia‬‏ ‏‮Monaco‬‏ ‏‮Seychelles‬‏ +‏‮The‬‏ ‏‮current‬‏ ‏‮anonymous‬‏ ‏‮user‬‏ ‏‮failed‬‏ ‏‮to‬‏ ‏‮upgrade‬‏. ‏‮The‬‏ ‏‮non‬‏-‏‮anonymous‬‏ ‏‮credential‬‏ ‏‮is‬‏ ‏‮already‬‏ ‏‮associated‬‏ ‏‮with‬‏ ‏‮a‬‏ ‏‮different‬‏ ‏‮user‬‏ ‏‮account‬‏. ‏‮You‬‏’‏‮ve‬‏ ‏‮entered‬‏ ‏‮an‬‏ ‏‮incorrect‬‏ ‏‮password‬‏ ‏‮too‬‏ ‏‮many‬‏ ‏‮times‬‏. ‏‮Try‬‏ ‏‮again‬‏ ‏‮in‬‏ ‏‮a‬‏ ‏‮few‬‏ ‏‮minutes‬‏. ‏‮Your‬‏ ‏‮request‬‏ ‏‮to‬‏ ‏‮reset‬‏ ‏‮your‬‏ ‏‮password‬‏ ‏‮has‬‏ ‏‮expired‬‏ ‏‮or‬‏ ‏‮the‬‏ ‏‮link‬‏ ‏‮has‬‏ ‏‮already‬‏ ‏‮been‬‏ ‏‮used‬‏ ‏‮Twitter‬‏ diff --git a/translations/ar.xtb b/translations/ar.xtb index 979aa8df..1e70d65f 100644 --- a/translations/ar.xtb +++ b/translations/ar.xtb @@ -71,6 +71,7 @@ نيوكاليدونيا موناكو سيشيل +تعذّرت ترقية حساب المستخدم المجهول الحالي لأنّ بيانات اعتماد الحساب غير المجهول مقترنة بحساب مستخدم آخر. لقد أدخلت كلمة مرور غير صحيحة لمرات كثيرة جدًا. يُرجى المحاولة مجددًا بعد بضع دقائق. الأمان انتهت صلاحية طلبك لإعادة تعيين كلمة المرور أو سبق أن تم استخدام الرابط diff --git a/translations/bg.xtb b/translations/bg.xtb index 797899fb..0221d4d5 100644 --- a/translations/bg.xtb +++ b/translations/bg.xtb @@ -71,6 +71,7 @@ Нова Каледония Монако Сейшелски острови +Надстройването на текущия анонимен потребител не бе успешно. Посочените неанонимни идентификационни данни вече са свързани с профил на друг потребител. Въведохте неправилна парола твърде много пъти. Опитайте отново след няколко минути. Сигурност Заявката за възстановяване на паролата ви е изтекла или връзката вече е използвана diff --git a/translations/ca.xtb b/translations/ca.xtb index c4c838b7..6881edd8 100644 --- a/translations/ca.xtb +++ b/translations/ca.xtb @@ -71,6 +71,7 @@ Nova Caledònia Mònaco Seychelles +No s'ha pogut actualitzar l'usuari anònim actual. La credencial no anònima ja està associada a un altre compte d'usuari. Has introduït una contrasenya incorrecta massa vegades. Torna-ho a provar d'aquí a uns quants minuts. Seguretat La teva sol·licitud per restablir la contrasenya ha caducat o l'enllaç ja s'ha utilitzat diff --git a/translations/cs.xtb b/translations/cs.xtb index 0b55d2a7..8e30f5f4 100644 --- a/translations/cs.xtb +++ b/translations/cs.xtb @@ -71,6 +71,7 @@ Nová Kaledonie Monako Seychelly +Nepodařilo se upgradovat stávajícího anonymního uživatele. Identifikační údaje neanonymního uživatele jsou již přidruženy k účtu jiného uživatele. Provedli jste příliš mnoho neplatných pokusů o zadání hesla. Opakujte akci za chvíli. Zabezpečení Platnost vaší žádosti o obnovení hesla vypršela nebo byl odkaz již použit diff --git a/translations/da.xtb b/translations/da.xtb index 11dd4917..87a71e64 100644 --- a/translations/da.xtb +++ b/translations/da.xtb @@ -71,6 +71,7 @@ Ny Kaledonien Monaco Seychellerne +Opgradering af den nuværende anonyme bruger mislykkedes. De ikke-anonyme loginoplysninger er allerede knyttet til en anden brugerkonto. Du har indtastet en forkert adgangskode for mange gange. Prøv igen om et par minutter. Sikkerhed Din anmodning om at nulstille din adgangskode er udløbet, eller linket er allerede brugt diff --git a/translations/de.xtb b/translations/de.xtb index 3828a1c8..ea8ad36e 100644 --- a/translations/de.xtb +++ b/translations/de.xtb @@ -38,7 +38,7 @@ Macau Hallo , Konto löschen -Geben Sie den sechsstelligen Code ein, den wir an gesendet haben +Geben Sie den 6-stelligen Code ein, den wir an gesendet haben Folgen Sie der an gesendeten Anleitung, um Ihr Passwort zurückzusetzen. Ungarn Suriname @@ -71,6 +71,7 @@ Neukaledonien Monaco Seychellen +Der aktuelle anonyme Nutzer konnte sein Konto nicht upgraden, weil die entsprechenden Anmeldedaten bereits mit einem anderen nicht-anonymen Nutzerkonto verknüpft sind. Sie haben zu oft ein falsches Passwort eingegeben. Versuchen Sie es in einigen Minuten erneut. Sicherheit Ihre Anfrage zum Zurücksetzen des Passworts ist abgelaufen oder der Link wurde bereits verwendet @@ -243,7 +244,7 @@ Dominikanische Republik Jersey Passwort zurücksetzen -Geben Sie den sechsstelligen Code ein, den wir an &lrm; geschickt haben +Geben Sie den 6-stelligen Code ein, den wir an &lrm; geschickt haben Ruanda E-Mail-Adresse Diese Telefonnummer wurde schon zu oft verwendet diff --git a/translations/el.xtb b/translations/el.xtb index 6040db2a..6737804b 100644 --- a/translations/el.xtb +++ b/translations/el.xtb @@ -71,6 +71,7 @@ Νέα Καληδονία Μονακό Σεϋχέλλες +Δεν ήταν δυνατή η αναβάθμιση του τρέχοντος ανώνυμου χρήστη. Τα μη ανώνυμα διαπιστευτήρια σχετίζονται ήδη με έναν άλλο λογαριασμό χρήστη. Πληκτρολογήσατε πολλές φορές λανθασμένο κωδικό πρόσβασης. Δοκιμάστε ξανά σε λίγα λεπτά. Ασφάλεια Το αίτημα επαναφοράς του κωδικού πρόσβασής σας έληξε ή ο σύνδεσμος χρησιμοποιείται ήδη diff --git a/translations/en-GB.xtb b/translations/en-GB.xtb index efa5aaf9..be9625b0 100644 --- a/translations/en-GB.xtb +++ b/translations/en-GB.xtb @@ -71,6 +71,7 @@ New Caledonia Monaco Seychelles +The current anonymous user failed to upgrade. The non-anonymous credential is already associated with a different user account. You've entered an incorrect password too many times. Try again in a few minutes. Security Your request to reset your password has expired or the link has already been used diff --git a/translations/en-XA.xtb b/translations/en-XA.xtb index 0ec89e36..a465a54c 100644 --- a/translations/en-XA.xtb +++ b/translations/en-XA.xtb @@ -69,6 +69,7 @@ [Ñéŵ Çåļéðöñîå one two] [Möñåçö one] [Šéýçĥéļļéš one two] +[Ţĥé çûŕŕéñţ åñöñýmöûš ûšéŕ ƒåîļéð ţö ûþĝŕåðé. Ţĥé ñöñ-åñöñýmöûš çŕéðéñţîåļ îš åļŕéåðý åššöçîåţéð ŵîţĥ å ðéŕéñţ ûšéŕ åççöûñţ. one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty] [Ýöû’vé éñţéŕéð åñ îñçöŕŕéçţ þåššŵöŕð ţöö måñý ţîméš. Ţŕý åĝåîñ îñ å ƒéŵ mîñûţéš. one two three four five six seven eight nine ten eleven twelve thirteen fourteen] [Ýöûŕ ŕéqûéšţ ţö ŕéšéţ ýöûŕ þåššŵöŕð ĥåš éxþîŕéð öŕ ţĥé ļîñķ ĥåš åļŕéåðý бééñ ûšéð one two three four five six seven eight nine ten eleven twelve thirteen fourteen] [Ţŵîţţéŕ one] diff --git a/translations/es-419.xtb b/translations/es-419.xtb index 86336808..589d1191 100644 --- a/translations/es-419.xtb +++ b/translations/es-419.xtb @@ -71,6 +71,7 @@ Nueva Caledonia Mónaco Seychelles +No se pudo actualizar el usuario anónimo actual. La credencial no anónima ya está asociada con otra cuenta de usuario. Ingresaste una contraseña incorrecta demasiadas veces. Vuelve a intentarlo en unos minutos. Seguridad La solicitud para restablecer tu contraseña caducó o ya se usó el vínculo diff --git a/translations/es.xtb b/translations/es.xtb index b0e41128..379171e5 100644 --- a/translations/es.xtb +++ b/translations/es.xtb @@ -71,6 +71,7 @@ Nueva Caledonia Mónaco Seychelles +El usuario anónimo actual no ha podido realizar la actualización. Las credenciales no anónimas ya están asociadas a una cuenta de usuario diferente. Has introducido una contraseña incorrecta demasiadas veces. Vuelve a intentarlo en unos minutos. Seguridad La petición para cambiar la contraseña ha caducado o ya se ha usado el enlace diff --git a/translations/fa.xtb b/translations/fa.xtb index 789872c1..9a0ae6a7 100644 --- a/translations/fa.xtb +++ b/translations/fa.xtb @@ -26,7 +26,7 @@ چاد کد پس از مجدداً ارسال می‌شود بلاروس -این آدرس رایانامه با هیچ حسابی مطابقت ندارد. +این نشانی رایانامه با هیچ حسابی مطابقت ندارد. با ضربه زدن روی «» پیامکی ارسال می‌شود. ممکن است هزینه پیام و انتقال داده اعمال شود. سنت هلنا باربادوس @@ -71,6 +71,7 @@ کالدونیای جدید موناکو سیشل +کاربر ناشناس فعلی ارتقا داده نشد. اطلاعات کاربری شناخته‌شده قبلاً با حساب کاربری دیگری مرتبط شده است. دفعات زیادی گذرواژه اشتباه وارد کرده‌اید. پس از چند دقیقه دوباره امتحان کنید. امنیت درخواست بازنشانی گذرواژه‌تان منقضی شده یا قبلاً از پیوند استفاده شده است @@ -127,7 +128,7 @@ نام حسابتان را وارد کنید عراق ویتنام -این آدرس رایانامه درست نیست +این نشانی رایانامه درست نیست مایوت ورود به سیستم بازنشانی گذرواژه‌ کاربر @@ -154,7 +155,7 @@ استونی Facebook آرژانتین -آدرس رایانامه جدید را وارد کنید +نشانی رایانامه جدید را وارد کنید لغو پیوند جمهوری گینه سوئیس @@ -162,19 +163,19 @@ ژاپن مونته‌نگرو موزامبیک -هیچ حسابی منطبق با این آدرس رایانامه وجود ندارد +هیچ حسابی منطبق با این نشانی رایانامه وجود ندارد ترکیه تاجیکستان لائوس ارسال مجدد هندوراس نیجر -هیچ حسابی منطبق با این آدرس رایانامه وجود ندارد +هیچ حسابی منطبق با این نشانی رایانامه وجود ندارد اردن برای بازیابی گذرواژه‌تان، دستورالعمل‌های ارسال‌شده به را دنبال کنید ورود به سیستم با رایانامه روسیه -قبلاً یک حساب از این آدرس رایانامه استفاده کرده است. +قبلاً یک حساب از این نشانی رایانامه استفاده کرده است. لهستان اسرائیل خطا رخ داد @@ -199,7 +200,7 @@ درخواست مبنی بر تأیید رایانامه‌تان منقضی شده است یا پیوند قبلاً استفاده شده است عمان جزایر کارائیب هلند -آدرس رایانامه به‌روز است +نشانی رایانامه به‌روز است ورود به سیستم با Twitter شماره تلفن از قبل یک حساب دارید @@ -263,7 +264,7 @@ کوزوو بلغارستان تأیید رایانامه‌ مربوط به -برای تغییر آدرس رایانامه مرتبط با حسابتان باید دوباره وارد سیستم شوید. +برای تغییر نشانی رایانامه مرتبط با حسابتان باید دوباره وارد سیستم شوید. وانواتو ساموآی آمریکا برزیل @@ -283,7 +284,7 @@ میکرونزی هنگ کنگ آلبانی -آدرس رایانامه ورود به سیستم‌تان به برگردانده شد. +نشانی رایانامه ورود به سیستم‌تان به برگردانده شد. کامرون سوالبارد و یان ماین اوکراین diff --git a/translations/fi.xtb b/translations/fi.xtb index 4ab5a517..9d74a108 100644 --- a/translations/fi.xtb +++ b/translations/fi.xtb @@ -71,6 +71,7 @@ Uusi-Kaledonia Monaco Seychellit +Nykyisen anonyymin käyttäjän päivittäminen epäonnistui. Ei-anonyymi kirjautumistieto on jo yhdistetty toiseen käyttäjätiliin. Olet antanut virheellisen salasanan liian monta kertaa. Yritä uudelleen muutaman minuutin kuluttua. Turvallisuus Pyyntösi nollata salasana on vanhentunut tai linkkiä on jo käytetty. diff --git a/translations/fil.xtb b/translations/fil.xtb index f2c19d48..6825ec20 100644 --- a/translations/fil.xtb +++ b/translations/fil.xtb @@ -70,6 +70,7 @@ New Caledonia Monaco Seychelles +Hindi nakapag-upgrade ang kasalukuyang anonymous na user. Nauugnay na ang di-anonymous na kredensyal sa ibang account ng user. Nagpasok ka ng di-wastong password nang masyadong maraming beses. Pakisubukang muli pagkalipas ng ilang minuto. Ang iyong kahilingan na i-reset ang iyong password ay nag-expire na o nagamit na ang link Twitter diff --git a/translations/fr.xtb b/translations/fr.xtb index 934665a6..f23dfb11 100644 --- a/translations/fr.xtb +++ b/translations/fr.xtb @@ -71,6 +71,7 @@ Nouvelle-Calédonie Monaco Seychelles +Échec de la mise à jour de l'utilisateur anonyme actuel. Les identifiants non anonymes sont déjà associés à un autre compte utilisateur. Vous avez saisi un mot de passe incorrect un trop grand nombre de fois. Veuillez réessayer dans quelques minutes. Sécurité Votre demande de réinitialisation du mot de passe a expiré ou ce lien a déjà été utilisé diff --git a/translations/hi.xtb b/translations/hi.xtb index 410cecad..8ed3c7c2 100644 --- a/translations/hi.xtb +++ b/translations/hi.xtb @@ -71,6 +71,7 @@ न्यू कैलेडोनिया मॉनेको सेशल्स +मौजूदा अनाम उपयोगकर्ता अपग्रेड नहीं कर सका. गैर-अनाम क्रेडेंशियल पहले से ही किसी दूसरे उपयोगकर्ता खाते से जुड़ा हुआ है. आपने कई बार गलत पासवर्ड डाला. कुछ मिनटों में फिर से कोशिश करें. सुरक्षा आपके पासवर्ड रीसेट अनुरोध की समय-सीमा समाप्त हो गई है या लिंक का उपयोग पहले ही किया जा चुका है diff --git a/translations/hr.xtb b/translations/hr.xtb index 4bd3dcff..9deea5cd 100644 --- a/translations/hr.xtb +++ b/translations/hr.xtb @@ -71,6 +71,7 @@ Nova Kaledonija Monako Sejšeli +Trenutačni anonimni korisnik ne može izvršiti nadogradnju. Neanonimna vjerodajnica već je povezana s drugim korisničkim računom. Unijeli ste netočnu zaporku previše puta. Pokušajte ponovo za nekoliko minuta. Sigurnost Vaš je zahtjev za ponovno postavljanje zaporke istekao ili je veza iskorištena diff --git a/translations/hu.xtb b/translations/hu.xtb index 52bded14..8da2e74f 100644 --- a/translations/hu.xtb +++ b/translations/hu.xtb @@ -71,6 +71,7 @@ Új-Kaledónia Monaco Seychelle-szigetek +A jelenlegi anonim felhasználó frissítése nem sikerült. A nem anonim hitelesítési adat már egy másik felhasználói fiókhoz van társítva. Túl sokszor adott meg hibás jelszót. Néhány perc múlva próbálja újra. Biztonság A jelszó-visszaállítási kérelem lejárt, vagy pedig már használták a linket. diff --git a/translations/id.xtb b/translations/id.xtb index 6e06d91c..2d002969 100644 --- a/translations/id.xtb +++ b/translations/id.xtb @@ -71,6 +71,7 @@ Kaledonia Baru Monako Republik Seychelles +Pengguna anonim saat ini gagal melakukan upgrade. Kredensial non-anonim tersebut telah dikaitkan dengan akun pengguna lain. Anda sudah terlalu sering memasukkan sandi yang salah. Coba beberapa menit lagi. Keamanan Permintaan untuk menyetel ulang sandi sudah tidak berlaku atau link telah digunakan diff --git a/translations/it.xtb b/translations/it.xtb index 6c36dbbf..0e7b5365 100644 --- a/translations/it.xtb +++ b/translations/it.xtb @@ -71,6 +71,7 @@ Nuova Caledonia Monaco Seychelles +Impossibile eseguire l'upgrade per l'utente anonimo attuale. La credenziale non anonima è già associata a un altro account utente. Hai effettuato troppi tentativi con una password errata. Riprova tra qualche minuto. Sicurezza La richiesta di reimpostazione della password è scaduta o il collegamento è già stato utilizzato diff --git a/translations/iw.xtb b/translations/iw.xtb index 7b61e0cd..101f4290 100644 --- a/translations/iw.xtb +++ b/translations/iw.xtb @@ -70,6 +70,7 @@ קלדוניה החדשה מונקו איי סיישל +המשתמש האנונימי הנוכחי לא הצליח לשדרג את החשבון לחשבון לא אנונימי. פרטי הכניסה לחשבון שאינו אנונימי כבר משויכים לחשבון משתמש אחר. הזנת סיסמה שגויה יותר מדי פעמים. אפשר יהיה לנסות שוב בעוד כמה דקות. הבקשה לאיפוס הסיסמה כבר לא בתוקף או שכבר נעשה שימוש בקישור Twitter diff --git a/translations/ja.xtb b/translations/ja.xtb index fe2b3355..fbb0fc56 100644 --- a/translations/ja.xtb +++ b/translations/ja.xtb @@ -71,6 +71,7 @@ ニューカレドニア モナコ セーシェル +現在の匿名ユーザーをアップグレードできませんでした。この認証情報はすでに別の非匿名ユーザー アカウントに関連付けられています。 正しくないパスワードを何度も入力しています。しばらくしてからもう一度お試しください。 セキュリティ パスワードの再設定のリクエストの期限が切れたか、リンクがすでに使用されています diff --git a/translations/ko.xtb b/translations/ko.xtb index d7d46164..f68440bf 100644 --- a/translations/ko.xtb +++ b/translations/ko.xtb @@ -71,6 +71,7 @@ 뉴칼레도니아 모나코 세이셸 +현재 익명 사용자를 업그레이드하지 못했습니다. 익명이 아닌 사용자 인증 정보가 이미 다른 사용자 계정에 연결되어 있습니다. 비밀번호 입력 오류 횟수를 초과했습니다. 잠시 후에 다시 시도해 주세요. 보안 비밀번호 재설정 요청이 만료되었거나 링크가 이미 사용되었습니다. diff --git a/translations/lt.xtb b/translations/lt.xtb index 5a8c60fb..10d89cff 100644 --- a/translations/lt.xtb +++ b/translations/lt.xtb @@ -70,6 +70,7 @@ Naujoji Kaledonija Monakas Seišeliai +Esamo anoniminio naudotojo naujovinti nepavyko. Neanoniminiai prisijungimo duomenys jau susieti su kito naudotojo paskyra. Įvedėte netinkamą slaptažodį per daug kartų. Po kelių minučių bandykite dar kartą. Jūsų užklausa pakeisti slaptažodį nebegalioja arba nuoroda jau buvo panaudota Twitter diff --git a/translations/lv.xtb b/translations/lv.xtb index 69104ba5..c75e478e 100644 --- a/translations/lv.xtb +++ b/translations/lv.xtb @@ -70,6 +70,7 @@ Jaunkaledonija Monako Seišelas +Pašreizējam anonīmajam lietotājam neizdevās veikt jaunināšanu. Akreditācijas dati, kuri nav anonīmi, jau ir saistīti ar citu lietotāja kontu. Esat pārāk daudz reižu ievadījis nepareizu paroli. Lūdzu, mēģiniet vēlreiz pēc dažām minūtēm. Jūsu pieprasījumam par paroles atiestatīšanu ir beidzies termiņš, vai saite jau ir izmantota Twitter diff --git a/translations/nl.xtb b/translations/nl.xtb index f0aaf8fb..10503e30 100644 --- a/translations/nl.xtb +++ b/translations/nl.xtb @@ -71,6 +71,7 @@ Nieuw-Caledonië Monaco Seychellen +De huidige anonieme gebruiker kon niet worden geüpgraded. De niet-anonieme gegevens zijn al gekoppeld aan een ander gebruikersaccount. U heeft te vaak een fout wachtwoord ingevoerd. Probeer het over enkele minuten opnieuw. Beveiliging Uw verzoek om uw wachtwoord te resetten, is verlopen of de link is al gebruikt diff --git a/translations/no.xtb b/translations/no.xtb index e368ab2b..f9fb967f 100644 --- a/translations/no.xtb +++ b/translations/no.xtb @@ -71,6 +71,7 @@ Ny-Caledonia Monaco Seychellene +Kunne ikke oppgradere den anonyme brukeren. Den ikke-anonyme legitimasjonen er allerede tilknyttet en annen brukerkonto. Du har angitt feil passord for mange ganger. Prøv igjen om noen minutter. Sikkerhet Forespørselen om å tilbakestille passordet ditt har utløpt, eller så er linken allerede brukt diff --git a/translations/pl.xtb b/translations/pl.xtb index a4a798d1..3fd1450d 100644 --- a/translations/pl.xtb +++ b/translations/pl.xtb @@ -71,6 +71,7 @@ Nowa Kaledonia Monako Seszele +Nie udało się uaktualnić bieżącego użytkownika anonimowego. Nieanonimowe dane logowania są już powiązane z innym kontem użytkownika. Zbyt wiele razy podano niepoprawne hasło. Spróbuj jeszcze raz za kilka minut. Zabezpieczenia Twoja prośba o zresetowanie hasła straciła ważność albo ktoś kliknął już ten link diff --git a/translations/pt-PT.xtb b/translations/pt-PT.xtb index 0839f4ee..280b91e3 100644 --- a/translations/pt-PT.xtb +++ b/translations/pt-PT.xtb @@ -70,6 +70,7 @@ Nova Caledónia Mónaco Seicheles +Ocorreu uma falha na atualização do utilizador anónimo atual. A credencial não anónima já está associada a outra conta de utilizador. Introduziu uma palavra-passe incorreta demasiadas vezes. Tente novamente dentro de alguns minutos. O pedido para repor a palavra-passe expirou ou o link já foi utilizado Twitter @@ -183,7 +184,7 @@ Código errado. Tente novamente. Ilhas Turcas e Caicos Reenviar código em -Verifique o seu e-mail +Verifique o seu email Adicionar palavra-passe Salvador Sérvia @@ -192,7 +193,7 @@ Suécia São Martinho Santa Lúcia -Verifique o seu e-mail +Verifique o seu email O seu pedido de validação do email expirou ou o link já foi utilizado Omã Países Baixos Caribenhos @@ -324,7 +325,7 @@ Alemanha Quirguizistão Já utilizou para iniciar sessão. Introduza a palavra-passe da conta em questão. -{plural_var,plural, =1{A palavra-passe não é suficientemente forte. Utilize, pelo menos, caráter e uma combinação de letras e números}one{A palavra-passe não é suficientemente forte. Utilize, pelo menos, caracteres e uma combinação de letras e números}other{A palavra-passe não é suficientemente forte. Utilize, pelo menos, caracteres e uma combinação de letras e números}} +{plural_var,plural, =1{A palavra-passe não é suficientemente forte. Utilize, pelo menos, caráter e uma combinação de letras e números}other{A palavra-passe não é suficientemente forte. Utilize, pelo menos, caracteres e uma combinação de letras e números}} Zimbabué Ocorreu um problema ao validar o número de telefone República Checa diff --git a/translations/pt.xtb b/translations/pt.xtb index dd62277f..3bded633 100644 --- a/translations/pt.xtb +++ b/translations/pt.xtb @@ -71,6 +71,7 @@ Nova Caledônia Mônaco Seicheles +Não foi possível fazer o upgrade deste usuário anônimo. A credencial não anônima já está associada a uma conta de usuário diferente. Você digitou a senha incorretamente várias vezes. Tente novamente em alguns minutos. Segurança Sua solicitação para redefinir a senha expirou ou o link já foi usado @@ -343,7 +344,7 @@ Líbia Google Quênia -Esta ação apagará todos os dados associados à sua conta e não poderá ser desfeita. Tem certeza de que deseja excluir sua conta? +Esta ação apagará todos os dados associados à sua conta e não poderá ser desfeita. Tem certeza que quer excluir sua conta? Talvez você receba um SMS quando tocar em "Verificar". Taxas de mensagens e dados podem ser aplicáveis. Bolívia Porto Rico diff --git a/translations/ro.xtb b/translations/ro.xtb index 7d28b9cf..14c00445 100644 --- a/translations/ro.xtb +++ b/translations/ro.xtb @@ -71,6 +71,7 @@ Noua Caledonie Monaco Seychelles +Nu am putut face upgrade pentru actualul utilizator anonim, deoarece datele de conectare sunt deja asociate altui cont de utilizator. Ați introdus de prea multe ori o parolă incorectă. Încercați din nou peste câteva minute. Securitate Solicitarea de resetare a parolei a expirat sau linkul a fost folosit deja diff --git a/translations/ru.xtb b/translations/ru.xtb index 1b7dae42..bfd41717 100644 --- a/translations/ru.xtb +++ b/translations/ru.xtb @@ -71,6 +71,7 @@ Новая Каледония Монако Сейшелы +Не удалось обновить текущий анонимный аккаунт, поскольку указанные учетные данные уже связаны с другим аккаунтом пользователя. Вы неправильно ввели пароль слишком много раз. Прежде чем повторить попытку, подождите несколько минут. Безопасность Срок действия запроса на сброс пароля истек, или ссылка уже использована. @@ -134,7 +135,7 @@ Отправить Непал Токелау -Выбранные учетные данные не поддерживаются поставщиком услуг аутентификации. +Поставщик услуг аутентификации не поддерживает эти учетные данные. Колумбия Вы указали номер телефона с ошибкой. Бурунди diff --git a/translations/sk.xtb b/translations/sk.xtb index 6f68ee39..c141e59e 100644 --- a/translations/sk.xtb +++ b/translations/sk.xtb @@ -71,6 +71,7 @@ Nová Kaledónia Monako Seychely +Nepodarilo sa inovovať aktuálneho anonymného používateľa. Neanonymné poverenie je už priradené k inému používateľskému účtu. Zadali ste nesprávne heslo príliš veľakrát. Skúste to znova o pár minút. Zabezpečenie Platnosť vašej žiadosti o obnovu hesla vypršala alebo už bol odkaz použitý diff --git a/translations/sl.xtb b/translations/sl.xtb index 4e693fb4..77cc3582 100644 --- a/translations/sl.xtb +++ b/translations/sl.xtb @@ -70,6 +70,7 @@ Nova Kaledonija Monako Sejšeli +Trenutno anonimni uporabnik ni mogel nadgraditi računa. Poverilnica za neanonimni račun je že povezana z drugim uporabniškim računom. Prevečkrat ste vnesli nepravilno geslo. Poskusite znova čez nekaj minut. Vaša zahteva za ponastavitev gesla je potekla ali pa je bila povezava že uporabljena Twitter diff --git a/translations/sr.xtb b/translations/sr.xtb index f60b0072..355f6f09 100644 --- a/translations/sr.xtb +++ b/translations/sr.xtb @@ -71,6 +71,7 @@ Нова Каледонија Монако Сејшели +Надоградња тренутног анонимног корисника није успела. Неанонимни акредитив је већ повезан са другим налогом корисника. Превише пута сте унели нетачну лозинку. Пробајте поново за неколико минута. Безбедност Захтев за ресетовање лозинке је истекао или је линк већ употребљен diff --git a/translations/sv.xtb b/translations/sv.xtb index 26f179c2..fc84f5d1 100644 --- a/translations/sv.xtb +++ b/translations/sv.xtb @@ -71,6 +71,7 @@ Nya Kaledonien Monaco Seychellerna +Den aktuella anonyma användaren kunde inte uppgradera. De icke-anonyma användaruppgifterna är redan kopplade till ett annat konto. Du har angett fel lösenord för många gånger. Försök igen om några minuter. Säkerhet Din begäran om att återställa ditt lösenord har upphört, eller så har länken redan använts diff --git a/translations/th.xtb b/translations/th.xtb index f7523581..6bb6a816 100644 --- a/translations/th.xtb +++ b/translations/th.xtb @@ -71,6 +71,7 @@ นิวแคลิโดเนีย โมนาโค เซเชลส์ +การอัปเกรดผู้ใช้ที่ไม่ระบุชื่อปัจจุบันล้มเหลว ข้อมูลรับรองที่ระบุชื่อได้เชื่อมโยงกับบัญชีผู้ใช้รายอื่นอยู่แล้ว คุณป้อนรหัสผ่านไม่ถูกต้องหลายครั้งเกินไป โปรดรอสักครู่แล้วลองอีกครั้ง ความปลอดภัย คำขอรีเซ็ตรหัสผ่านของคุณหมดอายุหรือมีการใช้งานลิงก์นี้แล้ว diff --git a/translations/tr.xtb b/translations/tr.xtb index 2abafb74..4c09a484 100644 --- a/translations/tr.xtb +++ b/translations/tr.xtb @@ -71,6 +71,7 @@ Yeni Kaledonya Monako Seyşeller +Mevcut anonim kullanıcı için yükseltme yapılamadı. Anonim olmayan kimlik bilgileri zaten farklı bir kullanıcı hesabıyla ilişkilendirilmiş. Çok fazla kez yanlış şifre girdiniz. Birkaç dakika içinde tekrar deneyin. Güvenlik Şifrenizi sıfırlama isteğinizin süresi dolmuş veya bağlantı zaten kullanılmış diff --git a/translations/uk.xtb b/translations/uk.xtb index e4451ccf..14dfd9c1 100644 --- a/translations/uk.xtb +++ b/translations/uk.xtb @@ -71,6 +71,7 @@ Нова Каледонія Монако Сейшельські Острови +Не вдалось оновити поточний анонімний обліковий запис. Указані облікові дані вже пов’язані з іншим обліковим записом користувача. Ви ввели неправильний пароль забагато разів. Спробуйте за кілька хвилин. Безпека Ваш запит на скидання пароля вже не дійсний або посиланням уже скористалися diff --git a/translations/vi.xtb b/translations/vi.xtb index 28f2754d..14325491 100644 --- a/translations/vi.xtb +++ b/translations/vi.xtb @@ -71,6 +71,7 @@ New Caledonia Monaco Seychelles +Người dùng ẩn danh hiện tại không thể nâng cấp. Thông tin đăng nhập không ẩn danh này đã được liên kết với một tài khoản người dùng khác. Bạn đã nhập sai mật khẩu quá nhiều lần. Hãy thử lại sau một vài phút. Bảo mật Yêu cầu đặt lại mật khẩu của bạn đã hết hạn hoặc liên kết đã được sử dụng diff --git a/translations/zh-CN.xtb b/translations/zh-CN.xtb index 9867af89..329172d2 100644 --- a/translations/zh-CN.xtb +++ b/translations/zh-CN.xtb @@ -73,6 +73,7 @@ 新喀里多尼亚 摩纳哥 塞舌尔 +当前匿名用户未能升级。此非匿名凭据已与另一用户帐号关联。 您输入错误密码的次数过多,请过几分钟再试。 安全 您的密码重置请求已过期,或者相关链接已被使用 diff --git a/translations/zh-TW.xtb b/translations/zh-TW.xtb index c38d1900..b1fe67d7 100644 --- a/translations/zh-TW.xtb +++ b/translations/zh-TW.xtb @@ -71,6 +71,7 @@ 新喀里多尼亞 摩納哥 塞席爾 +當前的匿名使用者無法升級,因為該名使用者的非匿名憑證已連結至其他使用者帳戶。 您輸入錯誤密碼的次數過多,請於幾分鐘後再試一次。 安全性 您的密碼重設要求已過期,或是先前已使用過重設密碼的連結