Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,9 @@ const ui = initializeUI({

#### `autoUpgradeAnonymousUsers`

The `autoUpgradeAnonymousUsers` behavior will automatically upgrade a user who is anonymously authenticated with your application upon a successful sign in (including OAuth). You can optionally provide a callback to handle an upgrade (such as merging account data). During the async callback, the UI will stay in a pending state.
The `autoUpgradeAnonymousUsers` behavior will automatically upgrade a user who is anonymously authenticated with your application upon a successful sign in (including OAuth). You can optionally provide callbacks to handle successful upgrades and failed upgrade attempts. During async callbacks, the UI will stay in a pending state.

When an upgrade succeeds, the anonymous user's UID is preserved and the new credential is linked to that user. When an upgrade fails (for example, because an OAuth credential is already linked to another account), `onUpgradeFailure` receives the original error and the anonymous user's `oldUserId` so your app can decide whether to migrate anonymous user data into the existing account. Return `"handled"` from `onUpgradeFailure` to suppress the default FirebaseUI error. Return `undefined`, omit the callback, or throw from the callback to preserve the default error behavior.

```ts
import { autoUpgradeAnonymousUsers } from '@firebase-oss/ui-core';
Expand All @@ -346,7 +348,12 @@ const ui = initializeUI({
behaviors: [autoUpgradeAnonymousUsers({
async onUpgrade(ui, oldUserId, credential) {
// Some account upgrade logic.
}
},
async onUpgradeFailure({ ui, oldUserId, error, credential, provider }) {
// Optional merge-conflict handling.
// Return "handled" if your app handled the failure and FirebaseUI
// should not show the default error.
},
})],
});
```
Expand Down
188 changes: 164 additions & 24 deletions packages/core/src/auth.test.ts

Large diffs are not rendered by default.

140 changes: 88 additions & 52 deletions packages/core/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,47 @@ function setPendingState(ui: FirebaseUI) {
ui.setState("pending");
}

type AnonymousUpgradeAttempt =
| { status: "upgraded"; credential: UserCredential }
| { status: "stopped" }
| { status: "skipped" };

async function attemptAnonymousCredentialUpgrade(
ui: FirebaseUI,
credential: AuthCredential
): Promise<AnonymousUpgradeAttempt> {
if (!hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
return { status: "skipped" };
}

const wasAnonymous = ui.auth.currentUser?.isAnonymous === true;
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);

if (result) {
return { status: "upgraded", credential: result };
}

return wasAnonymous ? { status: "stopped" } : { status: "skipped" };
}

async function attemptAnonymousProviderUpgrade(
ui: FirebaseUI,
provider: AuthProvider
): Promise<AnonymousUpgradeAttempt> {
if (!hasBehavior(ui, "autoUpgradeAnonymousProvider")) {
return { status: "skipped" };
}

const wasAnonymous = ui.auth.currentUser?.isAnonymous === true;
const result = await getBehavior(ui, "autoUpgradeAnonymousProvider")(ui, provider);

if (result) {
return { status: "upgraded", credential: result };
}

return wasAnonymous ? { status: "stopped" } : { status: "skipped" };
}

/**
* Signs in with an email and password.
*
Expand All @@ -72,23 +113,23 @@ function setPendingState(ui: FirebaseUI) {
* @param ui - The FirebaseUI instance.
* @param email - The email to sign in with.
* @param password - The password to sign in with.
* @returns {Promise<UserCredential>} A promise containing the user credential.
* @returns {Promise<UserCredential | void>} A promise containing the user credential, or void if handled.
*/
export async function signInWithEmailAndPassword(
ui: FirebaseUI,
email: string,
password: string
): Promise<UserCredential> {
): Promise<UserCredential | void> {
try {
setPendingState(ui);
const credential = EmailAuthProvider.credential(email, password);

if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);

if (result) {
return handlePendingCredential(ui, result);
}
const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential);
if (upgrade.status === "upgraded") {
return handlePendingCredential(ui, upgrade.credential);
}
if (upgrade.status === "stopped") {
return;
}

const result = await _signInWithCredential(ui.auth, credential);
Expand All @@ -110,14 +151,14 @@ export async function signInWithEmailAndPassword(
* @param email - The email address for the new account.
* @param password - The password for the new account.
* @param displayName - Optional display name for the user.
* @returns {Promise<UserCredential>} A promise containing the user credential.
* @returns {Promise<UserCredential | void>} A promise containing the user credential, or void if handled.
*/
export async function createUserWithEmailAndPassword(
ui: FirebaseUI,
email: string,
password: string,
displayName?: string
): Promise<UserCredential> {
): Promise<UserCredential | void> {
try {
setPendingState(ui);
const credential = EmailAuthProvider.credential(email, password);
Expand All @@ -126,16 +167,16 @@ export async function createUserWithEmailAndPassword(
throw new FirebaseError("auth/display-name-required", getTranslation(ui, "errors", "displayNameRequired"));
}

if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);

if (result) {
if (hasBehavior(ui, "requireDisplayName")) {
await getBehavior(ui, "requireDisplayName")(ui, result.user, displayName!);
}

return handlePendingCredential(ui, result);
const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential);
if (upgrade.status === "upgraded") {
if (hasBehavior(ui, "requireDisplayName")) {
await getBehavior(ui, "requireDisplayName")(ui, upgrade.credential.user, displayName!);
}

return handlePendingCredential(ui, upgrade.credential);
}
if (upgrade.status === "stopped") {
return;
}

const result = await _createUserWithEmailAndPassword(ui.auth, email, password);
Expand Down Expand Up @@ -213,24 +254,23 @@ export async function verifyPhoneNumber(
* @param ui - The FirebaseUI instance.
* @param verificationId - The verification ID from the phone verification process.
* @param verificationCode - The verification code sent to the phone.
* @returns {Promise<UserCredential>} A promise containing the user credential.
* @returns {Promise<UserCredential | void>} A promise containing the user credential, or void if handled.
*/
export async function confirmPhoneNumber(
ui: FirebaseUI,
verificationId: string,
verificationCode: string
): Promise<UserCredential> {
): Promise<UserCredential | void> {
try {
setPendingState(ui);
const currentUser = ui.auth.currentUser;
const credential = PhoneAuthProvider.credential(verificationId, verificationCode);

if (currentUser?.isAnonymous && hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
const result = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);

if (result) {
return handlePendingCredential(ui, result);
}
const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential);
if (upgrade.status === "upgraded") {
return handlePendingCredential(ui, upgrade.credential);
}
if (upgrade.status === "stopped") {
return;
}

const result = await _signInWithCredential(ui.auth, credential);
Expand Down Expand Up @@ -294,9 +334,9 @@ export async function sendSignInLinkToEmail(ui: FirebaseUI, email: string): Prom
* @param ui - The FirebaseUI instance.
* @param email - The email address associated with the sign-in link.
* @param link - The sign-in link from the email.
* @returns {Promise<UserCredential>} A promise containing the user credential.
* @returns {Promise<UserCredential | void>} A promise containing the user credential, or void if handled.
*/
export async function signInWithEmailLink(ui: FirebaseUI, email: string, link: string): Promise<UserCredential> {
export async function signInWithEmailLink(ui: FirebaseUI, email: string, link: string): Promise<UserCredential | void> {
const credential = EmailAuthProvider.credentialWithLink(email, link);
return signInWithCredential(ui, credential);
}
Expand All @@ -308,19 +348,17 @@ export async function signInWithEmailLink(ui: FirebaseUI, email: string, link: s
*
* @param ui - The FirebaseUI instance.
* @param credential - The authentication credential to sign in with.
* @returns {Promise<UserCredential>} A promise containing the user credential.
* @returns {Promise<UserCredential | void>} A promise containing the user credential, or void if handled.
*/
export async function signInWithCredential(ui: FirebaseUI, credential: AuthCredential): Promise<UserCredential> {
export async function signInWithCredential(ui: FirebaseUI, credential: AuthCredential): Promise<UserCredential | void> {
try {
setPendingState(ui);
if (hasBehavior(ui, "autoUpgradeAnonymousCredential")) {
const userCredential = await getBehavior(ui, "autoUpgradeAnonymousCredential")(ui, credential);

// If they got here, they're either not anonymous or they've been linked.
// If the credential has been linked, we don't need to sign them in, so return early.
if (userCredential) {
return handlePendingCredential(ui, userCredential);
}
const upgrade = await attemptAnonymousCredentialUpgrade(ui, credential);
if (upgrade.status === "upgraded") {
return handlePendingCredential(ui, upgrade.credential);
}
if (upgrade.status === "stopped") {
return;
}

const result = await _signInWithCredential(ui.auth, credential);
Expand Down Expand Up @@ -377,19 +415,17 @@ export async function signInAnonymously(ui: FirebaseUI): Promise<UserCredential>
*
* @param ui - The FirebaseUI instance.
* @param provider - The authentication provider to sign in with.
* @returns {Promise<UserCredential | never>} A promise containing the user credential, or never if using redirect strategy.
* @returns {Promise<UserCredential | void>} A promise containing the user credential, or void if handled.
*/
export async function signInWithProvider(ui: FirebaseUI, provider: AuthProvider): Promise<UserCredential | never> {
export async function signInWithProvider(ui: FirebaseUI, provider: AuthProvider): Promise<UserCredential | void> {
try {
setPendingState(ui);
if (hasBehavior(ui, "autoUpgradeAnonymousProvider")) {
const credential = await getBehavior(ui, "autoUpgradeAnonymousProvider")(ui, provider);

// If we got here, the user is either not anonymous, or they have been linked
// via a popup, and the credential has been created.
if (credential) {
return handlePendingCredential(ui, credential);
}
const upgrade = await attemptAnonymousProviderUpgrade(ui, provider);
if (upgrade.status === "upgraded") {
return handlePendingCredential(ui, upgrade.credential);
}
if (upgrade.status === "stopped") {
return;
}

const strategy = getBehavior(ui, "providerSignInStrategy");
Expand Down Expand Up @@ -424,9 +460,9 @@ export async function completeEmailLinkSignIn(ui: FirebaseUI, currentUrl: string
const email = window.localStorage.getItem("emailForSignIn");
if (!email) return null;

// signInWithEmailLink handles behavior checks, credential creation, and error handling
// signInWithEmailLink handles behavior checks, credential creation, and error handling.
const result = await signInWithEmailLink(ui, email, currentUrl);
return handlePendingCredential(ui, result);
return result ?? null;
} finally {
window.localStorage.removeItem("emailForSignIn");
}
Expand Down
Loading
Loading