blob: c9a10da7f2539b76ae16b518dea773732cc4c0fb [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/signin_interaction/signin_interaction_controller.h"
#include "base/feature_list.h"
#include "base/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "components/prefs/pref_service.h"
#include "components/signin/core/browser/account_consistency_method.h"
#include "components/signin/core/browser/signin_pref_names.h"
#include "components/unified_consent/feature.h"
#import "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/signin/authentication_service.h"
#include "ios/chrome/browser/signin/authentication_service_factory.h"
#include "ios/chrome/browser/signin/identity_manager_factory.h"
#import "ios/chrome/browser/signin/signin_util.h"
#import "ios/chrome/browser/ui/authentication/authentication_ui_util.h"
#import "ios/chrome/browser/ui/authentication/chrome_signin_view_controller.h"
#import "ios/chrome/browser/ui/signin_interaction/signin_interaction_presenting.h"
#import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/signin/chrome_identity.h"
#import "ios/public/provider/chrome/browser/signin/chrome_identity_interaction_manager.h"
#import "ios/public/provider/chrome/browser/signin/chrome_identity_service.h"
#include "services/identity/public/cpp/identity_manager.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using signin_ui::CompletionCallback;
@interface SigninInteractionController ()<
ChromeIdentityInteractionManagerDelegate,
ChromeSigninViewControllerDelegate> {
ios::ChromeBrowserState* browserState_;
signin_metrics::AccessPoint accessPoint_;
signin_metrics::PromoAction promoAction_;
BOOL isCancelling_;
BOOL isDismissing_;
BOOL interactionManagerDismissalIgnored_;
CompletionCallback completionCallback_;
ChromeSigninViewController* signinViewController_;
ChromeIdentityInteractionManager* identityInteractionManager_;
ChromeIdentity* signInIdentity_;
BOOL identityAdded_;
}
// The dispatcher for this class.
@property(nonatomic, weak, readonly) id<ApplicationCommands> dispatcher;
// The object responsible for presenting the UI.
@property(nonatomic, weak, readonly) id<SigninInteractionPresenting> presenter;
@end
@implementation SigninInteractionController
@synthesize dispatcher = dispatcher_;
@synthesize presenter = presenter_;
- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
presentationProvider:(id<SigninInteractionPresenting>)presenter
accessPoint:(signin_metrics::AccessPoint)accessPoint
promoAction:(signin_metrics::PromoAction)promoAction
dispatcher:(id<ApplicationCommands>)dispatcher {
self = [super init];
if (self) {
DCHECK(browserState);
DCHECK(presenter);
browserState_ = browserState;
presenter_ = presenter;
accessPoint_ = accessPoint;
promoAction_ = promoAction;
dispatcher_ = dispatcher;
}
return self;
}
- (void)cancel {
// Cancelling and dismissing the |identityInteractionManager_| may call the
// |completionCallback_| which could lead to |self| being released before the
// end of this method. |self| is retained here to prevent this from happening.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-variable"
// Retain this object through the rest of this method in case this object's
// owner frees this object during the execution of the completion block.
SigninInteractionController* strongSelf = self;
#pragma clang diagnostic pop
isCancelling_ = YES;
[self.presenter dismissError];
[identityInteractionManager_ cancelAndDismissAnimated:NO];
[signinViewController_ cancel];
isCancelling_ = NO;
}
- (void)cancelAndDismiss {
isDismissing_ = YES;
[self cancel];
isDismissing_ = NO;
}
- (void)signInWithIdentity:(ChromeIdentity*)identity
completion:(signin_ui::CompletionCallback)completion {
signin_metrics::LogSigninAccessPointStarted(accessPoint_, promoAction_);
completionCallback_ = [completion copy];
ios::ChromeIdentityService* identityService =
ios::GetChromeBrowserProvider()->GetChromeIdentityService();
if (unified_consent::IsUnifiedConsentFeatureEnabled()) {
[self showSigninViewControllerWithIdentity:identity identityAdded:NO];
} else if (identity) {
DCHECK(identityService->IsValidIdentity(identity));
DCHECK(!signinViewController_);
[self showSigninViewControllerWithIdentity:identity identityAdded:NO];
} else if (identityService->HasIdentities()) {
DCHECK(!signinViewController_);
[self showSigninViewControllerWithIdentity:nil identityAdded:NO];
} else {
identityInteractionManager_ =
identityService->CreateChromeIdentityInteractionManager(browserState_,
self);
if (!identityInteractionManager_) {
// Abort sign-in if the ChromeIdentityInteractionManager returned is
// nil (this can happen when the iOS internal provider is not used).
[self runCompletionCallbackWithSuccess:NO showAccountsSettings:NO];
return;
}
__weak SigninInteractionController* weakSelf = self;
[identityInteractionManager_
addAccountWithCompletion:^(ChromeIdentity* identity, NSError* error) {
[weakSelf handleIdentityAdded:identity error:error shouldSignIn:YES];
}];
}
}
- (void)reAuthenticateWithCompletion:(CompletionCallback)completion {
signin_metrics::LogSigninAccessPointStarted(accessPoint_, promoAction_);
completionCallback_ = [completion copy];
AccountInfo accountInfo =
IdentityManagerFactory::GetForBrowserState(browserState_)
->GetPrimaryAccountInfo();
std::string emailToReauthenticate = accountInfo.email;
std::string idToReauthenticate = accountInfo.gaia;
if (emailToReauthenticate.empty() || idToReauthenticate.empty()) {
// This corresponds to a re-authenticate request after the user was signed
// out. This corresponds to the case where the identity was removed as a
// result of the permissions being removed on the server or the identity
// being removed from another app.
//
// Simply use the the last signed-in user email in this case and go though
// the entire sign-in flow as sync needs to be configured.
emailToReauthenticate = browserState_->GetPrefs()->GetString(
prefs::kGoogleServicesLastUsername);
idToReauthenticate = browserState_->GetPrefs()->GetString(
prefs::kGoogleServicesLastAccountId);
}
DCHECK(!emailToReauthenticate.empty());
DCHECK(!idToReauthenticate.empty());
identityInteractionManager_ =
ios::GetChromeBrowserProvider()
->GetChromeIdentityService()
->CreateChromeIdentityInteractionManager(browserState_, self);
__weak SigninInteractionController* weakSelf = self;
[identityInteractionManager_
reauthenticateUserWithID:base::SysUTF8ToNSString(idToReauthenticate)
email:base::SysUTF8ToNSString(emailToReauthenticate)
completion:^(ChromeIdentity* identity, NSError* error) {
[weakSelf handleIdentityAdded:identity
error:error
shouldSignIn:YES];
}];
}
- (void)addAccountWithCompletion:(CompletionCallback)completion {
completionCallback_ = [completion copy];
identityInteractionManager_ =
ios::GetChromeBrowserProvider()
->GetChromeIdentityService()
->CreateChromeIdentityInteractionManager(browserState_, self);
__weak SigninInteractionController* weakSelf = self;
[identityInteractionManager_
addAccountWithCompletion:^(ChromeIdentity* identity, NSError* error) {
[weakSelf handleIdentityAdded:identity error:error shouldSignIn:NO];
}];
}
#pragma mark - ChromeIdentityInteractionManager operations
- (void)handleIdentityAdded:(ChromeIdentity*)identity
error:(NSError*)error
shouldSignIn:(BOOL)shouldSignIn {
if (!identityInteractionManager_)
return;
if (error) {
// Filter out cancel and errors handled internally by ChromeIdentity.
if (!ShouldHandleSigninError(error)) {
[self runCompletionCallbackWithSuccess:NO showAccountsSettings:NO];
return;
}
__weak SigninInteractionController* weakSelf = self;
ProceduralBlock dismissAction = ^{
[weakSelf runCompletionCallbackWithSuccess:NO showAccountsSettings:NO];
};
[self.presenter presentError:error dismissAction:dismissAction];
return;
}
if (shouldSignIn) {
[self showSigninViewControllerWithIdentity:identity identityAdded:YES];
} else {
[self runCompletionCallbackWithSuccess:YES showAccountsSettings:NO];
}
}
- (void)dismissPresentedViewControllersAnimated:(BOOL)animated
completion:(ProceduralBlock)completion {
if (self.presenter.isPresenting) {
[self.presenter dismissAllViewControllersAnimated:animated
completion:completion];
} else if (completion) {
completion();
}
interactionManagerDismissalIgnored_ = NO;
}
#pragma mark - ChromeIdentityInteractionManagerDelegate
- (void)interactionManager:(ChromeIdentityInteractionManager*)interactionManager
presentViewController:(UIViewController*)viewController
animated:(BOOL)animated
completion:(ProceduralBlock)completion {
[self.presenter presentViewController:viewController
animated:animated
completion:completion];
}
- (void)interactionManager:(ChromeIdentityInteractionManager*)interactionManager
dismissViewControllerAnimated:(BOOL)animated
completion:(ProceduralBlock)completion {
// Avoid awkward double transitions by not dismissing
// identityInteractionManager_| if the signin view controller will be
// displayed on top of it. |identityInteractionManager_| will be dismissed
// when the signin view controller will be dismissed.
if ([interactionManager isCanceling]) {
[self dismissPresentedViewControllersAnimated:animated
completion:completion];
} else {
interactionManagerDismissalIgnored_ = YES;
if (completion) {
completion();
}
}
}
#pragma mark - ChromeSigninViewController operations
- (void)showSigninViewControllerWithIdentity:(ChromeIdentity*)signInIdentity
identityAdded:(BOOL)identityAdded {
signinViewController_ =
[[ChromeSigninViewController alloc] initWithBrowserState:browserState_
accessPoint:accessPoint_
promoAction:promoAction_
signInIdentity:signInIdentity
dispatcher:self.dispatcher];
[signinViewController_ setDelegate:self];
[signinViewController_
setModalPresentationStyle:UIModalPresentationFormSheet];
[signinViewController_
setModalTransitionStyle:UIModalTransitionStyleCoverVertical];
signInIdentity_ = signInIdentity;
identityAdded_ = identityAdded;
if (identityInteractionManager_) {
// If |identityInteractionManager_| is currently displayed,
// |signinViewController_| is presented on top of it (instead of on top of
// |presentingViewController_|), to avoid an awkward transition (dismissing
// |identityInteractionManager_|, followed by presenting
// |signinViewController_|).
[self.presenter presentTopViewController:signinViewController_
animated:YES
completion:nil];
} else {
[self.presenter presentViewController:signinViewController_
animated:YES
completion:nil];
}
}
- (void)dismissSigninViewControllerWithSignInSuccess:(BOOL)success
showAccountsSettings:
(BOOL)showAccountsSettings {
DCHECK(signinViewController_);
if ((isCancelling_ && !isDismissing_) || !self.presenter.isPresenting) {
[self runCompletionCallbackWithSuccess:success
showAccountsSettings:showAccountsSettings];
return;
}
ProceduralBlock completion = ^{
[self runCompletionCallbackWithSuccess:success
showAccountsSettings:showAccountsSettings];
};
[self dismissPresentedViewControllersAnimated:YES completion:completion];
}
#pragma mark - ChromeSigninViewControllerDelegate
- (void)willStartSignIn:(ChromeSigninViewController*)controller {
DCHECK_EQ(controller, signinViewController_);
}
- (void)willStartAddAccount:(ChromeSigninViewController*)controller {
DCHECK_EQ(controller, signinViewController_);
}
- (void)didSkipSignIn:(ChromeSigninViewController*)controller {
DCHECK_EQ(controller, signinViewController_);
[self dismissSigninViewControllerWithSignInSuccess:NO
showAccountsSettings:NO];
}
- (void)didSignIn:(ChromeSigninViewController*)controller {
DCHECK_EQ(controller, signinViewController_);
}
- (void)didUndoSignIn:(ChromeSigninViewController*)controller
identity:(ChromeIdentity*)identity {
DCHECK_EQ(controller, signinViewController_);
if ([signInIdentity_ isEqual:identity]) {
signInIdentity_ = nil;
if (identityAdded_) {
// This is best effort. If the operation fails, the account will be left
// on the device. The user will not be warned either as this call is
// asynchronous (but undo is not), the application might be in an unknown
// state when the forget identity operation finishes.
ios::GetChromeBrowserProvider()
->GetChromeIdentityService()
->ForgetIdentity(identity, nil);
}
[self dismissSigninViewControllerWithSignInSuccess:NO
showAccountsSettings:NO];
}
}
- (void)didFailSignIn:(ChromeSigninViewController*)controller {
DCHECK_EQ(controller, signinViewController_);
[self dismissSigninViewControllerWithSignInSuccess:NO
showAccountsSettings:NO];
}
- (void)didAcceptSignIn:(ChromeSigninViewController*)controller
showAccountsSettings:(BOOL)showAccountsSettings {
DCHECK_EQ(controller, signinViewController_);
[self dismissSigninViewControllerWithSignInSuccess:YES
showAccountsSettings:showAccountsSettings];
}
#pragma mark - Utility methods
- (void)runCompletionCallbackWithSuccess:(BOOL)success
showAccountsSettings:(BOOL)showAccountsSettings {
// In order to avoid awkward double transitions, |identityInteractionManager_|
// is not dismissed when requested (except when canceling). However, in case
// of errors, |identityInteractionManager_| needs to be directly dismissed,
// which is done here.
if (interactionManagerDismissalIgnored_) {
[self dismissPresentedViewControllersAnimated:YES completion:nil];
}
if (showAccountsSettings) {
[self.presenter showAccountsSettings];
}
// Cleaning up and calling the |completionCallback_| should be done last.
identityInteractionManager_ = nil;
signinViewController_ = nil;
// Ensure self is not destroyed in the callbacks.
SigninInteractionController* strongSelf = self;
if (completionCallback_) {
completionCallback_(success);
completionCallback_ = nil;
}
strongSelf = nil;
}
@end