Skip to content

Commit

Permalink
FirebaseUI release (#305)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wti806 authored and bojeil-google committed Feb 5, 2018
1 parent a2b741b commit d355eec
Show file tree
Hide file tree
Showing 86 changed files with 6,710 additions and 145 deletions.
203 changes: 199 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<script src="https://www.gstatic.com/firebasejs/ui/2.5.1/firebase-ui-auth__{LANGUAGE_CODE}.js"></script>
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/2.5.1/firebase-ui-auth.css" />
<script src="https://www.gstatic.com/firebasejs/ui/2.6.0/firebase-ui-auth__{LANGUAGE_CODE}.js"></script>
<link type="text/css" rel="stylesheet" href="https://www.gstatic.com/firebasejs/ui/2.6.0/firebase-ui-auth.css" />
```

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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -203,6 +206,18 @@ for a more in-depth example, showcasing a Single Page Application mode.
</html>
```

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
Expand Down Expand Up @@ -284,6 +299,19 @@ FirebaseUI supports the following configuration parameters.
</thead>
<tbody>
<tr>
<td>autoUpgradeAnonymousUsers</td>
<td>No</td>
<td>
Whether to automatically upgrade existing anonymous users on sign-in/sign-up.
See <a href="#upgrading-anonymous-users">Upgrading anonymous users</a>.
<br/>
<em>Default:</em>
<code>false</code>
When set to <code>true</code>, <code>signInFailure</code> callback is
required to be provided to handle merge conflicts.
</td>
</tr>
<tr>
<td>callbacks</td>
<td>No</td>
<td>
Expand Down Expand Up @@ -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>|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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -677,6 +736,130 @@ FirebaseUI is displayed.
</html>
```

### 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: '<url-to-redirect-to-on-success>',
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('<url-to-redirect-to-on-success>');
});

}
}
});
```


## Customizing FirebaseUI for authentication

Currently, FirebaseUI does not offer customization out of the box. However, the
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion changelog.txt
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 9 additions & 1 deletion javascript/testing/acclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
};


Expand Down Expand Up @@ -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_);
}
};
Expand Down
9 changes: 9 additions & 0 deletions javascript/testing/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,21 @@ 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',
SEND_PASSWORD_RESET_EMAIL: 'sendPasswordResetEmail',
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',
Expand Down Expand Up @@ -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',
Expand Down
8 changes: 4 additions & 4 deletions javascript/utils/acclient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<firebaseui.auth.Account>} opt_localAccounts The local account
* @param {Array<firebaseui.auth.Account>=} 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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion javascript/utils/account.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
4 changes: 2 additions & 2 deletions javascript/utils/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
Loading

0 comments on commit d355eec

Please sign in to comment.