Skip to content

Commit dcbefb2

Browse files
committed
feat: Add support for handling existing credentials with different providers
1 parent b5e3231 commit dcbefb2

File tree

3 files changed

+75
-23
lines changed

3 files changed

+75
-23
lines changed

packages/firebaseui-core/src/auth.ts

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,47 @@ import {
2121
import { FirebaseUIError } from './errors';
2222
import { type TranslationsConfig } from './translations';
2323

24-
function handleFirebaseError(error: any, translations?: TranslationsConfig, language?: string): never {
24+
async function handleFirebaseError(
25+
error: any,
26+
opts?: { language?: string; translations?: TranslationsConfig; enableHandleExistingCredential?: boolean }
27+
): Promise<never | UserCredential> {
28+
if (error?.code === 'auth/account-exists-with-different-credential' && opts?.enableHandleExistingCredential) {
29+
if (error.credential) {
30+
window.sessionStorage.setItem('pendingCred', JSON.stringify(error.credential));
31+
}
32+
33+
throw new FirebaseUIError(
34+
{
35+
code: 'auth/account-exists-with-different-credential',
36+
customData: {
37+
email: error.customData?.email,
38+
},
39+
},
40+
opts?.translations,
41+
opts?.language
42+
);
43+
}
44+
2545
// TODO: Debug why instanceof FirebaseError is not working
2646
if (error?.name === 'FirebaseError') {
27-
throw new FirebaseUIError(error, translations, language);
47+
throw new FirebaseUIError(error, opts?.translations, opts?.language);
48+
}
49+
throw new FirebaseUIError({ code: 'unknown' }, opts?.translations, opts?.language);
50+
}
51+
52+
async function handlePendingCredential(user: UserCredential): Promise<UserCredential> {
53+
const pendingCredString = window.sessionStorage.getItem('pendingCred');
54+
if (!pendingCredString) return user;
55+
56+
try {
57+
const pendingCred = JSON.parse(pendingCredString);
58+
const result = await linkWithCredential(user.user, pendingCred);
59+
window.sessionStorage.removeItem('pendingCred');
60+
return result;
61+
} catch (error) {
62+
window.sessionStorage.removeItem('pendingCred');
63+
return user;
2864
}
29-
throw new FirebaseUIError({ code: 'unknown' }, translations, language);
3065
}
3166

3267
export async function fuiSignInWithEmailAndPassword(
@@ -37,19 +72,22 @@ export async function fuiSignInWithEmailAndPassword(
3772
language?: string;
3873
translations?: TranslationsConfig;
3974
enableAutoUpgradeAnonymous?: boolean;
75+
enableHandleExistingCredential?: boolean;
4076
}
4177
): Promise<UserCredential> {
4278
try {
4379
const currentUser = auth.currentUser;
4480
const credential = EmailAuthProvider.credential(email, password);
4581

4682
if (currentUser?.isAnonymous && opts?.enableAutoUpgradeAnonymous) {
47-
return await linkWithCredential(currentUser, credential);
83+
const result = await linkWithCredential(currentUser, credential);
84+
return handlePendingCredential(result);
4885
}
4986

50-
return await signInWithCredential(auth, credential);
87+
const result = await signInWithCredential(auth, credential);
88+
return handlePendingCredential(result);
5189
} catch (error) {
52-
handleFirebaseError(error, opts?.translations, opts?.language);
90+
return await handleFirebaseError(error, opts);
5391
}
5492
}
5593

@@ -61,19 +99,22 @@ export async function fuiCreateUserWithEmailAndPassword(
6199
language?: string;
62100
translations?: TranslationsConfig;
63101
enableAutoUpgradeAnonymous?: boolean;
102+
enableHandleExistingCredential?: boolean;
64103
}
65104
): Promise<UserCredential> {
66105
try {
67106
const currentUser = auth.currentUser;
68107
const credential = EmailAuthProvider.credential(email, password);
69108

70109
if (currentUser?.isAnonymous && opts?.enableAutoUpgradeAnonymous) {
71-
return await linkWithCredential(currentUser, credential);
110+
const result = await linkWithCredential(currentUser, credential);
111+
return handlePendingCredential(result);
72112
}
73113

74-
return await createUserWithEmailAndPassword(auth, email, password);
114+
const result = await createUserWithEmailAndPassword(auth, email, password);
115+
return handlePendingCredential(result);
75116
} catch (error) {
76-
handleFirebaseError(error, opts?.translations, opts?.language);
117+
return await handleFirebaseError(error, opts);
77118
}
78119
}
79120

@@ -89,7 +130,7 @@ export async function fuiSignInWithPhoneNumber(
89130
try {
90131
return await signInWithPhoneNumber(auth, phoneNumber, recaptchaVerifier);
91132
} catch (error) {
92-
handleFirebaseError(error, opts?.translations, opts?.language);
133+
return (await handleFirebaseError(error, opts)) as never;
93134
}
94135
}
95136

@@ -100,6 +141,7 @@ export async function fuiConfirmPhoneNumber(
100141
language?: string;
101142
translations?: TranslationsConfig;
102143
enableAutoUpgradeAnonymous?: boolean;
144+
enableHandleExistingCredential?: boolean;
103145
}
104146
): Promise<UserCredential> {
105147
try {
@@ -109,12 +151,13 @@ export async function fuiConfirmPhoneNumber(
109151

110152
if (currentUser?.isAnonymous && opts?.enableAutoUpgradeAnonymous) {
111153
const result = await linkWithCredential(currentUser, credential);
112-
return result;
154+
return handlePendingCredential(result);
113155
}
114156

115-
return await signInWithCredential(auth, credential);
157+
const result = await signInWithCredential(auth, credential);
158+
return handlePendingCredential(result);
116159
} catch (error) {
117-
handleFirebaseError(error, opts?.translations, opts?.language);
160+
return await handleFirebaseError(error, opts);
118161
}
119162
}
120163

@@ -129,7 +172,7 @@ export async function fuiSendPasswordResetEmail(
129172
try {
130173
await sendPasswordResetEmail(auth, email);
131174
} catch (error) {
132-
handleFirebaseError(error, opts?.translations, opts?.language);
175+
return (await handleFirebaseError(error, opts)) as never;
133176
}
134177
}
135178

@@ -156,7 +199,7 @@ export async function fuiSendSignInLinkToEmail(
156199
await sendSignInLinkToEmail(auth, email, actionCodeSettings);
157200
window.localStorage.setItem('emailForSignIn', email);
158201
} catch (error) {
159-
handleFirebaseError(error, opts?.translations, opts?.language);
202+
return (await handleFirebaseError(error, opts)) as never;
160203
}
161204
}
162205

@@ -172,6 +215,7 @@ export async function fuiSignInWithEmailLink(
172215
language?: string;
173216
translations?: TranslationsConfig;
174217
enableAutoUpgradeAnonymous?: boolean;
218+
enableHandleExistingCredential?: boolean;
175219
}
176220
): Promise<UserCredential> {
177221
try {
@@ -182,14 +226,14 @@ export async function fuiSignInWithEmailLink(
182226
if (currentUser?.isAnonymous && isAnonymousUpgrade && opts?.enableAutoUpgradeAnonymous) {
183227
const result = await linkWithCredential(currentUser, credential);
184228
window.localStorage.removeItem('emailLinkAnonymousUpgrade');
185-
return result;
229+
return handlePendingCredential(result);
186230
}
187231

188232
const result = await signInWithCredential(auth, credential);
189233
window.localStorage.removeItem('emailLinkAnonymousUpgrade');
190-
return result;
234+
return handlePendingCredential(result);
191235
} catch (error) {
192-
handleFirebaseError(error, opts?.translations, opts?.language);
236+
return await handleFirebaseError(error, opts);
193237
}
194238
}
195239

@@ -201,9 +245,10 @@ export async function fuiSignInAnonymously(
201245
}
202246
): Promise<UserCredential> {
203247
try {
204-
return await signInAnonymously(auth);
248+
const result = await signInAnonymously(auth);
249+
return handlePendingCredential(result);
205250
} catch (error) {
206-
handleFirebaseError(error, opts?.translations, opts?.language);
251+
return await handleFirebaseError(error, opts);
207252
}
208253
}
209254

@@ -214,6 +259,7 @@ export async function fuiSignInWithOAuth(
214259
language?: string;
215260
translations?: TranslationsConfig;
216261
enableAutoUpgradeAnonymous?: boolean;
262+
enableHandleExistingCredential?: boolean;
217263
}
218264
): Promise<void> {
219265
try {
@@ -225,7 +271,7 @@ export async function fuiSignInWithOAuth(
225271
await signInWithRedirect(auth, provider);
226272
}
227273
} catch (error) {
228-
handleFirebaseError(error, opts?.translations, opts?.language);
274+
return (await handleFirebaseError(error, opts)) as never;
229275
}
230276
}
231277

@@ -236,6 +282,7 @@ export async function fuiCompleteEmailLinkSignIn(
236282
language?: string;
237283
translations?: TranslationsConfig;
238284
enableAutoUpgradeAnonymous?: boolean;
285+
enableHandleExistingCredential?: boolean;
239286
}
240287
): Promise<UserCredential | null> {
241288
try {
@@ -248,8 +295,8 @@ export async function fuiCompleteEmailLinkSignIn(
248295

249296
const result = await fuiSignInWithEmailLink(auth, email, currentUrl, opts);
250297
window.localStorage.removeItem('emailForSignIn');
251-
return result;
298+
return handlePendingCredential(result);
252299
} catch (error) {
253-
handleFirebaseError(error, opts?.translations, opts?.language);
300+
return await handleFirebaseError(error, opts);
254301
}
255302
}

packages/firebaseui-core/src/translations.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const ERROR_CODE_MAP: Record<string, ErrorKey> = {
2626
'auth/requires-recent-login': 'requiresRecentLogin',
2727
'auth/provider-already-linked': 'providerAlreadyLinked',
2828
'auth/invalid-verification-code': 'invalidVerificationCode',
29+
'auth/account-exists-with-different-credential': 'accountExistsWithDifferentCredential',
2930
};
3031

3132
type TranslationCategory = keyof Required<TranslationStrings>;
@@ -82,6 +83,8 @@ export const defaultTranslations: Record<'en', TranslationStrings> = {
8283
invalidVerificationCode: 'Invalid verification code. Please try again',
8384
unknownError: 'An unexpected error occurred',
8485
popupClosed: 'The sign-in popup was closed. Please try again.',
86+
accountExistsWithDifferentCredential:
87+
'An account already exists with this email. Please sign in with the original provider.',
8588
},
8689
messages: {
8790
passwordResetEmailSent: 'Password reset email sent successfully',

packages/firebaseui-core/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type TranslationStrings = {
2525
invalidVerificationCode?: string;
2626
unknownError?: string;
2727
popupClosed?: string;
28+
accountExistsWithDifferentCredential?: string;
2829
};
2930
messages?: {
3031
passwordResetEmailSent?: string;
@@ -73,6 +74,7 @@ export interface FUIConfig {
7374
language?: string;
7475
enableAutoAnonymousLogin?: boolean;
7576
enableAutoUpgradeAnonymous?: boolean;
77+
enableHandleExistingCredential?: boolean;
7678
translations?: Partial<Record<string, Partial<TranslationStrings>>>;
7779
tosUrl?: string;
7880
privacyPolicyUrl?: string;

0 commit comments

Comments
 (0)