Firebase have a weird scenario that when someone sign in first with google provider then you will not be able to sign in with another provider as you can see in this question Firebase JS API auth - account-exists-with-different-credential.
Finally I achieve this following the 2 first answer of the above question, but have 1 question and 1 improvement.
The question is that why when I use async inner catchError it expect a normal action instead of an observable.
The improvement is that when I call the popup is there a way to hide it ? or avoid to call again SigninProvider action?(The reason is because the user will see 2 popups).
const resultProvider = await this.afAuth.auth.signInWithPopup(linkedProvider);
await resultProvider.user.linkWithCredential(error.credential);
Full effect
@Effect()
signInProvider: Observable<Action> = this.actions$.pipe(
ofType(AuthActionTypes.SIGNIN_PROVIDER),
map((action: SigninProvider) => action.payload),
switchMap((provider) => {
let result: any;
let provi = provider;
if (provider === 'google') {
result = this.firebaseAuthService.signInGoogleLogin();
} else if (provider === 'facebook') {
result = this.firebaseAuthService.signInFacebookLogin();
} else if (provider === 'twitter') {
result = this.firebaseAuthService.signInTwitterLogin();
}
const resultAction: Observable<Action> = result.pipe(
switchMap((userSignIn) => {
console.log("TCL: AuthEffects -> userSignIn", userSignIn)
return combineLatest(this.afAuth.authState.pipe(take(1)), of(userSignIn));
}),
switchMap(([user, userFromProvider]) => {
console.log("TCL: AuthEffects -> user", user)
const userEmail = user.email ? user.email : userFromProvider.additionalUserInfo.profile.email;
const userDto: CreateUserProviderDto = {
uid: user.uid,
fullName: user.displayName,
username: userEmail,
email: userEmail,
terms: true
};
return combineLatest(of(user), from(user.getIdToken()), this.usersService.usersSignInProviderPost(userDto));
}),
map(([user, token, userFromBackend]) => {
const currentUser: CurrentUser = {
token: token, refreshToken: user.refreshToken, user: {
_id: userFromBackend._id,
uid: userFromBackend.uid,
email: userFromBackend.email,
profile: userFromBackend.profile,
fullName: userFromBackend.fullName,
username: userFromBackend.username
}
};
this.localStorageService
.setItem(AUTH_KEY, {
token: token,
refreshToken: user.refreshToken,
isAuthenticated: true,
user: currentUser.user
});
this.ngZone.run(() => {
this.router.navigate(['/']);
});
return new SigninProviderSuccess(currentUser);
}),
catchError(async (error) => {
let displayMessage;
if (error.email && error.credential && error.code === 'auth/account-exists-with-different-credential') {
const providers = await this.afAuth.auth.fetchSignInMethodsForEmail(error.email)
const firstPopupProviderMethod =
providers.find(p => this.firebaseAuthService.supportedPopupSignInMethods.includes(p));
// Test: Could this happen with email link then trying social provider?
if (!firstPopupProviderMethod) {
throw new Error(`Your account is linked to a provider that isn't supported.`);
}
const linkedProvider = this.firebaseAuthService.getProvider(firstPopupProviderMethod);
linkedProvider.setCustomParameters({ login_hint: error.email });
const resultProvider = await this.afAuth.auth.signInWithPopup(linkedProvider);
await resultProvider.user.linkWithCredential(error.credential);
return new SigninProvider(provi);
} else if (typeof error === 'object'
&& error !== null
&& error.hasOwnProperty('code')
&& error.hasOwnProperty('message')) {
const errorNoTypes: any = { ...error };
displayMessage = `${errorNoTypes.code} - ${error.message}`;
this.toastrService.danger('Try it again!', displayMessage, {
duration: 8000
});
}
return new RetrieveError(error);
}),
);
return resultAction;
})
);