From f4d5b2478a60e692029346562edb81e74a6508a0 Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Fri, 2 Feb 2018 07:55:03 -0800 Subject: [PATCH 1/9] Anonymous user upgrade federated (#401) * Add anonymous sign in upgrade * Returns merge error in the correct place. * Adds comments and minor changes * Adds correct error handling for linking errors * Addresses comments Addresses comments Minor enhances Fixes bug where wrong error is returned --- FirebaseAuthUI/FUIAuth.h | 6 + FirebaseAuthUI/FUIAuth.m | 103 ++++++++++++------ FirebaseAuthUI/FUIAuthErrors.h | 13 +++ FirebaseAuthUI/FUIAuthErrors.m | 2 + .../FUIPhoneVerificationViewController.m | 4 +- .../Samples/Auth/FUIAuthViewController.m | 75 ++++++++----- 6 files changed, 143 insertions(+), 60 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.h b/FirebaseAuthUI/FUIAuth.h index 6304b269326..2afb8af7a0f 100644 --- a/FirebaseAuthUI/FUIAuth.h +++ b/FirebaseAuthUI/FUIAuth.h @@ -201,6 +201,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 privacyPolicyURL @brief The URL of your app's Privacy Policy. If not nil, a privacy policy notice is displayed on the initial sign-in screen and potentially the phone number auth and diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index c0b8cb85bda..3ed3081c500 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -169,10 +169,10 @@ - (void)signInWithProviderUI:(id)providerUI // Sign out first to make sure sign in starts with a clean state. [providerUI signOut]; [providerUI signInWithDefaultValue:defaultValue - presentingViewController:presentingViewController - completion:^(FIRAuthCredential *_Nullable credential, - NSError *_Nullable error, - _Nullable FIRAuthResultCallback result) { + presentingViewController:presentingViewController + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error, + _Nullable FIRAuthResultCallback result) { BOOL isAuthPickerShown = [presentingViewController isKindOfClass:[FUIAuthPickerViewController class]]; if (error) { @@ -185,37 +185,78 @@ - (void)signInWithProviderUI:(id)providerUI return; } - [self.auth signInAndRetrieveDataWithCredential:credential - completion:^(FIRAuthDataResult *_Nullable authResult, - NSError *_Nullable error) { - if (error.code == FIRAuthErrorCodeAccountExistsWithDifferentCredential) { - NSString *email = error.userInfo[kErrorUserInfoEmailKey]; - [self handleAccountLinkingForEmail:email - newCredential:credential - presentingViewController:presentingViewController - signInResult: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 b74ebf72aeb..fa8f7c45df4 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 77dc81def66..837af18f0c3 100644 --- a/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m +++ b/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m @@ -150,8 +150,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; } @@ -181,12 +191,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 = @""; @@ -217,8 +230,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]; @@ -249,13 +262,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); } } @@ -281,21 +312,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]; } } From 4f1570cf5aa4ca625943fc6fee21c5393cc17782 Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Fri, 9 Feb 2018 10:50:24 -0800 Subject: [PATCH 2/9] anonymous upgrade for email password accounts (#405) * anonymous upgrade for email password accounts * addresses comments --- FirebaseAuthUI/FUIAuth.m | 7 +- FirebaseAuthUI/FUIAuthErrorUtils.h | 7 ++ FirebaseAuthUI/FUIAuthErrorUtils.m | 4 + .../FUIPasswordSignInViewController.m | 74 +++++++++++++------ .../FUIPasswordSignUpViewController.m | 64 +++++++++++----- .../Samples/Auth/FUIAuthViewController.m | 14 ++-- 6 files changed, 119 insertions(+), 51 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index 3ed3081c500..dcb25bf98d5 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -202,8 +202,9 @@ - (void)signInWithProviderUI:(id)providerUI }; // Check for the presence of an anonymous user and whether automatic upgrade is enabled. - if (_auth.currentUser.isAnonymous && [FUIAuth defaultAuthUI].shouldAutoUpgradeAnonymousUsers) { - [_auth.currentUser + if (self.auth.currentUser.isAnonymous && + [FUIAuth defaultAuthUI].shouldAutoUpgradeAnonymousUsers) { + [self.auth.currentUser linkAndRetrieveDataWithCredential:credential completion:^(FIRAuthDataResult *_Nullable authResult, NSError * _Nullable error) { @@ -214,7 +215,7 @@ - (void)signInWithProviderUI:(id)providerUI FIRAuthCredential *newCredential = credential; // Check for and handle special case for Phone Auth Provider. if (providerUI.providerID == FIRPhoneAuthProviderID) { - // Obtain temporary Phone Auth provider. + // Obtain temporary Phone Auth credential. newCredential = error.userInfo[FIRAuthUpdatedCredentialKey]; } NSDictionary *userInfo = @{ diff --git a/FirebaseAuthUI/FUIAuthErrorUtils.h b/FirebaseAuthUI/FUIAuthErrorUtils.h index 43395d810bd..58a85d7ad6c 100644 --- a/FirebaseAuthUI/FUIAuthErrorUtils.h +++ b/FirebaseAuthUI/FUIAuthErrorUtils.h @@ -36,6 +36,13 @@ NS_ASSUME_NONNULL_BEGIN */ + (NSError *)userCancelledSignInError; +/** @fn userCancelledSignIn + @brief Constructs an @c NSError with the @c FUIAuthErrorCodeMergeConflict code. + @param userInfo The userInfo dictionary to add to the NSError object. + @return The merge conflict error. + */ ++ (NSError *)mergeConflictErrorWithUserInfo:(NSDictionary *)userInfo; + /** @fn providerErrorWithUnderlyingError:providerID: @brief Constructs an @c NSError with the @c FUIAuthErrorCodeProviderError code and a populated @c NSUnderlyingErrorKey and @c FUIAuthErrorUserInfoProviderIDKey in the diff --git a/FirebaseAuthUI/FUIAuthErrorUtils.m b/FirebaseAuthUI/FUIAuthErrorUtils.m index 180ff2ca9b8..80b17b2f5bf 100644 --- a/FirebaseAuthUI/FUIAuthErrorUtils.m +++ b/FirebaseAuthUI/FUIAuthErrorUtils.m @@ -26,6 +26,10 @@ + (NSError *)userCancelledSignInError { return [self errorWithCode:FUIAuthErrorCodeUserCancelledSignIn userInfo:nil]; } ++ (NSError *)mergeConflictErrorWithUserInfo:(NSDictionary *)userInfo { + return [self errorWithCode:FUIAuthErrorCodeMergeConflict userInfo:userInfo]; +} + + (NSError *)providerErrorWithUnderlyingError:(NSError *)underlyingError providerID:(NSString *)providerID { return [self errorWithCode:FUIAuthErrorCodeProviderError diff --git a/FirebaseAuthUI/FUIPasswordSignInViewController.m b/FirebaseAuthUI/FUIPasswordSignInViewController.m index d5272f62592..be6f42aef14 100644 --- a/FirebaseAuthUI/FUIPasswordSignInViewController.m +++ b/FirebaseAuthUI/FUIPasswordSignInViewController.m @@ -18,10 +18,12 @@ #import #import "FUIAuthBaseViewController_Internal.h" +#import "FUIAuthErrorUtils.h" #import "FUIAuthStrings.h" #import "FUIAuthTableViewCell.h" #import "FUIAuthUtils.h" #import "FUIAuth_Internal.h" +#import "FUIAuthErrors.h" #import "FUIPasswordRecoveryViewController.h" /** @var kCellReuseIdentifier @@ -127,35 +129,59 @@ - (void)signInWithDefaultValue:(NSString *)email andPassword:(NSString *)passwor } [self incrementActivity]; - FIRAuthCredential *credential = [FIREmailAuthProvider credentialWithEmail:email password:password]; - [self.auth signInAndRetrieveDataWithCredential:credential - completion:^(FIRAuthDataResult *_Nullable authResult, - NSError *_Nullable error) { - [self decrementActivity]; - - if (error) { - switch (error.code) { - case FIRAuthErrorCodeWrongPassword: - [self showAlertWithMessage:FUILocalizedString(kStr_WrongPasswordError)]; - return; - case FIRAuthErrorCodeUserNotFound: - [self showAlertWithMessage:FUILocalizedString(kStr_UserNotFoundError)]; - return; - case FIRAuthErrorCodeUserDisabled: - [self showAlertWithMessage:FUILocalizedString(kStr_AccountDisabledError)]; - return; - case FIRAuthErrorCodeTooManyRequests: - [self showAlertWithMessage:FUILocalizedString(kStr_SignInTooManyTimesError)]; + + void (^completeSignInBlock)(FIRAuthDataResult *, NSError *) = ^(FIRAuthDataResult *authResult, + NSError *error) { + [self decrementActivity]; + + if (error) { + switch (error.code) { + case FIRAuthErrorCodeWrongPassword: + [self showAlertWithMessage:FUILocalizedString(kStr_WrongPasswordError)]; + return; + case FIRAuthErrorCodeUserNotFound: + [self showAlertWithMessage:FUILocalizedString(kStr_UserNotFoundError)]; + return; + case FIRAuthErrorCodeUserDisabled: + [self showAlertWithMessage:FUILocalizedString(kStr_AccountDisabledError)]; + return; + case FIRAuthErrorCodeTooManyRequests: + [self showAlertWithMessage:FUILocalizedString(kStr_SignInTooManyTimesError)]; + return; + } + } + [self.navigationController dismissViewControllerAnimated:YES completion:^{ + [self.authUI invokeResultCallbackWithAuthDataResult:authResult error:error]; + }]; + }; + + // Check for the presence of an anonymous user and whether automatic upgrade is enabled. + if (self.auth.currentUser.isAnonymous && + [FUIAuth defaultAuthUI].shouldAutoUpgradeAnonymousUsers) { + + [self.auth.currentUser + linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + if (error.code == FIRAuthErrorCodeEmailAlreadyInUse) { + NSDictionary *userInfo = @{ FUIAuthCredentialKey : credential }; + NSError *mergeError = [FUIAuthErrorUtils mergeConflictErrorWithUserInfo:userInfo]; + [self.navigationController dismissViewControllerAnimated:YES completion:^{ + [self.authUI invokeResultCallbackWithAuthDataResult:authResult error:mergeError]; + }]; return; + } + completeSignInBlock(nil, error); + return; } - } - - [self dismissNavigationControllerAnimated:YES completion:^{ - [self.authUI invokeResultCallbackWithAuthDataResult:authResult error:error]; + completeSignInBlock(authResult, nil); }]; - }]; + } else { + [self.auth signInAndRetrieveDataWithCredential:credential completion:completeSignInBlock]; + } } - (void)signIn { diff --git a/FirebaseAuthUI/FUIPasswordSignUpViewController.m b/FirebaseAuthUI/FUIPasswordSignUpViewController.m index 749c309e81c..0a8d7b0b84a 100644 --- a/FirebaseAuthUI/FUIPasswordSignUpViewController.m +++ b/FirebaseAuthUI/FUIPasswordSignUpViewController.m @@ -180,29 +180,57 @@ - (void)signUpWithEmail:(NSString *)email [self incrementActivity]; - [self.auth createUserWithEmail:email - password:password - completion:^(FIRAuthDataResult *_Nullable authDataResult, - NSError *_Nullable error) { - if (error) { - [self decrementActivity]; - - [self finishSignUpWithAuthDataResult:nil error:error]; - return; - } - - FIRUserProfileChangeRequest *request = [authDataResult.user profileChangeRequest]; - request.displayName = username; - [request commitChangesWithCompletion:^(NSError *_Nullable error) { - [self decrementActivity]; - + // Check for the presence of an anonymous user and whether automatic upgrade is enabled. + if (self.auth.currentUser.isAnonymous && + [FUIAuth defaultAuthUI].shouldAutoUpgradeAnonymousUsers) { + FIRAuthCredential *credential = + [FIREmailAuthProvider credentialWithEmail:email password:password]; + [self.auth.currentUser + linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError * _Nullable error) { if (error) { + [self decrementActivity]; [self finishSignUpWithAuthDataResult:nil error:error]; return; } - [self finishSignUpWithAuthDataResult:authDataResult error:nil]; + FIRUserProfileChangeRequest *request = [authResult.user profileChangeRequest]; + request.displayName = username; + [request commitChangesWithCompletion:^(NSError *_Nullable error) { + [self decrementActivity]; + + if (error) { + [self finishSignUpWithAuthDataResult:nil error:error]; + return; + } + [self finishSignUpWithAuthDataResult:authResult error:nil]; + }]; }]; - }]; + } else { + [self.auth createUserAndRetrieveDataWithEmail:email + password:password + completion:^(FIRAuthDataResult *_Nullable authDataResult, + NSError *_Nullable error) { + if (error) { + [self decrementActivity]; + + [self finishSignUpWithAuthDataResult:nil error:error]; + return; + } + + FIRUserProfileChangeRequest *request = [authDataResult.user profileChangeRequest]; + request.displayName = username; + [request commitChangesWithCompletion:^(NSError *_Nullable error) { + [self decrementActivity]; + + if (error) { + [self finishSignUpWithAuthDataResult:nil error:error]; + return; + } + [self finishSignUpWithAuthDataResult:authDataResult error:nil]; + }]; + }]; + } } - (void)finishSignUpWithAuthDataResult:(nullable FIRAuthDataResult *)authDataResult diff --git a/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m b/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m index 837af18f0c3..867254e5bad 100644 --- a/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m +++ b/samples/objc/FirebaseUI-demo-objc/Samples/Auth/FUIAuthViewController.m @@ -266,19 +266,21 @@ - (void)authUI:(FUIAuth *)authUI } 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) { + [self showAlertWithTitlte:@"Sign-In error" message:error.description]; NSLog(@"%@",error.description); + return; } + 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); }]; return; } From e7cd44e7efffb867057b87e19e632bb0d58ac071 Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Thu, 15 Feb 2018 09:33:15 -0800 Subject: [PATCH 3/9] Adds anonymous upgrade for google account email conflict (#407) * Adds anonymous upgrade for Google email conflict * Addresses comments * Addresses comments. --- FirebaseAuthUI/FUIAuth.m | 105 +++++++++++++++++++---- FirebaseAuthUI/FUIAuthProvider.h | 7 +- FirebaseFacebookAuthUI/FUIFacebookAuth.m | 15 ++++ 3 files changed, 110 insertions(+), 17 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index dcb25bf98d5..e5d259cacb6 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -19,12 +19,14 @@ #import #import +#import #import #import #import "FUIAuthBaseViewController_Internal.h" #import "FUIAuthErrors.h" #import "FUIAuthPickerViewController.h" #import "FUIAuthStrings.h" +#import "FUIGoogleAuth.h" #import "FUIEmailEntryViewController.h" #import "FUIPasswordVerificationViewController.h" #import "FUIPasswordSignInViewController.h" @@ -184,7 +186,6 @@ - (void)signInWithProviderUI:(id)providerUI } return; } - // Block to complete sign-in void (^completeSignInBlock)(FIRAuthDataResult *, NSError *) = ^(FIRAuthDataResult *authResult, NSError *error) { @@ -211,7 +212,6 @@ - (void)signInWithProviderUI:(id)providerUI 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) { @@ -221,11 +221,40 @@ - (void)signInWithProviderUI:(id)providerUI NSDictionary *userInfo = @{ FUIAuthCredentialKey : newCredential, }; - mergeError = [NSError errorWithDomain:FUIAuthErrorDomain - code:FUIAuthErrorCodeMergeConflict - userInfo:userInfo]; - result(nil, mergeError); + NSError *mergeError = [NSError errorWithDomain:FUIAuthErrorDomain + code:FUIAuthErrorCodeMergeConflict + userInfo:userInfo]; completeSignInBlock(authResult, mergeError); + } else if (error.code == FIRAuthErrorCodeEmailAlreadyInUse) { + if ([providerUI respondsToSelector:@selector(email)]) { + // Link federated providers + [self signInWithEmailHint:[providerUI email] + presentingViewController:presentingViewController + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + completeSignInBlock(nil, error); + return; + } + [authResult.user linkAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult + *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + completeSignInBlock(nil, error); + return; + } + FIRAuthCredential *newCredential = credential; + NSDictionary *userInfo = @{ + FUIAuthCredentialKey : newCredential, + }; + NSError *mergeError = [NSError errorWithDomain:FUIAuthErrorDomain + code:FUIAuthErrorCodeMergeConflict + userInfo:userInfo]; + completeSignInBlock(authResult, mergeError); + }]; + }]; + } } else { if (!isAuthPickerShown || error.code != FUIAuthErrorCodeUserCancelledSignIn) { [self invokeResultCallbackWithAuthDataResult:nil error:error]; @@ -261,16 +290,60 @@ - (void)signInWithProviderUI:(id)providerUI }]; } -/** @fn handleAccountLinkingForEmail:newCredential:presentingViewController:signInResult - @brief Handles the account linking case after a user tries to sign-in which has a credential - with an email which is already used by a different account. - @param email The email address used by an existing account and and also the credential used in - the sign-in attempt. - @param newCredential The credential used in the lastest sign-in attempt. - @param presentingViewController The view controller used to present the UI. - @param result block which takes the result of this method as a parameter; a nullable - AuthResult indicating success or a nullable Error indicating failure. - */ +- (void)signInWithEmailHint:(NSString *)emailHint + presentingViewController:(UIViewController *)presentingViewController + completion:(FIRAuthDataResultCallback)completion { + NSString *kTempApp = @"tempApp"; + FIROptions *options = [FIROptions defaultOptions]; + // Create an new app instance in order to create a new auth instance. + if (![FIRApp appNamed:kTempApp]) { + [FIRApp configureWithName:kTempApp options:options]; + } + FIRApp *tempApp = [FIRApp appNamed:kTempApp]; + // Create a new auth instance in order to perform a successful sign-in without losing the + // currently signed in user on the default auth instance. + FIRAuth *auth = [FIRAuth authWithApp:tempApp]; + + [self.auth fetchProvidersForEmail:emailHint completion:^(NSArray *_Nullable providers, + NSError *_Nullable error) { + NSString *federatedProviderID = [self federatedAuthProviderFromProviders:providers]; + // Google case + if ([federatedProviderID isEqualToString:FIRGoogleAuthProviderID]) { + id googleProviderUI; + for (id provider in self.providers) { + if ([provider.providerID isEqualToString:FIRGoogleAuthProviderID]) { + googleProviderUI = provider; + break; + } + } + [googleProviderUI signOut]; + [googleProviderUI signInWithDefaultValue:emailHint + presentingViewController:presentingViewController + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error, + FIRAuthResultCallback _Nullable result) { + if (error) { + completion(nil, error); + return; + } + + [auth signInAndRetrieveDataWithCredential:credential completion:completion]; + }]; + } + }]; +} + +- (nullable NSString *)federatedAuthProviderFromProviders:(NSArray *) providers { + NSSet *providerSet = + [NSSet setWithArray:@[ FIRFacebookAuthProviderID, FIRGoogleAuthProviderID ]]; + for (NSString *provider in providers) { + if ( [providerSet containsObject:provider]) { + return provider; + } + } + return nil; +} + - (void)handleAccountLinkingForEmail:(NSString *)email newCredential:(FIRAuthCredential *)newCredential presentingViewController:(UIViewController *)presentingViewController diff --git a/FirebaseAuthUI/FUIAuthProvider.h b/FirebaseAuthUI/FUIAuthProvider.h index 0a2de5c1a49..4d2f3d2b55a 100644 --- a/FirebaseAuthUI/FUIAuthProvider.h +++ b/FirebaseAuthUI/FUIAuthProvider.h @@ -124,10 +124,15 @@ __attribute__((deprecated("This is deprecated API and will be removed in a futur @optional; /** @property idToken - @brief User Id Token obtained during sign in. Not all providers can return, thus it's optional + @brief User Id Token obtained during sign in. Not all providers can return, thus it's optional. */ @property(nonatomic, copy, readonly) NSString *idToken; +/** @fn email + @brief The email address associated with this provider, if any. + */ +- (NSString *)email; + /** @fn handleOpenURL: @brief May be used to help complete a sign-in flow which requires a callback from Safari. @param URL The URL which may be handled by the auth provider if an URL is expected. diff --git a/FirebaseFacebookAuthUI/FUIFacebookAuth.m b/FirebaseFacebookAuthUI/FUIFacebookAuth.m index 59e8f8447e6..86cbf299d19 100644 --- a/FirebaseFacebookAuthUI/FUIFacebookAuth.m +++ b/FirebaseFacebookAuthUI/FUIFacebookAuth.m @@ -61,6 +61,11 @@ @implementation FUIFacebookAuth { @brief The presenting view controller for interactive sign-in. */ UIViewController *_presentingViewController; + + /** @var _email + @brief The email address associated with this account. + */ + NSString *_email; } - (instancetype)initWithPermissions:(NSArray *)permissions { @@ -144,12 +149,22 @@ - (void)signInWithDefaultValue:(nullable NSString *)defaultValue NSError *newError = [FUIAuthErrorUtils userCancelledSignInError]; [self completeSignInFlowWithAccessToken:nil error:newError]; } else { + // Retrieve email. + [[[FBSDKGraphRequest alloc] initWithGraphPath:@"me" parameters:@{ @"fields" : @"email" }] + startWithCompletionHandler:^(FBSDKGraphRequestConnection *connection, id result, + NSError *error) { + self->_email = result[@"email"]; + }]; [self completeSignInFlowWithAccessToken:result.token.tokenString error:nil]; } }]; } +- (NSString *)email { + return _email; +} + - (void)signOut { [_loginManager logOut]; } From ea3ce2dd0a139d090a86416cf162c747957b1dab Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Fri, 16 Feb 2018 10:34:06 -0800 Subject: [PATCH 4/9] Facebook support for email_in_user_when_upgrading (#408) * Facebook support for email_in_user_when_upgrading Adds support for Facebook when autolinking an exisiting user to a new federated user (Google). * Addresses comments --- FirebaseAuthUI/FUIAuth.m | 31 +++++++++++++++++----------- FirebaseGoogleAuthUI/FUIGoogleAuth.m | 10 +++++++++ 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index e5d259cacb6..acb9207344c 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -306,22 +306,29 @@ - (void)signInWithEmailHint:(NSString *)emailHint [self.auth fetchProvidersForEmail:emailHint completion:^(NSArray *_Nullable providers, NSError *_Nullable error) { - NSString *federatedProviderID = [self federatedAuthProviderFromProviders:providers]; - // Google case - if ([federatedProviderID isEqualToString:FIRGoogleAuthProviderID]) { - id googleProviderUI; + if (error) { + completion(nil, error); + return; + } + NSString *existingFederatedProviderID = [self federatedAuthProviderFromProviders:providers]; + // Set of providers which can be auto-linked + NSSet *supportedProviders = + [NSSet setWithObjects:FIRGoogleAuthProviderID, FIRFacebookAuthProviderID, nil]; + if ([supportedProviders containsObject:existingFederatedProviderID]) { + id authProviderUI; + // Retrieve the FUIAuthProvider instance from FUIAuth for the existing provider ID. for (id provider in self.providers) { - if ([provider.providerID isEqualToString:FIRGoogleAuthProviderID]) { - googleProviderUI = provider; + if ([provider.providerID isEqualToString:existingFederatedProviderID]) { + authProviderUI = provider; break; } } - [googleProviderUI signOut]; - [googleProviderUI signInWithDefaultValue:emailHint - presentingViewController:presentingViewController - completion:^(FIRAuthCredential *_Nullable credential, - NSError *_Nullable error, - FIRAuthResultCallback _Nullable result) { + [authProviderUI signOut]; + [authProviderUI signInWithDefaultValue:emailHint + presentingViewController:presentingViewController + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error, + FIRAuthResultCallback _Nullable result) { if (error) { completion(nil, error); return; diff --git a/FirebaseGoogleAuthUI/FUIGoogleAuth.m b/FirebaseGoogleAuthUI/FUIGoogleAuth.m index 17258a2823a..54aec698d3c 100644 --- a/FirebaseGoogleAuthUI/FUIGoogleAuth.m +++ b/FirebaseGoogleAuthUI/FUIGoogleAuth.m @@ -55,6 +55,11 @@ @implementation FUIGoogleAuth { @brief The callback which should be invoked when the sign in flow completes (or is cancelled.) */ FIRAuthProviderSignInCompletionBlock _pendingSignInCallback; + + /** @var _email + @brief The email address associated with this account. + */ + NSString *_email; } - (instancetype)init { @@ -143,6 +148,10 @@ - (BOOL)handleOpenURL:(NSURL *)URL sourceApplication:(NSString *)sourceApplicati return [signIn handleURL:URL sourceApplication:sourceApplication annotation:nil]; } +- (NSString *)email { + return _email; +} + #pragma mark - GIDSignInDelegate methods - (void)signIn:(GIDSignIn *)signIn @@ -161,6 +170,7 @@ - (void)signIn:(GIDSignIn *)signIn } return; } + _email = user.profile.email; UIActivityIndicatorView *activityView = [FUIAuthBaseViewController addActivityIndicator:_presentingViewController.view]; [activityView startAnimating]; From 7bd2a7e0f2174f4a84bf5da884a3ea38238474e7 Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Fri, 23 Feb 2018 11:16:22 -0800 Subject: [PATCH 5/9] =?UTF-8?q?Adds=20email/password=20=E2=80=9Cemail-alre?= =?UTF-8?q?ady-in-use=E2=80=9D=20flow=20when=20upgrading=20to=20a=20federa?= =?UTF-8?q?ted=20credential=20(#410)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds email/password “email-already-in-use” flow * addresses commnets * Addresses comments --- FirebaseAuthUI/FUIAuth.m | 70 ++++++++++++------- .../FUIPasswordSignInViewController.m | 10 ++- ...FUIPasswordSignInViewController_Internal.h | 35 ++++++++++ 3 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 FirebaseAuthUI/FUIPasswordSignInViewController_Internal.h diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index acb9207344c..334c7c139d5 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -28,6 +28,7 @@ #import "FUIAuthStrings.h" #import "FUIGoogleAuth.h" #import "FUIEmailEntryViewController.h" +#import "FUIPasswordSignInViewController_Internal.h" #import "FUIPasswordVerificationViewController.h" #import "FUIPasswordSignInViewController.h" @@ -165,7 +166,7 @@ - (BOOL)signOutWithError:(NSError *_Nullable *_Nullable)error { } - (void)signInWithProviderUI:(id)providerUI - presentingViewController:(UIViewController *)presentingViewController + presentingViewController:(FUIAuthBaseViewController *)presentingViewController defaultValue:(nullable NSString *)defaultValue { // Sign out first to make sure sign in starts with a clean state. @@ -291,7 +292,7 @@ - (void)signInWithProviderUI:(id)providerUI } - (void)signInWithEmailHint:(NSString *)emailHint - presentingViewController:(UIViewController *)presentingViewController + presentingViewController:(FUIAuthBaseViewController *)presentingViewController completion:(FIRAuthDataResultCallback)completion { NSString *kTempApp = @"tempApp"; FIROptions *options = [FIROptions defaultOptions]; @@ -302,7 +303,7 @@ - (void)signInWithEmailHint:(NSString *)emailHint FIRApp *tempApp = [FIRApp appNamed:kTempApp]; // Create a new auth instance in order to perform a successful sign-in without losing the // currently signed in user on the default auth instance. - FIRAuth *auth = [FIRAuth authWithApp:tempApp]; + FIRAuth *tempAuth = [FIRAuth authWithApp:tempApp]; [self.auth fetchProvidersForEmail:emailHint completion:^(NSArray *_Nullable providers, NSError *_Nullable error) { @@ -310,39 +311,54 @@ - (void)signInWithEmailHint:(NSString *)emailHint completion(nil, error); return; } - NSString *existingFederatedProviderID = [self federatedAuthProviderFromProviders:providers]; - // Set of providers which can be auto-linked + NSString *existingFederatedProviderID = [self authProviderFromProviders:providers]; + // Set of providers which can be auto-linked. NSSet *supportedProviders = - [NSSet setWithObjects:FIRGoogleAuthProviderID, FIRFacebookAuthProviderID, nil]; + [NSSet setWithObjects:FIRGoogleAuthProviderID, + FIRFacebookAuthProviderID, + FIREmailAuthProviderID, + nil]; if ([supportedProviders containsObject:existingFederatedProviderID]) { - id authProviderUI; - // Retrieve the FUIAuthProvider instance from FUIAuth for the existing provider ID. - for (id provider in self.providers) { - if ([provider.providerID isEqualToString:existingFederatedProviderID]) { - authProviderUI = provider; - break; + if ([existingFederatedProviderID isEqualToString:FIREmailAuthProviderID]) { + FUIAuth *authUI = [[FUIAuth alloc]initWithAuth:tempAuth]; + // Email password sign-in + FUIPasswordSignInViewController *controller = + [[FUIPasswordSignInViewController alloc] initWithAuthUI:authUI email:emailHint]; + controller.onDismissCallback = completion; + [presentingViewController pushViewController:controller]; + } else { + id authProviderUI; + // Retrieve the FUIAuthProvider instance from FUIAuth for the existing provider ID. + for (id provider in self.providers) { + if ([provider.providerID isEqualToString:existingFederatedProviderID]) { + authProviderUI = provider; + break; + } } + [authProviderUI signOut]; + [authProviderUI signInWithDefaultValue:emailHint + presentingViewController:presentingViewController + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error, + FIRAuthResultCallback _Nullable result) { + if (error) { + if (completion) { + completion(nil, error); + } + return; + } + [tempAuth signInAndRetrieveDataWithCredential:credential completion:completion]; + }]; } - [authProviderUI signOut]; - [authProviderUI signInWithDefaultValue:emailHint - presentingViewController:presentingViewController - completion:^(FIRAuthCredential *_Nullable credential, - NSError *_Nullable error, - FIRAuthResultCallback _Nullable result) { - if (error) { - completion(nil, error); - return; - } - - [auth signInAndRetrieveDataWithCredential:credential completion:completion]; - }]; } }]; } -- (nullable NSString *)federatedAuthProviderFromProviders:(NSArray *) providers { +- (nullable NSString *)authProviderFromProviders:(NSArray *) providers { NSSet *providerSet = - [NSSet setWithArray:@[ FIRFacebookAuthProviderID, FIRGoogleAuthProviderID ]]; + [NSSet setWithArray:@[ FIRFacebookAuthProviderID, + FIRGoogleAuthProviderID, + FIREmailAuthProviderID ]]; for (NSString *provider in providers) { if ( [providerSet containsObject:provider]) { return provider; diff --git a/FirebaseAuthUI/FUIPasswordSignInViewController.m b/FirebaseAuthUI/FUIPasswordSignInViewController.m index be6f42aef14..91176d172e4 100644 --- a/FirebaseAuthUI/FUIPasswordSignInViewController.m +++ b/FirebaseAuthUI/FUIPasswordSignInViewController.m @@ -14,7 +14,7 @@ // limitations under the License. // -#import "FUIPasswordSignInViewController.h" +#import "FUIPasswordSignInViewController_Internal.h" #import #import "FUIAuthBaseViewController_Internal.h" @@ -80,6 +80,10 @@ - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil _email = [email copy]; self.title = FUILocalizedString(kStr_SignInTitle); + __weak FUIPasswordSignInViewController *weakself = self; + _onDismissCallback = ^(FIRAuthDataResult *authResult, NSError *error){ + [weakself.authUI invokeResultCallbackWithAuthDataResult:authResult error:error]; + }; } return self; } @@ -153,7 +157,9 @@ - (void)signInWithDefaultValue:(NSString *)email andPassword:(NSString *)passwor } } [self.navigationController dismissViewControllerAnimated:YES completion:^{ - [self.authUI invokeResultCallbackWithAuthDataResult:authResult error:error]; + if (self->_onDismissCallback) { + self->_onDismissCallback(authResult, error); + } }]; }; diff --git a/FirebaseAuthUI/FUIPasswordSignInViewController_Internal.h b/FirebaseAuthUI/FUIPasswordSignInViewController_Internal.h new file mode 100644 index 00000000000..3958533cf87 --- /dev/null +++ b/FirebaseAuthUI/FUIPasswordSignInViewController_Internal.h @@ -0,0 +1,35 @@ +// +// Copyright (c) 2018 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "FUIPasswordSignInViewController.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FUIPasswordSignInViewController () + +/** @property onDismissCallback: + @brief Sets an optional custom callback for FUIPasswordSigInViewController during dismissal. If + this property is set the default dismissal routine is not triggered and should be included + in this block if necessary. This block is NOT set to nil after use, set to nil after using + if you wish to avoid circular references. + */ +@property(nonatomic, strong, nullable) FIRAuthDataResultCallback onDismissCallback; + +NS_ASSUME_NONNULL_END + + +@end From f0cede5fed129c43644b78beb1ba498ed0e0a8dc Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Thu, 1 Mar 2018 16:05:42 -0800 Subject: [PATCH 6/9] Adds dialog before sign in prompt (#412) * Adds dialog before sign in prompt * Addresses Comments --- FirebaseAuthUI/FUIAuth.m | 59 +++++++++++++------ FirebaseAuthUI/FUIAuthBaseViewController.m | 18 +++++- .../FUIAuthBaseViewController_Internal.h | 28 +++++++++ 3 files changed, 86 insertions(+), 19 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index 334c7c139d5..fe744db8ab7 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -231,6 +231,7 @@ - (void)signInWithProviderUI:(id)providerUI // Link federated providers [self signInWithEmailHint:[providerUI email] presentingViewController:presentingViewController + originalError:error completion:^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error) { if (error) { @@ -293,6 +294,7 @@ - (void)signInWithProviderUI:(id)providerUI - (void)signInWithEmailHint:(NSString *)emailHint presentingViewController:(FUIAuthBaseViewController *)presentingViewController + originalError:(NSError *)originalError completion:(FIRAuthDataResultCallback)completion { NSString *kTempApp = @"tempApp"; FIROptions *options = [FIROptions defaultOptions]; @@ -320,12 +322,24 @@ - (void)signInWithEmailHint:(NSString *)emailHint nil]; if ([supportedProviders containsObject:existingFederatedProviderID]) { if ([existingFederatedProviderID isEqualToString:FIREmailAuthProviderID]) { - FUIAuth *authUI = [[FUIAuth alloc]initWithAuth:tempAuth]; - // Email password sign-in - FUIPasswordSignInViewController *controller = - [[FUIPasswordSignInViewController alloc] initWithAuthUI:authUI email:emailHint]; - controller.onDismissCallback = completion; - [presentingViewController pushViewController:controller]; + + [FUIAuthBaseViewController showSignInAlertWithEmail:emailHint + providerShortName:@"Email/Password" + providerSignInLabel:@"Sign in with Email/Password" + presentingViewController:presentingViewController + signinHandler:^{ + FUIAuth *authUI = [[FUIAuth alloc]initWithAuth:tempAuth]; + // Email password sign-in + FUIPasswordSignInViewController *controller = + [[FUIPasswordSignInViewController alloc] initWithAuthUI:authUI email:emailHint]; + controller.onDismissCallback = completion; + [presentingViewController pushViewController:controller]; + } + cancelHandler:^{ + if (completion) { + completion(nil, originalError); + } + }]; } else { id authProviderUI; // Retrieve the FUIAuthProvider instance from FUIAuth for the existing provider ID. @@ -335,19 +349,30 @@ - (void)signInWithEmailHint:(NSString *)emailHint break; } } - [authProviderUI signOut]; - [authProviderUI signInWithDefaultValue:emailHint - presentingViewController:presentingViewController - completion:^(FIRAuthCredential *_Nullable credential, - NSError *_Nullable error, - FIRAuthResultCallback _Nullable result) { - if (error) { - if (completion) { - completion(nil, error); + + [FUIAuthBaseViewController showSignInAlertWithEmail:emailHint + provider:authProviderUI + presentingViewController:presentingViewController + signinHandler:^{ + [authProviderUI signOut]; + [authProviderUI signInWithDefaultValue:emailHint + presentingViewController:presentingViewController + completion:^(FIRAuthCredential *_Nullable credential, + NSError *_Nullable error, + FIRAuthResultCallback _Nullable result) { + if (error) { + if (completion) { + completion(nil, error); + } + return; } - return; + [tempAuth signInAndRetrieveDataWithCredential:credential completion:completion]; + }]; + } + cancelHandler:^{ + if (completion) { + completion(nil, originalError); } - [tempAuth signInAndRetrieveDataWithCredential:credential completion:completion]; }]; } } diff --git a/FirebaseAuthUI/FUIAuthBaseViewController.m b/FirebaseAuthUI/FUIAuthBaseViewController.m index 5cdb6d613a9..7dcb2516411 100644 --- a/FirebaseAuthUI/FUIAuthBaseViewController.m +++ b/FirebaseAuthUI/FUIAuthBaseViewController.m @@ -247,15 +247,29 @@ + (void)showSignInAlertWithEmail:(NSString *)email presentingViewController:(UIViewController *)presentingViewController signinHandler:(FUIAuthAlertActionHandler)signinHandler cancelHandler:(FUIAuthAlertActionHandler)cancelHandler { + [self showSignInAlertWithEmail:email + providerShortName:provider.shortName + providerSignInLabel:provider.signInLabel + presentingViewController:presentingViewController + signinHandler:signinHandler + cancelHandler:cancelHandler]; +} + ++ (void)showSignInAlertWithEmail:(NSString *)email + providerShortName:(NSString *)providerShortName + providerSignInLabel:(NSString *)providerSignInLabel + presentingViewController:(UIViewController *)presentingViewController + signinHandler:(FUIAuthAlertActionHandler)signinHandler + cancelHandler:(FUIAuthAlertActionHandler)cancelHandler { NSString *message = [NSString stringWithFormat:FUILocalizedString(kStr_ProviderUsedPreviouslyMessage), - email, provider.shortName]; + email, providerShortName]; UIAlertController *alertController = [UIAlertController alertControllerWithTitle:FUILocalizedString(kStr_ExistingAccountTitle) message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *signInAction = - [UIAlertAction actionWithTitle:provider.signInLabel + [UIAlertAction actionWithTitle:providerSignInLabel style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { signinHandler(); diff --git a/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h b/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h index 6ba4833e376..698303315ba 100644 --- a/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h +++ b/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h @@ -56,6 +56,34 @@ NS_ASSUME_NONNULL_BEGIN actionTitle:(NSString *)actionTitle presentingViewController:(UIViewController *)presentingViewController; +/** @fn showSignInAlertWithEmail:provider:handler: + @brief Displays an alert to conform with user whether she wants to proceed with the provider. + @param email The email address to sign in with. + @param provider The identity provider to sign in with. + @param signinHandler Handler for the sign in action of the alert. + @param cancelHandler Handler for the cancel action of the alert. + */ ++ (void)showSignInAlertWithEmail:(NSString *)email + provider:(id)provider + presentingViewController:(UIViewController *)presentingViewController + signinHandler:(FUIAuthAlertActionHandler)signinHandler + cancelHandler:(FUIAuthAlertActionHandler)cancelHandler; + +/** @fn showSignInAlertWithEmail:providerShortName:providerSignInLabel:handler: + @brief Displays an alert to conform with user whether she wants to proceed with the provider. + @param email The email address to sign in with. + @param providerShortName The name of the provider as displayed in the sign-in alert message. + @param providerSignInLabel The name of the provider as displayed in the sign-in alert button. + @param signinHandler Handler for the sign in action of the alert. + @param cancelHandler Handler for the cancel action of the alert. + */ ++ (void)showSignInAlertWithEmail:(NSString *)email + providerShortName:(NSString *)providerShortName + providerSignInLabel:(NSString *)providerSignInLabel + presentingViewController:(UIViewController *)presentingViewController + signinHandler:(FUIAuthAlertActionHandler)signinHandler + cancelHandler:(FUIAuthAlertActionHandler)cancelHandler; + /** @fn pushViewController: @brief Push the view controller to the navigation controller of the current view controller with animation. The pushed view controller will have a fixed "Back" title for back button. From 93238ad45a9286dc3a0fe99ce0c8c7c35fae0797 Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Mon, 5 Mar 2018 15:42:21 -0800 Subject: [PATCH 7/9] Handles email mismatch case (#414) * Handles the email mismatch case * Improvements * fixes typo --- FirebaseAuthUI/FUIAuth.m | 79 +++++++++++++++++-- FirebaseAuthUI/FUIAuthBaseViewController.m | 27 +++++++ .../FUIAuthBaseViewController_Internal.h | 17 ++++ 3 files changed, 115 insertions(+), 8 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index fe744db8ab7..a2f881b9512 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -32,6 +32,18 @@ #import "FUIPasswordVerificationViewController.h" #import "FUIPasswordSignInViewController.h" +/** @typedef EmailHintSignInCallback + @brief The type of block invoked when an emailHint sign-in event completes. + + @param authResult Optionally; Result of sign-in request containing both the user and + the additional user info associated with the user. + @param error Optionally; the error which occurred - or nil if the request was successful. + @param credential Optionally; The credential used to sign-in. + */ +typedef void (^EmailHintSignInCallback)(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error, + FIRAuthCredential *_Nullable credential); + /** @var kAppNameCodingKey @brief The key used to encode the app Name for NSCoding. */ @@ -233,11 +245,25 @@ - (void)signInWithProviderUI:(id)providerUI presentingViewController:presentingViewController originalError:error completion:^(FIRAuthDataResult *_Nullable authResult, - NSError *_Nullable error) { + NSError *_Nullable error, + FIRAuthCredential *_Nullable credential) { if (error) { completeSignInBlock(nil, error); return; } + + if (![authResult.user.email isEqualToString:[providerUI email]] + && credential) { + NSDictionary *userInfo = @{ + FUIAuthCredentialKey : credential, + }; + NSError *mergeError = [NSError errorWithDomain:FUIAuthErrorDomain + code:FUIAuthErrorCodeMergeConflict + userInfo:userInfo]; + completeSignInBlock(nil, mergeError); + return; + } + [authResult.user linkAndRetrieveDataWithCredential:credential completion:^(FIRAuthDataResult *_Nullable authResult, @@ -295,7 +321,7 @@ - (void)signInWithProviderUI:(id)providerUI - (void)signInWithEmailHint:(NSString *)emailHint presentingViewController:(FUIAuthBaseViewController *)presentingViewController originalError:(NSError *)originalError - completion:(FIRAuthDataResultCallback)completion { + completion:(EmailHintSignInCallback)completion { NSString *kTempApp = @"tempApp"; FIROptions *options = [FIROptions defaultOptions]; // Create an new app instance in order to create a new auth instance. @@ -310,7 +336,7 @@ - (void)signInWithEmailHint:(NSString *)emailHint [self.auth fetchProvidersForEmail:emailHint completion:^(NSArray *_Nullable providers, NSError *_Nullable error) { if (error) { - completion(nil, error); + completion(nil, error, nil); return; } NSString *existingFederatedProviderID = [self authProviderFromProviders:providers]; @@ -332,12 +358,16 @@ - (void)signInWithEmailHint:(NSString *)emailHint // Email password sign-in FUIPasswordSignInViewController *controller = [[FUIPasswordSignInViewController alloc] initWithAuthUI:authUI email:emailHint]; - controller.onDismissCallback = completion; + controller.onDismissCallback = ^(FIRAuthDataResult *result, NSError *error) { + if (completion) { + completion(result, error, nil); + } + }; [presentingViewController pushViewController:controller]; } cancelHandler:^{ if (completion) { - completion(nil, originalError); + completion(nil, originalError, nil); } }]; } else { @@ -362,16 +392,49 @@ - (void)signInWithEmailHint:(NSString *)emailHint FIRAuthResultCallback _Nullable result) { if (error) { if (completion) { - completion(nil, error); + completion(nil, error, nil); } return; } - [tempAuth signInAndRetrieveDataWithCredential:credential completion:completion]; + + [tempAuth signInAndRetrieveDataWithCredential:credential + completion:^(FIRAuthDataResult *_Nullable authResult, + NSError *_Nullable error) { + if (error) { + if (completion) { + completion(nil, error, nil); + } + } + + // Handle potential email mismatch. + if (![emailHint isEqualToString:authResult.user.email]) { + NSString *signedInEmail = authResult.user.email; + NSString *title = + [NSString stringWithFormat:@"Continue sign in with %@?", signedInEmail]; + NSString *message = + [NSString stringWithFormat:@"You originally wanted to sign in with %@", + emailHint]; + [FUIAuthBaseViewController showAlertWithTitle:title + message:message + actionTitle:@"Continue" + presentingViewController:presentingViewController + actionHandler:^{ + if (completion) { + completion(authResult, nil, credential); + } + } + cancelHandler:^{ + if (completion) { + completion(nil, error, credential); + } + }]; + } + }]; }]; } cancelHandler:^{ if (completion) { - completion(nil, originalError); + completion(nil, originalError, nil); } }]; } diff --git a/FirebaseAuthUI/FUIAuthBaseViewController.m b/FirebaseAuthUI/FUIAuthBaseViewController.m index 7dcb2516411..dcbc361b98f 100644 --- a/FirebaseAuthUI/FUIAuthBaseViewController.m +++ b/FirebaseAuthUI/FUIAuthBaseViewController.m @@ -242,6 +242,33 @@ + (void)showAlertWithTitle:(nullable NSString *)title [presentingViewController presentViewController:alertController animated:YES completion:nil]; } ++ (void)showAlertWithTitle:(nullable NSString *)title + message:(NSString *)message + actionTitle:(NSString *)actionTitle + presentingViewController:(UIViewController *)presentingViewController + actionHandler:(FUIAuthAlertActionHandler)actionHandler + cancelHandler:(FUIAuthAlertActionHandler)cancelHandler { + UIAlertController *alertController = + [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction *okAction = + [UIAlertAction actionWithTitle:actionTitle + style:UIAlertActionStyleDefault + handler:^(UIAlertAction *_Nonnull action) { + actionHandler(); + }]; + [alertController addAction:okAction]; + UIAlertAction *cancelAction = + [UIAlertAction actionWithTitle:FUILocalizedString(kStr_Cancel) + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * _Nonnull action) { + cancelHandler(); + }]; + [alertController addAction:cancelAction]; + [presentingViewController presentViewController:alertController animated:YES completion:nil]; +} + + (void)showSignInAlertWithEmail:(NSString *)email provider:(id)provider presentingViewController:(UIViewController *)presentingViewController diff --git a/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h b/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h index 698303315ba..1a75abed6e5 100644 --- a/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h +++ b/FirebaseAuthUI/FUIAuthBaseViewController_Internal.h @@ -56,6 +56,23 @@ NS_ASSUME_NONNULL_BEGIN actionTitle:(NSString *)actionTitle presentingViewController:(UIViewController *)presentingViewController; +/** @fn showAlertWithTitle:message:actionTitle:presentingViewController: + @brief Displays an alert view with given title, message and action title on top of the + specified view controller. + @param title The title of the alert. + @param message The message of the alert. + @param actionTitle The title of the action button. + @param actionHandler The block to execute if the action button is tapped. + @param cancelHandler The block to execute if the cancel button is tapped. + @param presentingViewController The controller which shows alert. + */ ++ (void)showAlertWithTitle:(nullable NSString *)title + message:(NSString *)message + actionTitle:(NSString *)actionTitle + presentingViewController:(UIViewController *)presentingViewController + actionHandler:(FUIAuthAlertActionHandler)actionHandler + cancelHandler:(FUIAuthAlertActionHandler)cancelHandler; + /** @fn showSignInAlertWithEmail:provider:handler: @brief Displays an alert to conform with user whether she wants to proceed with the provider. @param email The email address to sign in with. From 6faa87f6474b525394613709deafdc36f2c23249 Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Wed, 11 Apr 2018 18:37:03 -0700 Subject: [PATCH 8/9] Handles email mismatch case and fixes bug for federated credential already in use. (#432) * Handles the email mismatch case * Improvements * fixes typo * Addresses comments --- FirebaseAuthUI/FUIAuth.m | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index a2f881b9512..462f8bd4c3b 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -246,7 +246,7 @@ - (void)signInWithProviderUI:(id)providerUI originalError:error completion:^(FIRAuthDataResult *_Nullable authResult, NSError *_Nullable error, - FIRAuthCredential *_Nullable credential) { + FIRAuthCredential *_Nullable existingCredential) { if (error) { completeSignInBlock(nil, error); return; @@ -336,7 +336,9 @@ - (void)signInWithEmailHint:(NSString *)emailHint [self.auth fetchProvidersForEmail:emailHint completion:^(NSArray *_Nullable providers, NSError *_Nullable error) { if (error) { - completion(nil, error, nil); + if (completion) { + completion(nil, error, nil); + } return; } NSString *existingFederatedProviderID = [self authProviderFromProviders:providers]; @@ -370,7 +372,7 @@ - (void)signInWithEmailHint:(NSString *)emailHint completion(nil, originalError, nil); } }]; - } else { + } else { // Federated sign-in case. id authProviderUI; // Retrieve the FUIAuthProvider instance from FUIAuth for the existing provider ID. for (id provider in self.providers) { @@ -429,6 +431,9 @@ - (void)signInWithEmailHint:(NSString *)emailHint } }]; } + if (completion) { + completion(authResult, error, credential); + } }]; }]; } From 3557efa34646af19f17e1f5530caa763e04f676a Mon Sep 17 00:00:00 2001 From: Zsika Phillip Date: Tue, 17 Jul 2018 09:03:27 -0700 Subject: [PATCH 9/9] Adds anonymous user upgrade --- FirebaseAuthUI/FUIAuth.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuthUI/FUIAuth.m b/FirebaseAuthUI/FUIAuth.m index 462f8bd4c3b..426dc95524c 100644 --- a/FirebaseAuthUI/FUIAuth.m +++ b/FirebaseAuthUI/FUIAuth.m @@ -302,7 +302,7 @@ - (void)signInWithProviderUI:(id)providerUI [self handleAccountLinkingForEmail:email newCredential:credential presentingViewController:presentingViewController - singInResult:result]; + signInResult:result]; return; } if (error) {