diff --git a/FirebaseAuthUI/FUIAuth.h b/FirebaseAuthUI/FUIAuth.h index 8fa95b3ff2a..80e6f01f298 100644 --- a/FirebaseAuthUI/FUIAuth.h +++ b/FirebaseAuthUI/FUIAuth.h @@ -190,6 +190,12 @@ __attribute__((deprecated("Instead use authUI:didSignInWithAuthDataResult:error: */ @property(nonatomic, copy, nullable) NSURL *TOSURL; +/** @property shouldAutoUpgradeAnonymousUsers + @brief Whether to enable auto upgrading of anonymous accounts, defaults to NO. + */ +@property(nonatomic, assign, getter=shouldAutoUpgradeAnonymousUsers) BOOL + shouldAutoUpgradeAnonymousUsers; + /** @property delegate @brief A delegate that receives callbacks or provides custom UI for @c FUIAuth. */ diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index af789c66ec5..63245d1e77a 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -174,37 +174,78 @@ - (void)signInWithProviderUI:(id)providerUI return; } - [self.auth signInAndRetrieveDataWithCredential:credential - completion:^(FIRAuthDataResult *_Nullable authResult, - NSError *_Nullable error) { - if (error && error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) { - NSString *email = error.userInfo[kErrorUserInfoEmailKey]; - [self handleAccountLinkingForEmail:email - newCredential:credential - presentingViewController:presentingViewController - singInResult:result]; - return; + // Block to complete sign-in + void (^completeSignInBlock)(FIRAuthDataResult *, NSError *) = ^(FIRAuthDataResult *authResult, + NSError *error) { + if (result) { + result(authResult.user, nil); } - - if (error) { - if (result) { - result(nil, error); - } - [self invokeResultCallbackWithAuthDataResult:nil error:error]; + // Hide Auth Picker Controller which was presented modally. + if (isAuthPickerShown && presentingViewController.presentingViewController) { + [presentingViewController dismissViewControllerAnimated:YES completion:^{ + [self invokeResultCallbackWithAuthDataResult:authResult error:error]; + }]; } else { - if (result) { - result(authResult.user, nil); + [self invokeResultCallbackWithAuthDataResult:authResult error:error]; + } + }; + + // Check for the presence of an anonymous user and whether automatic upgrade is enabled. + if (_auth.currentUser.isAnonymous && [FUIAuth defaultAuthUI].shouldAutoUpgradeAnonymousUsers) { + [_auth.currentUser + linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError * _Nullable error) { + if (error) { + // Check for "credential in use" conflict error and handle appropriately. + if (error.code == FIRAuthErrorCodeCredentialAlreadyInUse) { + NSError *mergeError; + FIRAuthCredential *newCredential = credential; + // Check for and handle special case for Phone Auth Provider. + if (providerUI.providerID == FIRPhoneAuthProviderID) { + // Obtain temporary Phone Auth provider. + newCredential = error.userInfo[FIRAuthUpdatedCredentialKey]; + } + NSDictionary *userInfo = @{ + FUIAuthCredentialKey : newCredential, + }; + mergeError = [NSError errorWithDomain:FUIAuthErrorDomain + code:FUIAuthErrorCodeMergeConflict + userInfo:userInfo]; + result(nil, mergeError); + completeSignInBlock(authResult, mergeError); + } else { + if (!isAuthPickerShown || error.code != FUIAuthErrorCodeUserCancelledSignIn) { + [self invokeResultCallbackWithAuthDataResult:nil error:error]; + } + if (result) { + result(nil, error); + } + } } - // Hide Auth Picker Controller which was presented modally. - if (isAuthPickerShown && presentingViewController.presentingViewController) { - [presentingViewController dismissViewControllerAnimated:YES completion:^{ - [self invokeResultCallbackWithAuthDataResult:authResult error:nil]; - }]; - } else { - [self invokeResultCallbackWithAuthDataResult:authResult error:nil]; + }]; + } else { + [self.auth signInAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error && error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) { + NSString *email = error.userInfo[kErrorUserInfoEmailKey]; + [self handleAccountLinkingForEmail:email + newCredential:credential + presentingViewController:presentingViewController + singInResult:result]; + return; } - } - }]; + if (error) { + if (result) { + result(nil, error); + } + [self invokeResultCallbackWithAuthDataResult:nil error:error]; + return; + } + completeSignInBlock(authResult, nil); + }]; + } }]; } diff --git a/FirebaseAuthUI/FUIAuthErrors.h b/FirebaseAuthUI/FUIAuthErrors.h index bee0bb225a5..87cfccf6e0d 100644 --- a/FirebaseAuthUI/FUIAuthErrors.h +++ b/FirebaseAuthUI/FUIAuthErrors.h @@ -28,6 +28,12 @@ extern NSString *const FUIAuthErrorDomain; */ extern NSString *const FUIAuthErrorUserInfoProviderIDKey; +/** @bar FUIAuthCredentialKey + @brief The key used to obtain the credential stored within the userInfo dictionary of the + error, if availalble. + */ +extern NSString *const FUIAuthCredentialKey; + /** @var FUIAuthErrorCode @brief Error codes used by FUIAuth. */ @@ -50,6 +56,13 @@ typedef NS_ENUM(NSUInteger, FUIAuthErrorCode) { key @c FUIAuthErrorUserInfoProviderIDKey). */ FUIAuthErrorCodeCantFindProvider = 3, + + /** @var FUIAuthErrorCodeMergeConflict + @brief Indicates that a merge conflict occurred while trying to automatically upgrade an + anonymous user. The non-anonymous credential can be obtained from the userInfo dictionary + of the corresponding NSError using the @c FUIAuthCredentialKey. + */ + FUIAuthErrorCodeMergeConflict = 4, }; NS_ASSUME_NONNULL_END diff --git a/FirebaseAuthUI/FUIAuthErrors.m b/FirebaseAuthUI/FUIAuthErrors.m index 90cc468826a..dc7df99a40d 100644 --- a/FirebaseAuthUI/FUIAuthErrors.m +++ b/FirebaseAuthUI/FUIAuthErrors.m @@ -19,3 +19,5 @@ NSString *const FUIAuthErrorDomain = @"FUIAuthErrorDomain"; NSString *const FUIAuthErrorUserInfoProviderIDKey = @"FUIAuthErrorUserInfoProviderIDKey"; + +NSString *const FUIAuthCredentialKey = @"FUIAuthCredentialKey"; diff --git a/FirebasePhoneAuthUI/FUIPhoneVerificationViewController.m b/FirebasePhoneAuthUI/FUIPhoneVerificationViewController.m index 2da2deb485d..8d370e6cee5 100644 --- a/FirebasePhoneAuthUI/FUIPhoneVerificationViewController.m +++ b/FirebasePhoneAuthUI/FUIPhoneVerificationViewController.m @@ -187,7 +187,9 @@ - (void)onNext:(NSString *)verificationCode { result:^(FIRUser *_Nullable user, NSError *_Nullable error) { [self decrementActivity]; self.navigationItem.rightBarButtonItem.enabled = YES; - if (!error || error.code == FUIAuthErrorCodeUserCancelledSignIn) { + if (!error || + error.code == FUIAuthErrorCodeUserCancelledSignIn || + error.code == FUIAuthErrorCodeMergeConflict) { [self.navigationController dismissViewControllerAnimated:YES completion:nil]; } else { UIAlertController *alertController = [FUIPhoneAuth alertControllerForError:error diff --git a/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m b/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m index db90e18dc8f..13a05264ee6 100644 --- a/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m +++ b/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m @@ -148,8 +148,18 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == kSectionsAnonymousSignIn && indexPath.row == 0) { - if (_authUI.auth.currentUser.isAnonymous) { - [self showAlertWithTitlte:@"" message:@"Already signed in anonymously"]; + FIRUser *currentUser = self.authUI.auth.currentUser; + if (currentUser.isAnonymous) { + // If the user is anonymous, delete the user to avoid dangling anonymous users. + if (currentUser.isAnonymous) { + [currentUser deleteWithCompletion:^(NSError * _Nullable error) { + if (error) { + [self showAlertWithTitlte:@"Error" message:error.localizedDescription]; + return; + } + [self showAlertWithTitlte:@"" message:@"Anonymous user deleted"]; + }]; + } [tableView deselectRowAtIndexPath:indexPath animated:NO]; return; } @@ -179,12 +189,15 @@ - (void)updateUI:(FIRAuth * _Nonnull) auth withUser:(FIRUser *_Nullable) user { self.cellUID.textLabel.text = user.uid; // If the user is anonymous, delete the user to avoid dangling anonymous users. - if (self.authUI.auth.currentUser.isAnonymous) { - self.buttonAuthorization.title = @"Delete Anonymous User"; - } else { + if (auth.currentUser.isAnonymous) { + [_anonymousSignIn.textLabel setText:@"Delete Anonymous User"]; + } + else { + [_anonymousSignIn.textLabel setText:@"Sign In Anonymously"]; self.buttonAuthorization.title = @"Sign Out"; } } else { + [_anonymousSignIn.textLabel setText:@"Sign In Anonymously"]; self.cellSignIn.textLabel.text = @"Not signed-in"; self.cellName.textLabel.text = @""; self.cellEmail.textLabel.text = @""; @@ -215,8 +228,8 @@ - (IBAction)onAuthUIDelegateChanged:(UISwitch *)sender { } - (IBAction)onAuthorization:(id)sender { - if (!self.auth.currentUser) { - + if (!_auth.currentUser || _auth.currentUser.isAnonymous) { + FUIAuth.defaultAuthUI.shouldAutoUpgradeAnonymousUsers = YES; _authUI.providers = [self getListOfIDPs]; _authUI.signInWithEmailHidden = ![self isEmailEnabled]; @@ -247,13 +260,31 @@ - (void)authUI:(FUIAuth *)authUI if (error) { if (error.code == FUIAuthErrorCodeUserCancelledSignIn) { [self showAlertWithTitlte:@"Error" message:error.localizedDescription]; - } else { - NSError *detailedError = error.userInfo[NSUnderlyingErrorKey]; - if (!detailedError) { - detailedError = error; - } - NSLog(@"ERROR: %@", detailedError.localizedDescription); + return; + } + if (error.code == FUIAuthErrorCodeMergeConflict) { + FIRAuthCredential *credential = error.userInfo[FUIAuthCredentialKey]; + NSString *anonymousUserID = authUI.auth.currentUser.uid; + NSString *messsage = [NSString stringWithFormat:@"A merge conflict occurred. The old user ID " + "was: %@. You are now signed in with the following credential type: %@", anonymousUserID, + [credential.provider uppercaseString]]; + [self showAlertWithTitlte:@"Merge Conflict" message:messsage]; + NSLog(@"%@", messsage); + [[FUIAuth defaultAuthUI].auth + signInAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + NSLog(@"%@",error.description); + } + }]; + return; + } + NSError *detailedError = error.userInfo[NSUnderlyingErrorKey]; + if (!detailedError) { + detailedError = error; } + NSLog(@"ERROR: %@", detailedError.localizedDescription); } } @@ -279,21 +310,9 @@ - (NSString *)getAllIdTokens { - (void)signOut { NSError *error; - FIRUser *currentUser = self.authUI.auth.currentUser; - // If the user is anonymous, delete the user to avoid dangling anonymous users. - if (currentUser.isAnonymous) { - [currentUser deleteWithCompletion:^(NSError * _Nullable error) { - if (error) { - [self showAlertWithTitlte:@"Error" message:error.localizedDescription]; - return; - } - [self showAlertWithTitlte:@"" message:@"Anonymous user deleted"]; - }]; - } else { - [self.authUI signOutWithError:&error]; - if (error) { - [self showAlertWithTitlte:@"Error" message:error.localizedDescription]; - } + [self.authUI signOutWithError:&error]; + if (error) { + [self showAlertWithTitlte:@"Error" message:error.localizedDescription]; } }