// Copyright 2024 The Chromium Authors
// 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/settings/google_services/manage_accounts/accounts_coordinator.h"
#import "base/apple/foundation_util.h"
#import "base/metrics/user_metrics.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/service/sync_service.h"
#import "ios/chrome/browser/ntp/ui_bundled/new_tab_page_feature.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/scene/scene_state.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/authentication/authentication_ui_util.h"
#import "ios/chrome/browser/ui/authentication/signout_action_sheet/signout_action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/scoped_ui_blocker/scoped_ui_blocker.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_mediator.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_mediator_delegate.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_table_view_controller.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_table_view_controller_constants.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/legacy_accounts_table_view_controller.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
using signin_metrics::AccessPoint;
using signin_metrics::PromoAction;
@interface AccountsCoordinator () <AccountsMediatorDelegate,
SettingsNavigationControllerDelegate,
SignoutActionSheetCoordinatorDelegate>
@end
@implementation AccountsCoordinator {
// Mediator.
AccountsMediator* _mediator;
// The view controller.
SettingsRootTableViewController<WithOverridableModelIdentityDataSource>*
_viewController;
BOOL _closeSettingsOnAddAccount;
// Alert coordinator to confirm identity removal.
AlertCoordinator* _confirmRemoveIdentityAlertCoordinator;
// Block the UI when the identity removal is in progress.
std::unique_ptr<ScopedUIBlocker> _UIBlocker;
// Coordinator to display modal alerts to the user.
AlertCoordinator* _errorAlertCoordinator;
// Modal alert for sign out.
SignoutActionSheetCoordinator* _signoutCoordinator;
}
@synthesize baseNavigationController = _baseNavigationController;
- (instancetype)initWithBaseViewController:(UIViewController*)viewController
browser:(Browser*)browser
closeSettingsOnAddAccount:(BOOL)closeSettingsOnAddAccount {
DCHECK(browser);
DCHECK(!browser->GetBrowserState()->IsOffTheRecord());
self = [super initWithBaseViewController:viewController browser:browser];
if (self) {
_closeSettingsOnAddAccount = closeSettingsOnAddAccount;
}
return self;
}
- (instancetype)initWithBaseNavigationController:
(UINavigationController*)navigationController
browser:(Browser*)browser
closeSettingsOnAddAccount:
(BOOL)closeSettingsOnAddAccount {
DCHECK(browser);
DCHECK(!browser->GetBrowserState()->IsOffTheRecord());
if ((self = [super initWithBaseViewController:navigationController
browser:browser])) {
_closeSettingsOnAddAccount = closeSettingsOnAddAccount;
_baseNavigationController = navigationController;
}
return self;
}
- (void)start {
base::RecordAction(base::UserMetricsAction("Signin_AccountsTableView_Open"));
ChromeBrowserState* browserState = self.browser->GetBrowserState();
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(browserState);
_mediator = [[AccountsMediator alloc]
initWithSyncService:syncService
accountManagerService:ChromeAccountManagerServiceFactory::
GetForBrowserState(browserState)
authService:AuthenticationServiceFactory::GetForBrowserState(
browserState)
identityManager:IdentityManagerFactory::GetForBrowserState(
browserState)];
if (base::FeatureList::IsEnabled(kIdentityDiscAccountMenu) &&
!syncService->HasSyncConsent()) {
AccountsTableViewController* viewController =
[[AccountsTableViewController alloc]
initWithCloseSettingsOnAddAccount:_closeSettingsOnAddAccount
applicationCommandsHandler:HandlerForProtocol(
self.browser
->GetCommandDispatcher(),
ApplicationCommands)
offerSignout:self.showSignoutButton];
_viewController = viewController;
_mediator.consumer = viewController;
_mediator.delegate = self;
_viewController.modelIdentityDataSource = _mediator;
AccountsTableViewController* accountsTableViewController =
base::apple::ObjCCast<AccountsTableViewController>(_viewController);
accountsTableViewController.mutator = _mediator;
} else {
LegacyAccountsTableViewController* viewController =
[[LegacyAccountsTableViewController alloc]
initWithBrowser:self.browser
closeSettingsOnAddAccount:_closeSettingsOnAddAccount
applicationCommandsHandler:
HandlerForProtocol(
self.browser->GetCommandDispatcher(),
ApplicationCommands)
signoutDismissalByParentCoordinator:
self.signoutDismissalByParentCoordinator];
_viewController = viewController;
_mediator.consumer = viewController;
_viewController.modelIdentityDataSource = _mediator;
}
if (_baseNavigationController) {
[self.baseNavigationController pushViewController:_viewController
animated:YES];
} else {
SettingsNavigationController* navigationController =
[[SettingsNavigationController alloc]
initWithRootViewController:_viewController
browser:self.browser
delegate:self];
UIBarButtonItem* doneButton = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(closeSettings)];
doneButton.accessibilityIdentifier = kSettingsAccountsTableViewDoneButtonId;
_viewController.navigationItem.rightBarButtonItem = doneButton;
[self.baseViewController presentViewController:navigationController
animated:YES
completion:nil];
}
}
- (void)stop {
[super stop];
AccountsTableViewController* accountsTableViewController =
base::apple::ObjCCast<AccountsTableViewController>(_viewController);
if (accountsTableViewController) {
accountsTableViewController.mutator = nil;
}
[_signoutCoordinator stop];
_signoutCoordinator = nil;
_viewController.modelIdentityDataSource = nil;
_viewController = nil;
_mediator.consumer = nil;
[_mediator disconnect];
_mediator = nil;
}
#pragma mark - SettingsNavigationControllerDelegate
- (void)closeSettings {
base::RecordAction(base::UserMetricsAction("Signin_AccountsTableView_Close"));
if ([_viewController respondsToSelector:@selector(settingsWillBeDismissed)]) {
[_viewController performSelector:@selector(settingsWillBeDismissed)];
}
[_viewController.navigationController dismissViewControllerAnimated:YES
completion:nil];
[self stop];
}
- (void)settingsWasDismissed {
[self stop];
}
#pragma mark - SignoutActionSheetCoordinatorDelegate
- (void)signoutActionSheetCoordinatorPreventUserInteraction:
(SignoutActionSheetCoordinator*)coordinator {
[_viewController preventUserInteraction];
}
- (void)signoutActionSheetCoordinatorAllowUserInteraction:
(SignoutActionSheetCoordinator*)coordinator {
[_viewController allowUserInteraction];
}
#pragma mark - AccountsMediatorDelegate
- (void)handleRemoveIdentity:(id<SystemIdentity>)identity
itemView:(UIView*)itemView {
_confirmRemoveIdentityAlertCoordinator = [[ActionSheetCoordinator alloc]
initWithBaseViewController:_viewController
browser:self.browser
title:nil
message:
l10n_util::GetNSString(
IDS_IOS_REMOVE_ACCOUNT_CONFIRMATION_MESSAGE)
rect:itemView.frame
view:itemView];
__weak __typeof(self) weakSelf = self;
// TODO(crbug.com/349071402): Record actions.
[_confirmRemoveIdentityAlertCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_REMOVE_ACCOUNT_LABEL)
action:^{
[weakSelf removeAccountDialogConfirmedWithIdentity:identity];
}
style:UIAlertActionStyleDestructive];
[_confirmRemoveIdentityAlertCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:^() {
[weakSelf handleAlertCoordinatorCancel];
}
style:UIAlertActionStyleCancel];
[_confirmRemoveIdentityAlertCoordinator start];
}
- (void)showAddAccountToDevice {
[_viewController preventUserInteraction];
__weak __typeof(self) weakSelf = self;
ShowSigninCommand* command = [[ShowSigninCommand alloc]
initWithOperation:AuthenticationOperation::kAddAccount
identity:nil
accessPoint:AccessPoint::ACCESS_POINT_SETTINGS
promoAction:PromoAction::PROMO_ACTION_NO_SIGNIN_PROMO
callback:^(SigninCoordinatorResult result,
SigninCompletionInfo* completionInfo) {
[weakSelf addAccountToDeviceCompleted];
}];
[HandlerForProtocol(self.browser->GetCommandDispatcher(), ApplicationCommands)
showSignin:command
baseViewController:_viewController];
}
- (void)signOutWithItemView:(UIView*)itemView {
DCHECK(!_signoutCoordinator);
_signoutCoordinator = [[SignoutActionSheetCoordinator alloc]
initWithBaseViewController:_viewController
browser:self.browser
rect:itemView.bounds
view:itemView
withSource:signin_metrics::ProfileSignout::
kUserClickedSignoutSettings];
__weak __typeof(self) weakSelf = self;
_signoutCoordinator.completion = ^(BOOL success) {
[weakSelf handleSignOutCompleted:success];
};
_signoutCoordinator.delegate = self;
[_signoutCoordinator start];
}
#pragma mark - Private
- (void)removeAccountDialogConfirmedWithIdentity:(id<SystemIdentity>)identity {
[self dismissConfirmRemoveIdentityAlertCoordinator];
NSArray<id<SystemIdentity>>* allIdentities =
ChromeAccountManagerServiceFactory::GetForBrowserState(
self.browser->GetBrowserState())
->GetAllIdentities();
if (![allIdentities containsObject:identity]) {
// If the identity was removed by another way (another window, another app
// or by gaia), there is nothing to do.
return;
}
SceneState* sceneState = self.browser->GetSceneState();
_UIBlocker = std::make_unique<ScopedUIBlocker>(sceneState);
[_viewController preventUserInteraction];
__weak __typeof(self) weakSelf = self;
GetApplicationContext()->GetSystemIdentityManager()->ForgetIdentity(
identity, base::BindOnce(
[](__typeof(self) strongSelf, NSError* error) {
if (error) {
LOG(ERROR) << "Failed to remove idenity.";
[strongSelf forgetIdentityFailedWithError:error];
}
[strongSelf forgetIdentityDone];
},
weakSelf));
}
- (void)forgetIdentityDone {
_UIBlocker.reset();
[_viewController allowUserInteraction];
}
- (void)forgetIdentityFailedWithError:(NSError*)error {
DCHECK(error);
__weak __typeof(self) weakSelf = self;
ProceduralBlock dismissAction = ^{
[weakSelf dismissAlertCoordinator];
};
_errorAlertCoordinator =
ErrorCoordinator(error, dismissAction, _viewController, self.browser);
[_errorAlertCoordinator start];
}
- (void)dismissAlertCoordinator {
[_errorAlertCoordinator stop];
_errorAlertCoordinator = nil;
}
- (void)dismissConfirmRemoveIdentityAlertCoordinator {
[_confirmRemoveIdentityAlertCoordinator stop];
_confirmRemoveIdentityAlertCoordinator = nil;
}
- (void)handleAlertCoordinatorCancel {
DCHECK(_confirmRemoveIdentityAlertCoordinator);
[_confirmRemoveIdentityAlertCoordinator stop];
_confirmRemoveIdentityAlertCoordinator = nil;
}
- (void)addAccountToDeviceCompleted {
[_viewController allowUserInteraction];
}
- (void)handleSignOutCompleted:(BOOL)success {
[_signoutCoordinator stop];
_signoutCoordinator = nil;
if (!success) {
return;
}
ChromeBrowserState* browserState = self.browser->GetBrowserState();
CHECK(!AuthenticationServiceFactory::GetForBrowserState(browserState)
->HasPrimaryIdentity(signin::ConsentLevel::kSignin));
[self closeSettings];
}
@end