blob: 5bd1c5820feb2c4bc5b9379aa5b72d1481b923db [file] [log] [blame]
// Copyright 2020 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/authentication/signin/user_signin/user_signin_coordinator.h"
#import "base/feature_list.h"
#import "base/ios/block_types.h"
#import "base/mac/foundation_util.h"
#import "ios/chrome/browser/main/browser.h"
#import "ios/chrome/browser/signin/authentication_service.h"
#import "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/signin/identity_manager_factory.h"
#import "ios/chrome/browser/sync/consent_auditor_factory.h"
#import "ios/chrome/browser/sync/sync_setup_service_factory.h"
#import "ios/chrome/browser/ui/authentication/authentication_flow.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_constants.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_coordinator+protected.h"
#import "ios/chrome/browser/ui/authentication/signin/user_signin/logging/user_signin_logger.h"
#import "ios/chrome/browser/ui/authentication/signin/user_signin/user_signin_mediator.h"
#import "ios/chrome/browser/ui/authentication/signin/user_signin/user_signin_view_controller.h"
#import "ios/chrome/browser/ui/authentication/unified_consent/unified_consent_coordinator.h"
#import "ios/chrome/browser/ui/commands/browsing_data_commands.h"
#import "ios/chrome/browser/ui/commands/command_dispatcher.h"
#import "ios/chrome/browser/unified_consent/unified_consent_service_factory.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using signin_metrics::AccessPoint;
using signin_metrics::PromoAction;
namespace {
const CGFloat kFadeOutAnimationDuration = 0.16f;
} // namespace
@interface UserSigninCoordinator () <UIAdaptivePresentationControllerDelegate,
UnifiedConsentCoordinatorDelegate,
UserSigninViewControllerDelegate,
UserSigninMediatorDelegate>
// Coordinator that handles the user consent before the user signs in.
@property(nonatomic, strong)
UnifiedConsentCoordinator* unifiedConsentCoordinator;
// Coordinator that handles adding a user account.
@property(nonatomic, strong) SigninCoordinator* addAccountSigninCoordinator;
// Coordinator that handles the advanced settings sign-in.
@property(nonatomic, strong)
SigninCoordinator* advancedSettingsSigninCoordinator;
// View controller that handles the sign-in UI.
@property(nonatomic, strong, readwrite)
UserSigninViewController* viewController;
// Mediator that handles the sign-in authentication state.
@property(nonatomic, strong) UserSigninMediator* mediator;
// Suggested identity shown at sign-in.
@property(nonatomic, strong, readonly) ChromeIdentity* defaultIdentity;
// Logger for sign-in operations.
@property(nonatomic, strong, readonly) UserSigninLogger* logger;
// Sign-in intent.
@property(nonatomic, assign, readonly) UserSigninIntent signinIntent;
// Whether an account has been added during sign-in flow.
@property(nonatomic, assign) BOOL addedAccount;
// YES if the view controller started the presenting animation.
@property(nonatomic, assign) BOOL viewControllerPresentingAnimation;
// Callback to be invoked when the view controller presenting animation is done.
@property(nonatomic, copy) ProceduralBlock interruptCallback;
@end
@implementation UserSigninCoordinator
@synthesize baseNavigationController = _baseNavigationController;
#pragma mark - Public
- (instancetype)initWithBaseNavigationController:
(UINavigationController*)navigationController
browser:(Browser*)browser
signinIntent:(UserSigninIntent)signinIntent
logger:(UserSigninLogger*)logger {
if (self = [self initWithBaseViewController:navigationController
browser:browser
identity:nil
signinIntent:signinIntent
logger:logger]) {
_baseNavigationController = navigationController;
}
return self;
}
- (instancetype)initWithBaseViewController:(UIViewController*)viewController
browser:(Browser*)browser
identity:(ChromeIdentity*)identity
signinIntent:(UserSigninIntent)signinIntent
logger:(UserSigninLogger*)logger {
self = [super initWithBaseViewController:viewController browser:browser];
if (self) {
_defaultIdentity = identity;
_signinIntent = signinIntent;
DCHECK(logger);
_logger = logger;
}
return self;
}
#pragma mark - SigninCoordinator
- (void)start {
AuthenticationService* authenticationService =
AuthenticationServiceFactory::GetForBrowserState(
self.browser->GetBrowserState());
// The user should be signed out before triggering sign-in or upgrade states.
// Users are allowed to be signed-in during FirstRun for testing purposes.
DCHECK(base::FeatureList::IsEnabled(signin::kMobileIdentityConsistency) ||
!authenticationService->IsAuthenticated() ||
self.signinIntent == UserSigninIntentFirstRun);
[super start];
self.viewController = [self generateUserSigninViewController];
self.viewController.delegate = self;
self.viewController.useFirstRunSkipButton =
self.signinIntent == UserSigninIntentFirstRun;
self.mediator = [[UserSigninMediator alloc]
initWithAuthenticationService:AuthenticationServiceFactory::
GetForBrowserState(
self.browser->GetBrowserState())
identityManager:IdentityManagerFactory::GetForBrowserState(
self.browser->GetBrowserState())
consentAuditor:ConsentAuditorFactory::GetForBrowserState(
self.browser->GetBrowserState())
unifiedConsentService:UnifiedConsentServiceFactory::
GetForBrowserState(
self.browser->GetBrowserState())
syncSetupService:SyncSetupServiceFactory::GetForBrowserState(
self.browser->GetBrowserState())];
self.mediator.delegate = self;
self.unifiedConsentCoordinator = [[UnifiedConsentCoordinator alloc]
initWithBaseViewController:nil
browser:self.browser];
self.unifiedConsentCoordinator.delegate = self;
// Set UnifiedConsentCoordinator properties.
if (self.defaultIdentity) {
self.unifiedConsentCoordinator.selectedIdentity = self.defaultIdentity;
}
self.unifiedConsentCoordinator.autoOpenIdentityPicker =
self.logger.promoAction == PromoAction::PROMO_ACTION_NOT_DEFAULT;
[self.unifiedConsentCoordinator start];
// Display UnifiedConsentViewController within the host.
self.viewController.unifiedConsentViewController =
self.unifiedConsentCoordinator.viewController;
[self presentUserSigninViewController];
[self.logger logSigninStarted];
}
- (void)interruptWithAction:(SigninCoordinatorInterruptAction)action
completion:(ProceduralBlock)completion {
// Chrome should never start before the first run is fully finished. Therefore
// we do not expect the sign-in to be interrupted for first run.
DCHECK(self.signinIntent != UserSigninIntentFirstRun);
if (self.mediator.isAuthenticationInProgress) {
[self.logger
logSigninCompletedWithResult:SigninCoordinatorResultInterrupted
addedAccount:self.addAccountSigninCoordinator != nil
advancedSettingsShown:self.advancedSettingsSigninCoordinator !=
nil];
}
__weak UserSigninCoordinator* weakSelf = self;
if (self.addAccountSigninCoordinator) {
// |self.addAccountSigninCoordinator| needs to be interupted before
// interrupting |self.viewController|.
// The add account view should not be dismissed since the
// |self.viewController| will take care of that according to |action|.
[self.addAccountSigninCoordinator
interruptWithAction:SigninCoordinatorInterruptActionNoDismiss
completion:^{
// |self.addAccountSigninCoordinator.signinCompletion|
// is expected to be called before this block.
// Therefore |weakSelf.addAccountSigninCoordinator| is
// expected to be nil.
DCHECK(!weakSelf.addAccountSigninCoordinator);
[weakSelf interruptUserSigninUIWithAction:action
completion:completion];
}];
return;
} else if (self.advancedSettingsSigninCoordinator) {
// |self.viewController| has already been dismissed. The interruption should
// be sent to |self.advancedSettingsSigninCoordinator|.
DCHECK(!self.viewController);
DCHECK(!self.mediator);
[self.advancedSettingsSigninCoordinator
interruptWithAction:action
completion:^{
// |self.advancedSettingsSigninCoordinator.signinCompletion|
// is expected to be called before this block.
// Therefore |weakSelf.advancedSettingsSigninCoordinator| is
// expected to be nil.
DCHECK(!weakSelf.advancedSettingsSigninCoordinator);
if (completion) {
completion();
}
}];
return;
}
[self interruptUserSigninUIWithAction:action completion:completion];
}
#pragma mark - UnifiedConsentCoordinatorDelegate
- (void)unifiedConsentCoordinatorDidTapSettingsLink:
(UnifiedConsentCoordinator*)coordinator {
[self startSigninFlow];
}
- (void)unifiedConsentCoordinatorDidReachBottom:
(UnifiedConsentCoordinator*)coordinator {
DCHECK_EQ(self.unifiedConsentCoordinator, coordinator);
[self.viewController markUnifiedConsentScreenReachedBottom];
}
- (void)unifiedConsentCoordinatorDidTapOnAddAccount:
(UnifiedConsentCoordinator*)coordinator {
DCHECK_EQ(self.unifiedConsentCoordinator, coordinator);
[self userSigninViewControllerDidTapOnAddAccount];
}
- (void)unifiedConsentCoordinatorNeedPrimaryButtonUpdate:
(UnifiedConsentCoordinator*)coordinator {
DCHECK_EQ(self.unifiedConsentCoordinator, coordinator);
[self.viewController setConfirmationButtonProperties];
}
#pragma mark - UserSigninViewControllerDelegate
- (BOOL)unifiedConsentCoordinatorHasIdentity {
return self.unifiedConsentCoordinator.selectedIdentity != nil;
}
- (void)userSigninViewControllerDidTapOnAddAccount {
DCHECK(!self.addAccountSigninCoordinator);
[self notifyUserSigninAttempted];
self.addAccountSigninCoordinator = [SigninCoordinator
addAccountCoordinatorWithBaseViewController:self.viewController
browser:self.browser
accessPoint:self.logger.accessPoint];
__weak UserSigninCoordinator* weakSelf = self;
self.addAccountSigninCoordinator.signinCompletion =
^(SigninCoordinatorResult signinResult,
SigninCompletionInfo* signinCompletionInfo) {
if (signinResult == SigninCoordinatorResultSuccess) {
weakSelf.unifiedConsentCoordinator.selectedIdentity =
signinCompletionInfo.identity;
weakSelf.addedAccount = YES;
}
[weakSelf.addAccountSigninCoordinator stop];
weakSelf.addAccountSigninCoordinator = nil;
};
[self.addAccountSigninCoordinator start];
}
- (void)userSigninViewControllerDidScrollOnUnifiedConsent {
[self.unifiedConsentCoordinator scrollToBottom];
}
- (void)userSigninViewControllerDidTapOnSkipSignin {
[self cancelSignin];
}
- (void)userSigninViewControllerDidTapOnSignin {
[self notifyUserSigninAttempted];
[self startSigninFlow];
}
#pragma mark - UserSigninMediatorDelegate
- (BOOL)userSigninMediatorGetSettingsLinkWasTapped {
return self.unifiedConsentCoordinator.settingsLinkWasTapped;
}
- (int)userSigninMediatorGetConsentConfirmationId {
if (self.userSigninMediatorGetSettingsLinkWasTapped) {
return self.unifiedConsentCoordinator.openSettingsStringId;
}
return self.viewController.acceptSigninButtonStringId;
}
- (const std::vector<int>&)userSigninMediatorGetConsentStringIds {
return self.unifiedConsentCoordinator.consentStringIds;
}
- (void)userSigninMediatorSigninFinishedWithResult:
(SigninCoordinatorResult)signinResult {
[self.logger logSigninCompletedWithResult:signinResult
addedAccount:self.addedAccount
advancedSettingsShown:self.unifiedConsentCoordinator
.settingsLinkWasTapped];
BOOL settingsWasTapped = self.unifiedConsentCoordinator.settingsLinkWasTapped;
ChromeIdentity* identity = self.unifiedConsentCoordinator.selectedIdentity;
__weak UserSigninCoordinator* weakSelf = self;
ProceduralBlock completion = ^void() {
[weakSelf viewControllerDismissedWithResult:signinResult
identity:identity
settingsLinkWasTapped:settingsWasTapped];
};
switch (self.signinIntent) {
case UserSigninIntentFirstRun: {
// The caller is responsible for cleaning up the base view controller for
// first run sign-in.
if (completion) {
completion();
}
break;
}
case UserSigninIntentSignin:
case UserSigninIntentUpgrade: {
if (self.viewController.presentingViewController) {
[self.viewController dismissViewControllerAnimated:YES
completion:completion];
} else {
// When the user swipes to dismiss the view controller. The sequence is:
// * The user swipe the view controller
// * The view controller is dismissed
// * [self presentationControllerDidDismiss] is called
// * The mediator is canceled
// And then this method is called by the mediator. Therefore the view
// controller already dismissed, and should not be dismissed again.
completion();
}
break;
}
}
}
- (void)userSigninMediatorSigninFailed {
[self.unifiedConsentCoordinator resetSettingLinkTapped];
self.unifiedConsentCoordinator.uiDisabled = NO;
[self.viewController signinDidStop];
[self.viewController setConfirmationButtonProperties];
}
#pragma mark - Private
// Cancels the sign-in flow if it is in progress, or dismiss the sign-in view
// if the sign-in is not in progress.
- (void)cancelSignin {
[self.mediator cancelSignin];
}
// Notifies the observers that the user is attempting sign-in.
- (void)notifyUserSigninAttempted {
[[NSNotificationCenter defaultCenter]
postNotificationName:kUserSigninAttemptedNotification
object:self];
}
// Called when |self.viewController| is dismissed. If |settingsWasTapped| is
// NO, the sign-in is finished and
// |runCompletionCallbackWithSigninResult:identity:| is called.
// Otherwise, the advanced settings sign-in is presented.
- (void)viewControllerDismissedWithResult:(SigninCoordinatorResult)signinResult
identity:(ChromeIdentity*)identity
settingsLinkWasTapped:(BOOL)settingsWasTapped {
DCHECK(!self.addAccountSigninCoordinator);
DCHECK(!self.advancedSettingsSigninCoordinator);
DCHECK(self.unifiedConsentCoordinator);
DCHECK(self.mediator);
DCHECK(self.viewController);
[self.unifiedConsentCoordinator stop];
self.unifiedConsentCoordinator = nil;
self.mediator = nil;
self.viewController = nil;
if (!settingsWasTapped || self.signinIntent == UserSigninIntentFirstRun) {
// For first run intent, the UserSigninCoordinator owner is reponsible to
// open the advanced settings sign-in.
[self runCompletionCallbackWithSigninResult:signinResult
identity:identity
showAdvancedSettingsSignin:settingsWasTapped];
return;
}
self.advancedSettingsSigninCoordinator = [SigninCoordinator
advancedSettingsSigninCoordinatorWithBaseViewController:
self.baseViewController
browser:self.browser];
__weak UserSigninCoordinator* weakSelf = self;
self.advancedSettingsSigninCoordinator.signinCompletion = ^(
SigninCoordinatorResult advancedSigninResult,
SigninCompletionInfo* signinCompletionInfo) {
[weakSelf
advancedSettingsSigninCoordinatorFinishedWithResult:advancedSigninResult
identity:signinCompletionInfo
.identity];
};
[self.advancedSettingsSigninCoordinator start];
}
// Displays the user sign-in view controller using the available base
// controller. First run requires an additional transitional fade animation when
// presenting this view.
- (void)presentUserSigninViewController {
// Always set the -UIViewController.modalPresentationStyle before accessing
// -UIViewController.presentationController.
self.viewController.modalPresentationStyle = UIModalPresentationFormSheet;
if (@available(iOS 13, *)) {
self.viewController.presentationController.delegate = self;
}
switch (self.signinIntent) {
case UserSigninIntentFirstRun: {
// Displays the sign-in screen with transitions specific to first-run.
DCHECK(self.baseNavigationController);
CATransition* transition = [CATransition animation];
transition.duration = kFadeOutAnimationDuration;
transition.type = kCATransitionFade;
[self.baseNavigationController.view.layer addAnimation:transition
forKey:kCATransition];
[self.baseNavigationController pushViewController:self.viewController
animated:NO];
break;
}
case UserSigninIntentUpgrade: {
// Avoid presenting the promo if the current device orientation is not
// supported. The promo will be presented at a later moment, when the
// device orientation is supported.
UIInterfaceOrientation orientation =
[UIApplication sharedApplication].statusBarOrientation;
NSUInteger supportedOrientationsMask =
[self.viewController supportedInterfaceOrientations];
if (!((1 << orientation) & supportedOrientationsMask)) {
[self
runCompletionCallbackWithSigninResult:
SigninCoordinatorResultInterrupted
identity:self.unifiedConsentCoordinator
.selectedIdentity
showAdvancedSettingsSignin:NO];
return;
}
[self presentUserViewControllerToBaseViewController];
break;
}
case UserSigninIntentSignin: {
[self presentUserViewControllerToBaseViewController];
break;
}
}
}
// Presents |self.viewController|. This method is only relevant when
// |self.signinIntent| is not UserSigninIntentFirstRun.
- (void)presentUserViewControllerToBaseViewController {
DCHECK_NE(UserSigninIntentFirstRun, self.signinIntent);
DCHECK(self.baseViewController);
self.viewControllerPresentingAnimation = YES;
__weak __typeof(self) weakSelf = self;
ProceduralBlock completion = ^{
weakSelf.viewControllerPresentingAnimation = NO;
if (weakSelf.interruptCallback) {
// The view controller is fully presented, the coordinator
// can be dismissed. UIKit doesn't allow a view controller
// to be dismissed during the animation.
// See crbug.com/1126170
ProceduralBlock interruptCallback = weakSelf.interruptCallback;
weakSelf.interruptCallback = nil;
interruptCallback();
}
};
[self.baseViewController presentViewController:self.viewController
animated:YES
completion:completion];
}
// Interrupts the sign-in when |self.viewController| is presented, by dismissing
// it if needed (according to |action|). Then |completion| is called.
// This method should not be called if |self.addAccountSigninCoordinator| has
// not been stopped before.
- (void)interruptUserSigninUIWithAction:(SigninCoordinatorInterruptAction)action
completion:(ProceduralBlock)completion {
if (self.viewControllerPresentingAnimation) {
// UIKit doesn't allow a view controller to be dismissed during the
// animation. The interruption has to be processed when the view controller
// will be fully presented.
// See crbug.com/1126170
DCHECK(!self.interruptCallback);
__weak __typeof(self) weakSelf = self;
self.interruptCallback = ^() {
[weakSelf interruptUserSigninUIWithAction:action completion:completion];
};
return;
}
DCHECK(self.viewController);
DCHECK(self.mediator);
DCHECK(self.unifiedConsentCoordinator);
DCHECK(!self.addAccountSigninCoordinator);
DCHECK(!self.advancedSettingsSigninCoordinator);
__weak UserSigninCoordinator* weakSelf = self;
ProceduralBlock runCompletionCallback = ^{
[weakSelf
runCompletionCallbackWithSigninResult:SigninCoordinatorResultInterrupted
identity:self.unifiedConsentCoordinator
.selectedIdentity
showAdvancedSettingsSignin:NO];
if (completion) {
completion();
}
};
switch (action) {
case SigninCoordinatorInterruptActionNoDismiss: {
[self.mediator cancelAndDismissAuthenticationFlowAnimated:NO];
runCompletionCallback();
break;
}
case SigninCoordinatorInterruptActionDismissWithAnimation: {
[self.mediator cancelAndDismissAuthenticationFlowAnimated:YES];
[self.viewController dismissViewControllerAnimated:YES
completion:runCompletionCallback];
break;
}
case SigninCoordinatorInterruptActionDismissWithoutAnimation: {
[self.mediator cancelAndDismissAuthenticationFlowAnimated:NO];
[self.viewController dismissViewControllerAnimated:NO
completion:runCompletionCallback];
break;
}
}
}
// Returns user policy used to handle existing data when switching signed in
// account.
- (ShouldClearData)shouldClearData {
switch (self.signinIntent) {
case UserSigninIntentFirstRun: {
return SHOULD_CLEAR_DATA_MERGE_DATA;
}
case UserSigninIntentUpgrade:
case UserSigninIntentSignin: {
return SHOULD_CLEAR_DATA_USER_CHOICE;
}
}
}
// Triggers the sign-in workflow.
- (void)startSigninFlow {
DCHECK(self.unifiedConsentCoordinator);
DCHECK(self.unifiedConsentCoordinator.selectedIdentity);
AuthenticationFlow* authenticationFlow = [[AuthenticationFlow alloc]
initWithBrowser:self.browser
identity:self.unifiedConsentCoordinator.selectedIdentity
shouldClearData:[self shouldClearData]
postSignInAction:POST_SIGNIN_ACTION_NONE
presentingViewController:self.viewController];
authenticationFlow.dispatcher = HandlerForProtocol(
self.browser->GetCommandDispatcher(), BrowsingDataCommands);
authenticationFlow.delegate = self.viewController;
self.unifiedConsentCoordinator.uiDisabled = YES;
[self.viewController signinWillStart];
[self.mediator
authenticateWithIdentity:self.unifiedConsentCoordinator.selectedIdentity
authenticationFlow:authenticationFlow];
}
// Triggers |self.signinCompletion| by calling
// |runCompletionCallbackWithSigninResult:identity:| when
// |self.advancedSettingsSigninCoordinator| is done.
- (void)advancedSettingsSigninCoordinatorFinishedWithResult:
(SigninCoordinatorResult)signinResult
identity:(ChromeIdentity*)
identity {
DCHECK(self.advancedSettingsSigninCoordinator);
[self.advancedSettingsSigninCoordinator stop];
self.advancedSettingsSigninCoordinator = nil;
[self runCompletionCallbackWithSigninResult:signinResult
identity:identity
showAdvancedSettingsSignin:NO];
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidDismiss:
(UIPresentationController*)presentationController {
// The view should be dismissible only if there is no sign-in in progress.
// See |presentationControllerShouldDismiss:|.
DCHECK(!self.mediator.isAuthenticationInProgress);
[self cancelSignin];
}
- (BOOL)presentationControllerShouldDismiss:
(UIPresentationController*)presentationController {
switch (self.signinIntent) {
case UserSigninIntentFirstRun: {
return NO;
}
case UserSigninIntentUpgrade:
case UserSigninIntentSignin: {
// Don't dismiss the view controller while the sign-in is in progress.
// To support this, the sign-in flow needs to be canceled after the view
// controller is dimissed, and the UI needs to be blocked until the
// sign-in flow is fully cancelled.
return !self.mediator.isAuthenticationInProgress;
}
}
}
#pragma mark - Methods for unittests
// Returns a UserSigninViewController instance. This method is overriden for
// unittests.
- (UserSigninViewController*)generateUserSigninViewController {
return [[UserSigninViewController alloc] init];
}
@end