chromium/ios/chrome/browser/ui/settings/google_services/manage_accounts/legacy_accounts_table_view_controller.mm

// Copyright 2015 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/legacy_accounts_table_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/memory/raw_ptr.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_service_utils.h"
#import "components/trusted_vault/trusted_vault_server_constants.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/settings/model/sync/utils/account_error_ui_info.h"
#import "ios/chrome/browser/settings/model/sync/utils/identity_error_util.h"
#import "ios/chrome/browser/settings/model/sync/utils/sync_util.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/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/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/commands/show_signin_command.h"
#import "ios/chrome/browser/shared/ui/symbols/chrome_icon.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_detail_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_link_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_model.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.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/cells/table_view_account_item.h"
#import "ios/chrome/browser/ui/authentication/enterprise/enterprise_utils.h"
#import "ios/chrome/browser/ui/authentication/signin/signin_constants.h"
#import "ios/chrome/browser/ui/authentication/signout_action_sheet/signout_action_sheet_coordinator.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/google_services/manage_accounts/accounts_model_identity_data_source.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/identity_view_item.h"
#import "ios/chrome/browser/ui/settings/settings_root_view_controlling.h"
#import "ios/chrome/browser/ui/settings/sync/sync_encryption_passphrase_table_view_controller.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

using signin_metrics::AccessPoint;
using signin_metrics::PromoAction;
using DismissViewCallback = SystemIdentityManager::DismissViewCallback;

namespace {

// The size of the symbol image.
const CGFloat kSymbolAddAccountPointSize = 20;

typedef NS_ENUM(NSInteger, AccountsSectionIdentifier) {
  SectionIdentifierAccounts = kSectionIdentifierEnumZero,
  SectionIdentifierError,
  SectionIdentifierSignOut,
};

typedef NS_ENUM(NSInteger, AccountsItemType) {
  ItemTypeAccount = kItemTypeEnumZero,
  // Sign in item.
  ItemTypeSignInHeader,
  ItemTypeAddAccount,
  // Indicates that restricted accounts are removed from the list.
  ItemTypeRestrictedAccountsFooter,
  // Provides sign out items used only for non-managed accounts.
  ItemTypeSignOut,
  // Detailed description of the actions taken by sign out, e.g. turning off
  // sync.
  ItemTypeSignOutSyncingFooter,
  // Indicates the errors related to the signed in account.
  ItemTypeAccountErrorMessage,
  // Button to resolve the account error.
  ItemTypeAccountErrorButton,
};

// Size of the symbols.
constexpr CGFloat kErrorSymbolSize = 22.;

}  // namespace

@interface LegacyAccountsTableViewController () <
    SignoutActionSheetCoordinatorDelegate> {
  raw_ptr<Browser> _browser;
  BOOL _closeSettingsOnAddAccount;
  // Whether an authentication operation is in progress (e.g switch accounts,
  // sign out).
  BOOL _authenticationOperationInProgress;
  // Whether the view controller is currently being dismissed and new dismiss
  // requests should be ignored.
  BOOL _isBeingDismissed;

  // Enable lookup of item corresponding to a given identity GAIA ID string.
  NSDictionary<NSString*, TableViewItem*>* _identityMap;

  // The type of account error that is being displayed in the error section for
  // syncing accounts. Is set to kNone when there is no error section.
  syncer::SyncService::UserActionableError _diplayedAccountErrorType;

  // The type of actionable the syncing user needs to take to resolve the error.
  AccountErrorUserActionableType _accountErrorUserActionableType;

  // ApplicationCommands handler.
  id<ApplicationCommands> _applicationHandler;

  // If YES, AccountsTableViewController should not dismiss itself only for a
  // sign-out reason. The parent coordinator is responsible to dismiss this
  // coordinator when a sign-out happens.
  BOOL _signoutDismissalByParentCoordinator;
}

// Modal alert to choose between remove an identity and show MyGoogle UI.
@property(nonatomic, strong)
    AlertCoordinator* removeOrMyGoogleChooserAlertCoordinator;

// Modal alert for confirming account removal.
@property(nonatomic, strong) AlertCoordinator* removeAccountCoordinator;

// Modal alert for sign out.
@property(nonatomic, strong) SignoutActionSheetCoordinator* signoutCoordinator;

// If YES, the UI elements are disabled.
@property(nonatomic, assign) BOOL uiDisabled;

@end

@implementation LegacyAccountsTableViewController {
  // Callback to dismiss MyGoogle (Account Detail).
  DismissViewCallback _accountDetailsControllerDismissCallback;
}

@synthesize modelIdentityDataSource;

- (instancetype)initWithBrowser:(Browser*)browser
              closeSettingsOnAddAccount:(BOOL)closeSettingsOnAddAccount
             applicationCommandsHandler:
                 (id<ApplicationCommands>)applicationCommandsHandler
    signoutDismissalByParentCoordinator:
        (BOOL)signoutDismissalByParentCoordinator {
  DCHECK(browser);
  DCHECK(!browser->GetBrowserState()->IsOffTheRecord());

  self = [super initWithStyle:ChromeTableViewStyle()];
  if (self) {
    _browser = browser;
    _closeSettingsOnAddAccount = closeSettingsOnAddAccount;
    _applicationHandler = applicationCommandsHandler;
    _signoutDismissalByParentCoordinator = signoutDismissalByParentCoordinator;
    _accountErrorUserActionableType = AccountErrorUserActionableType::kNoAction;
  }

  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  self.tableView.accessibilityIdentifier = kSettingsLegacyAccountsTableViewId;

  [self loadModel];
}

#pragma mark - SettingsControllerProtocol

- (void)reportDismissalUserAction {
  base::RecordAction(base::UserMetricsAction("MobileAccountsSettingsClose"));
}

- (void)reportBackUserAction {
  base::RecordAction(base::UserMetricsAction("MobileAccountsSettingsBack"));
}

- (void)settingsWillBeDismissed {
  if (!_accountDetailsControllerDismissCallback.is_null()) {
    DCHECK(self.presentedViewController);
    DCHECK(!self.signoutCoordinator);
    DCHECK(!self.removeOrMyGoogleChooserAlertCoordinator);
    DCHECK(!self.removeAccountCoordinator);
    std::move(_accountDetailsControllerDismissCallback).Run(/*animated=*/false);
  }
  [self dismissRemoveOrMyGoogleChooserAlert];
  [self.signoutCoordinator stop];
  self.signoutCoordinator = nil;
  [self dismissRemoveAccountCoordinator];
  _browser = nullptr;

  _isBeingDismissed = YES;
}

#pragma mark - SettingsRootTableViewController

- (void)reloadData {
  if (!_browser)
    return;

  if (![self authService]->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
    // This accounts table view will be popped or dismissed when the user
    // is signed out. Avoid reloading it in that case as that would lead to an
    // empty table view.
    return;
  }
  [super reloadData];
}

- (void)loadModel {
  if (!_browser)
    return;

  // Update the title with the name with the currently signed-in account.
  AuthenticationService* authService = self.authService;
  id<SystemIdentity> authenticatedIdentity =
      authService->GetPrimaryIdentity(signin::ConsentLevel::kSignin);

  self.title = l10n_util::GetNSString(
      IDS_IOS_GOOGLE_ACCOUNTS_MANAGEMENT_FROM_ACCOUNT_SETTINGS_TITLE);

  [super loadModel];

  if (!authService->HasPrimaryIdentity(signin::ConsentLevel::kSignin))
    return;

  TableViewModel* model = self.tableViewModel;

  NSMutableDictionary<NSString*, TableViewItem*>* mutableIdentityMap =
      [[NSMutableDictionary alloc] init];

  // Account cells.
  [model addSectionWithIdentifier:SectionIdentifierAccounts];
  [model setHeader:[self signInHeader]
      forSectionWithIdentifier:SectionIdentifierAccounts];

  NSString* authenticatedEmail = authenticatedIdentity.userEmail;
  for (const auto& identityViewItem :
       [self.modelIdentityDataSource identityViewItems]) {
    if (!identityViewItem) {
      // Ignore the case in which the identity is invalid at lookup time. This
      // may be due to inconsistencies between the identity service and
      // ProfileOAuth2TokenService.
      continue;
    }
    // TODO(crbug.com/40691260): This re-ordering will be redundant once we
    // apply ordering changes to the account reconciler.
    TableViewItem* item =
        [self accountItemWithIdentityViewItem:identityViewItem];
    if ([identityViewItem.userEmail isEqualToString:authenticatedEmail]) {
      [model insertItem:item
          inSectionWithIdentifier:SectionIdentifierAccounts
                          atIndex:0];
    } else {
      [model addItem:item toSectionWithIdentifier:SectionIdentifierAccounts];
    }

    [mutableIdentityMap setObject:item forKey:identityViewItem.gaiaID];
  }
  _identityMap = mutableIdentityMap;

  [model addItem:[self addAccountItem]
      toSectionWithIdentifier:SectionIdentifierAccounts];

  if (IsRestrictAccountsToPatternsEnabled()) {
    [model setFooter:[self restrictedIdentitiesFooterItem]
        forSectionWithIdentifier:SectionIdentifierAccounts];
  }

  // Account Storage errors section.
  [self updateErrorSectionModelAndReloadViewIfNeeded:NO];

  // Sign out section.
  [model addSectionWithIdentifier:SectionIdentifierSignOut];
  [model addItem:[self signOutItem]
      toSectionWithIdentifier:SectionIdentifierSignOut];

  // TODO(crbug.com/40066949): Simplify once kSync becomes unreachable or is
  // deleted from the codebase. See ConsentLevel::kSync documentation for
  // details.
  BOOL hasSyncConsent =
      authService->HasPrimaryIdentity(signin::ConsentLevel::kSync);
  TableViewLinkHeaderFooterItem* footerItem = nil;
  if ([self authService]->GetServiceStatus() ==
      AuthenticationService::ServiceStatus::SigninForcedByPolicy) {
    if (authService->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
      footerItem =
          [self signOutSyncingFooterItemForForcedSignin:hasSyncConsent];
    }
  } else if (hasSyncConsent) {
    footerItem = [self signOutSyncingFooterItem];
  }

  [model setFooter:footerItem
      forSectionWithIdentifier:SectionIdentifierSignOut];
}

#pragma mark - Model objects

- (TableViewTextHeaderFooterItem*)signInHeader {
  TableViewTextHeaderFooterItem* header =
      [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeSignInHeader];
  header.text = l10n_util::GetNSString(IDS_IOS_OPTIONS_ACCOUNTS_DESCRIPTION);
  return header;
}

- (TableViewLinkHeaderFooterItem*)signOutSyncingFooterItem {
  TableViewLinkHeaderFooterItem* footer = [[TableViewLinkHeaderFooterItem alloc]
      initWithType:ItemTypeSignOutSyncingFooter];
  footer.text = l10n_util::GetNSString(
      IDS_IOS_DISCONNECT_DIALOG_SYNCING_FOOTER_INFO_MOBILE);
  return footer;
}

- (TableViewLinkHeaderFooterItem*)signOutSyncingFooterItemForForcedSignin:
    (BOOL)syncConsent {
  TableViewLinkHeaderFooterItem* footer = [[TableViewLinkHeaderFooterItem alloc]
      initWithType:ItemTypeSignOutSyncingFooter];

  if (syncConsent) {
    NSString* text = l10n_util::GetNSString(
        IDS_IOS_DISCONNECT_DIALOG_SYNCING_FOOTER_INFO_MOBILE);
    text = [text stringByAppendingString:@"\n\n"];
    text = [text
        stringByAppendingString:
            l10n_util::GetNSString(
                IDS_IOS_ENTERPRISE_FORCED_SIGNIN_MESSAGE_WITH_LEARN_MORE)];
    footer.text = text;
  } else {
    footer.text = l10n_util::GetNSString(
        IDS_IOS_ENTERPRISE_FORCED_SIGNIN_MESSAGE_WITH_LEARN_MORE);
  }

  footer.urls = @[ [[CrURL alloc] initWithGURL:GURL(kChromeUIManagementURL)] ];
  return footer;
}

- (TableViewLinkHeaderFooterItem*)restrictedIdentitiesFooterItem {
  TableViewLinkHeaderFooterItem* footer = [[TableViewLinkHeaderFooterItem alloc]
      initWithType:ItemTypeRestrictedAccountsFooter];
  footer.text =
      l10n_util::GetNSString(IDS_IOS_OPTIONS_ACCOUNTS_RESTRICTED_IDENTITIES);
  footer.urls = @[ [[CrURL alloc] initWithGURL:GURL(kChromeUIManagementURL)] ];
  return footer;
}

- (TableViewItem*)accountItemWithIdentityViewItem:
    (IdentityViewItem*)identityViewItem {
  TableViewAccountItem* item =
      [[TableViewAccountItem alloc] initWithType:ItemTypeAccount];
  [self updateAccountItem:item withIdentityViewItem:identityViewItem];
  return item;
}

- (void)updateAccountItem:(TableViewAccountItem*)item
     withIdentityViewItem:(IdentityViewItem*)identityViewItem {
  item.image = identityViewItem.avatar;
  item.text = identityViewItem.userEmail;
  item.identity =
      [self.modelIdentityDataSource identityWithGaiaID:identityViewItem.gaiaID];
  item.accessibilityIdentifier = identityViewItem.accessibilityIdentifier;
  item.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}

- (TableViewItem*)addAccountItem {
  TableViewAccountItem* item =
      [[TableViewAccountItem alloc] initWithType:ItemTypeAddAccount];
  item.text =
      l10n_util::GetNSString(IDS_IOS_OPTIONS_ACCOUNTS_ADD_ACCOUNT_BUTTON);
  item.accessibilityIdentifier = kSettingsAccountsTableViewAddAccountCellId;
  item.image = CustomSymbolWithPointSize(kPlusCircleFillSymbol,
                                         kSymbolAddAccountPointSize);
  return item;
}

- (TableViewItem*)signOutItem {
  TableViewTextItem* item =
      [[TableViewTextItem alloc] initWithType:ItemTypeSignOut];
  item.text =
      l10n_util::GetNSString(IDS_IOS_DISCONNECT_DIALOG_CONTINUE_BUTTON_MOBILE);
  item.textColor = [self.modelIdentityDataSource isAccountSignedInNotSyncing]
                       ? [UIColor colorNamed:kBlueColor]
                       : [UIColor colorNamed:kRedColor];
  item.accessibilityTraits |= UIAccessibilityTraitButton;
  item.accessibilityIdentifier = kSettingsAccountsTableViewSignoutCellId;
  return item;
}

// Initializes the passphrase error message item.
- (TableViewItem*)accountErrorMessageItemWithMessageID:(int)messageID {
  SettingsImageDetailTextItem* item = [[SettingsImageDetailTextItem alloc]
      initWithType:ItemTypeAccountErrorMessage];
  item.detailText = l10n_util::GetNSString(messageID);
  item.image =
      DefaultSymbolWithPointSize(kErrorCircleFillSymbol, kErrorSymbolSize);
  item.imageViewTintColor = [UIColor colorNamed:kRed500Color];
  return item;
}

// Initializes the passphrase error button to open the passphrase dialog.
- (TableViewItem*)accountErrorButtonItemWithLabelID:(int)labelID {
  TableViewTextItem* item =
      [[TableViewTextItem alloc] initWithType:ItemTypeAccountErrorButton];
  item.text = l10n_util::GetNSString(labelID);
  item.textColor = [UIColor colorNamed:kBlueColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  return item;
}

#pragma mark - UITableViewDataSource

- (UIView*)tableView:(UITableView*)tableView
    viewForFooterInSection:(NSInteger)section {
  UIView* view = [super tableView:tableView viewForFooterInSection:section];
  NSInteger sectionIdentifier =
      [self.tableViewModel sectionIdentifierForSectionIndex:section];
  switch (sectionIdentifier) {
    case SectionIdentifierAccounts:
    case SectionIdentifierSignOut: {
      // Might be a different type of footer.
      TableViewLinkHeaderFooterView* linkView =
          base::apple::ObjCCast<TableViewLinkHeaderFooterView>(view);
      linkView.delegate = self;
      break;
    }
  }
  return view;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  // If there is an operation in process that does not allow selecting a cell or
  // if the settings will be dismissed, exit without performing the selection.
  if (self.uiDisabled || _isBeingDismissed) {
    return;
  }

  [super tableView:tableView didSelectRowAtIndexPath:indexPath];

  AccountsItemType itemType = static_cast<AccountsItemType>(
      [self.tableViewModel itemTypeForIndexPath:indexPath]);
  switch (itemType) {
    case ItemTypeAccount: {
      base::RecordAction(
          base::UserMetricsAction("Signin_AccountsTableView_AccountDetail"));
      TableViewAccountItem* item =
          base::apple::ObjCCastStrict<TableViewAccountItem>(
              [self.tableViewModel itemAtIndexPath:indexPath]);
      DCHECK(item.identity);

      UIView* itemView =
          [[tableView cellForRowAtIndexPath:indexPath] contentView];
      [self showAccountDetails:item.identity itemView:itemView];
      break;
    }
    case ItemTypeAddAccount: {
      base::RecordAction(
          base::UserMetricsAction("Signin_AccountsTableView_AddAccount"));
      [self showAddAccount];
      break;
    }
    case ItemTypeSignOut: {
      base::RecordAction(
          base::UserMetricsAction("Signin_AccountsTableView_SignOut"));
      UIView* itemView =
          [[tableView cellForRowAtIndexPath:indexPath] contentView];
      [self showSignOutWithItemView:itemView];
      break;
    }
    case ItemTypeAccountErrorButton: {
      base::RecordAction(
          base::UserMetricsAction("Signin_AccountsTableView_ErrorButton"));
      [self handleAccountErrorUserActionable];
      break;
    }
    case ItemTypeAccountErrorMessage:
      // Do not handle row selection on the account error message item because
      // its selection is disabled. The only purpose of the item is to show a
      // message that gives details on the error.
      break;
    case ItemTypeSignInHeader:
    case ItemTypeSignOutSyncingFooter:
    case ItemTypeRestrictedAccountsFooter:
      NOTREACHED_IN_MIGRATION();
      break;
  }

  [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark - Authentication operations

- (void)showAddAccount {
  DCHECK(!self.removeOrMyGoogleChooserAlertCoordinator);
  _authenticationOperationInProgress = YES;

  // TODO(crbug.com/40229802): Remove the following line when todo bug will be
  // fixed.
  [self 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) {
                 BOOL success = result == SigninCoordinatorResultSuccess;
                 [weakSelf handleDidAddAccount:success];
               }];
  [_applicationHandler showSignin:command baseViewController:self];
}

- (void)handleDidAddAccount:(BOOL)success {
  // TODO(crbug.com/40229802): Remove the following line when todo bug will be
  // fixed.
  [self allowUserInteraction];
  [self handleAuthenticationOperationDidFinish];
  if (success && _closeSettingsOnAddAccount) {
    [_applicationHandler closeSettingsUI];
  }
}

- (void)showAccountDetails:(id<SystemIdentity>)identity
                  itemView:(UIView*)itemView {
  if (self.removeOrMyGoogleChooserAlertCoordinator) {
    // It is possible for the user to tap twice on the cell. If the action
    // sheet coordinator already exists, we need to ignore the second tap.
    // Related to crbug.com/1497100.
    return;
  }
  self.removeOrMyGoogleChooserAlertCoordinator = [[ActionSheetCoordinator alloc]
      initWithBaseViewController:self
                         browser:_browser
                           title:nil
                         message:identity.userEmail
                            rect:itemView.frame
                            view:itemView];
  __weak __typeof(self) weakSelf = self;
  [self.removeOrMyGoogleChooserAlertCoordinator
      addItemWithTitle:l10n_util::GetNSString(
                           IDS_IOS_MANAGE_YOUR_GOOGLE_ACCOUNT_TITLE)
                action:^{
                  base::RecordAction(base::UserMetricsAction(
                      "Signin_AccountsTableView_AccountDetail_OpenMyGoogleUI"));
                  [weakSelf handleManageGoogleAccountWithIdentity:identity];
                  [weakSelf dismissRemoveOrMyGoogleChooserAlert];
                }
                 style:UIAlertActionStyleDefault];
  [self.removeOrMyGoogleChooserAlertCoordinator
      addItemWithTitle:l10n_util::GetNSString(
                           IDS_IOS_REMOVE_GOOGLE_ACCOUNT_TITLE)
                action:^{
                  base::RecordAction(base::UserMetricsAction(
                      "Signin_AccountsTableView_AccountDetail_RemoveAccount"));
                  [weakSelf handleRemoveAccountWithIdentity:identity];
                  [weakSelf dismissRemoveOrMyGoogleChooserAlert];
                }
                 style:UIAlertActionStyleDestructive];
  [self.removeOrMyGoogleChooserAlertCoordinator
      addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
                action:^() {
                  base::RecordAction(base::UserMetricsAction(
                      "Signin_AccountsTableView_AccountDetail_Cancel"));
                  [weakSelf handleAlertCoordinatorCancel];
                  [weakSelf dismissRemoveOrMyGoogleChooserAlert];
                }
                 style:UIAlertActionStyleCancel];
  [self.removeOrMyGoogleChooserAlertCoordinator start];
}

// Handles the manage Google account action from
// `self.removeOrMyGoogleChooserAlertCoordinator`. Action sheet created in
// `showAccountDetails:itemView:`
- (void)handleManageGoogleAccountWithIdentity:(id<SystemIdentity>)identity {
  DCHECK(self.removeOrMyGoogleChooserAlertCoordinator);
  // `self.removeOrMyGoogleChooserAlertCoordinator` should not be stopped, since
  // the coordinator has been confirmed.
  self.removeOrMyGoogleChooserAlertCoordinator = nil;
  __strong __typeof(self) weakSelf = self;
  _accountDetailsControllerDismissCallback =
      GetApplicationContext()
          ->GetSystemIdentityManager()
          ->PresentAccountDetailsController(
              identity, self,
              /*animated=*/YES,
              base::BindOnce(
                  [](__typeof(self) strongSelf) {
                    [strongSelf accountDetailsControllerWasDismissed];
                  },
                  weakSelf));
}

// Handles the account remove action from
// `self.removeOrMyGoogleChooserAlertCoordinator`. Action sheet created in
// `showAccountDetails:itemView:`
- (void)handleRemoveAccountWithIdentity:(id<SystemIdentity>)identity {
  DCHECK(self.removeOrMyGoogleChooserAlertCoordinator);
  // `self.removeOrMyGoogleChooserAlertCoordinator` should not be stopped, since
  // the coordinator has been confirmed.
  self.removeOrMyGoogleChooserAlertCoordinator = nil;
  DCHECK(!self.removeAccountCoordinator);
  NSString* title =
      l10n_util::GetNSStringF(IDS_IOS_REMOVE_ACCOUNT_ALERT_TITLE,
                              base::SysNSStringToUTF16(identity.userEmail));
  NSString* message =
      l10n_util::GetNSString(IDS_IOS_REMOVE_ACCOUNT_CONFIRMATION_MESSAGE);
  self.removeAccountCoordinator =
      [[AlertCoordinator alloc] initWithBaseViewController:self
                                                   browser:_browser
                                                     title:title
                                                   message:message];
  __weak __typeof(self) weakSelf = self;
  [self.removeAccountCoordinator
      addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
                action:^{
                  base::RecordAction(base::UserMetricsAction(
                      "Signin_AccountsTableView_AccountDetail_RemoveAccount_"
                      "ConfirmationCancelled"));
                  weakSelf.removeAccountCoordinator = nil;
                  [weakSelf dismissRemoveAccountCoordinator];
                }
                 style:UIAlertActionStyleCancel];
  [self.removeAccountCoordinator
      addItemWithTitle:l10n_util::GetNSString(IDS_IOS_REMOVE_ACCOUNT_LABEL)
                action:^{
                  base::RecordAction(base::UserMetricsAction(
                      "Signin_AccountsTableView_AccountDetail_RemoveAccount_"
                      "Confirmed"));
                  [weakSelf removeIdentity:identity];
                  [weakSelf dismissRemoveAccountCoordinator];
                }
                 style:UIAlertActionStyleDestructive];
  [self.removeAccountCoordinator start];
}

- (void)removeIdentity:(id<SystemIdentity>)identity {
  DCHECK(self.removeAccountCoordinator);
  self.removeAccountCoordinator = nil;
  self.uiDisabled = YES;
  __weak __typeof(self) weakSelf = self;
  GetApplicationContext()->GetSystemIdentityManager()->ForgetIdentity(
      identity, base::BindOnce(^(NSError* error) {
        weakSelf.uiDisabled = NO;
      }));
}

// Offer the user to sign-out near itemView
// If they sync, they can keep or delete their data.
- (void)showSignOutWithItemView:(UIView*)itemView {
  DCHECK(!self.signoutCoordinator);
  if (_authenticationOperationInProgress ||
      self != [self.navigationController topViewController]) {
    // An action is already in progress, ignore user's request.
    return;
  }
  self.signoutCoordinator = [[SignoutActionSheetCoordinator alloc]
      initWithBaseViewController:self
                         browser:_browser
                            rect:itemView.frame
                            view:itemView
                      withSource:signin_metrics::ProfileSignout::
                                     kUserClickedSignoutSettings];
  __weak LegacyAccountsTableViewController* weakSelf = self;
  self.signoutCoordinator.completion = ^(BOOL success) {
    [weakSelf.signoutCoordinator stop];
    weakSelf.signoutCoordinator = nil;
    if (success) {
      [weakSelf handleAuthenticationOperationDidFinish];
    }
  };
  self.signoutCoordinator.delegate = self;
  [self.signoutCoordinator start];
}

// Handles the cancel action for `self.removeOrMyGoogleChooserAlertCoordinator`.
- (void)handleAlertCoordinatorCancel {
  DCHECK(self.removeOrMyGoogleChooserAlertCoordinator);
  // `self.removeOrMyGoogleChooserAlertCoordinator` should not be stopped, since
  // the coordinator has been cancelled.
  self.removeOrMyGoogleChooserAlertCoordinator = nil;
}

// Sets `_authenticationOperationInProgress` to NO and pops this accounts
// table view controller if the user is signed out.
- (void)handleAuthenticationOperationDidFinish {
  DCHECK(_authenticationOperationInProgress);
  _authenticationOperationInProgress = NO;
  [self popViewIfSignedOut];
}

- (void)popViewIfSignedOut {
  if (!_browser)
    return;

  if ([self authService]->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
    return;
  }
  if (_authenticationOperationInProgress) {
    // The signed out state might be temporary (e.g. account switch, ...).
    // Don't pop this view based on intermediary values.
    return;
  }
  if (_isBeingDismissed || _signoutDismissalByParentCoordinator) {
    return;
  }
  _isBeingDismissed = YES;
  __weak __typeof(self) weakSelf = self;
  void (^popAccountsTableViewController)() = ^() {
    [base::apple::ObjCCastStrict<SettingsNavigationController>(
        weakSelf.navigationController)
        popViewControllerOrCloseSettingsAnimated:YES];
  };
  if (!_accountDetailsControllerDismissCallback.is_null()) {
    DCHECK(self.presentedViewController);
    DCHECK(!self.removeOrMyGoogleChooserAlertCoordinator);
    DCHECK(!self.removeAccountCoordinator);
    DCHECK(!self.signoutCoordinator);
    // TODO(crbug.com/40056250): Need to add a completion block in
    // `dismissAccountDetailsViewControllerBlock` callback, to trigger
    // `popAccountsTableViewController()`.
    // Once we have a completion block, we can set `animated` to YES.
    std::move(_accountDetailsControllerDismissCallback).Run(/*animated=*/false);
    popAccountsTableViewController();
  } else if (self.removeOrMyGoogleChooserAlertCoordinator ||
             self.removeAccountCoordinator || self.signoutCoordinator) {
    DCHECK(self.presentedViewController);
    // If `self` is presenting a view controller (like
    // `self.removeOrMyGoogleChooserAlertCoordinator`,
    // `self.removeAccountCoordinator`, it has to be dismissed before `self` can
    // be poped from the navigation controller.
    // This issue can be easily reproduced with EG tests, but not with Chrome
    // app itself.
    [self
        dismissViewControllerAnimated:NO
                           completion:^{
                             [weakSelf.removeOrMyGoogleChooserAlertCoordinator
                                     stop];
                             weakSelf.removeOrMyGoogleChooserAlertCoordinator =
                                 nil;
                             [weakSelf.removeAccountCoordinator stop];
                             weakSelf.removeAccountCoordinator = nil;
                             [weakSelf.signoutCoordinator stop];
                             weakSelf.signoutCoordinator = nil;
                             popAccountsTableViewController();
                           }];
  } else {
    DCHECK(!self.presentedViewController);
    // Pops `self`.
    popAccountsTableViewController();
  }
}

#pragma mark - Access to authentication service

- (AuthenticationService*)authService {
  DCHECK(_browser) << "-authService called after -settingsWillBeDismissed";
  return AuthenticationServiceFactory::GetForBrowserState(
      _browser->GetBrowserState());
}

#pragma mark - UIAdaptivePresentationControllerDelegate

- (void)presentationControllerDidDismiss:
    (UIPresentationController*)presentationController {
  base::RecordAction(
      base::UserMetricsAction("IOSAccountsSettingsCloseWithSwipe"));
}

#pragma mark - SignoutActionSheetCoordinatorDelegate

- (void)signoutActionSheetCoordinatorPreventUserInteraction:
    (SignoutActionSheetCoordinator*)coordinator {
  _authenticationOperationInProgress = YES;
  [self preventUserInteraction];
}

- (void)signoutActionSheetCoordinatorAllowUserInteraction:
    (SignoutActionSheetCoordinator*)coordinator {
  [self allowUserInteraction];
}

#pragma mark - TableViewLinkHeaderFooterItemDelegate

- (void)view:(TableViewLinkHeaderFooterView*)view didTapLinkURL:(CrURL*)URL {
  OpenNewTabCommand* command =
      [OpenNewTabCommand commandWithURLFromChrome:URL.gurl];
  [_applicationHandler closeSettingsUIAndOpenURL:command];
}

#pragma mark - Internal

- (void)handleAccountErrorUserActionable {
  switch (_accountErrorUserActionableType) {
    case AccountErrorUserActionableType::kEnterPassphrase: {
      [self openPassphraseDialog];
      break;
    }
    case AccountErrorUserActionableType::kReauthForFetchKeys: {
      [self openTrustedVaultReauthForFetchKeys];
      break;
    }
    case AccountErrorUserActionableType::kReauthForDegradedRecoverability: {
      [self openTrustedVaultReauthForDegradedRecoverability];
      break;
    }
    case AccountErrorUserActionableType::kNoAction:
      break;
  }
}

// Opens the trusted vault reauth dialog for fetch keys.
- (void)openTrustedVaultReauthForFetchKeys {
  trusted_vault::SecurityDomainId securityDomainID =
      trusted_vault::SecurityDomainId::kChromeSync;
  syncer::TrustedVaultUserActionTriggerForUMA trigger =
      syncer::TrustedVaultUserActionTriggerForUMA::kSettings;
  signin_metrics::AccessPoint accessPoint =
      signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS;
  [_applicationHandler
      showTrustedVaultReauthForFetchKeysFromViewController:self
                                          securityDomainID:securityDomainID
                                                   trigger:trigger
                                               accessPoint:accessPoint];
}

// Opens the trusted vault reauth dialog for degraded recoverability.
- (void)openTrustedVaultReauthForDegradedRecoverability {
  trusted_vault::SecurityDomainId securityDomainID =
      trusted_vault::SecurityDomainId::kChromeSync;
  syncer::TrustedVaultUserActionTriggerForUMA trigger =
      syncer::TrustedVaultUserActionTriggerForUMA::kSettings;
  signin_metrics::AccessPoint accessPoint =
      signin_metrics::AccessPoint::ACCESS_POINT_SETTINGS;
  [_applicationHandler
      showTrustedVaultReauthForDegradedRecoverabilityFromViewController:self
                                                       securityDomainID:
                                                           securityDomainID
                                                                trigger:trigger
                                                            accessPoint:
                                                                accessPoint];
}

// Opens the passphrase dialog.
- (void)openPassphraseDialog {
  UIViewController<SettingsRootViewControlling>* controllerToPush =
      [[SyncEncryptionPassphraseTableViewController alloc]
          initWithBrowser:_browser];

  // Verify that the accounts table is displayed from a navigation controller.
  DCHECK(self.navigationController);

  [self configureHandlersForRootViewController:controllerToPush];
  [self.navigationController pushViewController:controllerToPush animated:YES];
}

#pragma mark - Private methods

- (void)dismissRemoveOrMyGoogleChooserAlert {
  [self.removeOrMyGoogleChooserAlertCoordinator stop];
  self.removeOrMyGoogleChooserAlertCoordinator = nil;
}

- (void)dismissRemoveAccountCoordinator {
  [self.removeAccountCoordinator stop];
  self.removeAccountCoordinator = nil;
}

// Called with the account details controller has been dismissed.
- (void)accountDetailsControllerWasDismissed {
  _accountDetailsControllerDismissCallback.Reset();
}

#pragma mark - AccountsConsumer

- (void)reloadAllItems {
  if (!self.tableViewModel) {
    // No need to reload since the model has not been loaded yet.
    return;
  }
  [self reloadData];
}

- (void)popView {
  [self popViewIfSignedOut];
}

// Updates the error section in the table view model to indicate the latest
// account error if the states of the account error and the table view model
// don't match. If `reloadViewIfNeeded` is NO, only the model will be
// updated without reloading the view. Can refresh, add or remove the error
// section when an update is needed.
- (void)updateErrorSectionModelAndReloadViewIfNeeded:(BOOL)reloadViewIfNeeded {
  if ([self.modelIdentityDataSource isAccountSignedInNotSyncing]) {
    // If the account is signed in not syncing, the error handling will be
    // shown previously in account settings page and no need to load it in
    // this view.
    return;
  }
  AccountErrorUIInfo* errorInfo =
      [self.modelIdentityDataSource accountErrorUIInfo];
  BOOL hadErrorSection = [self.tableViewModel
      hasSectionForSectionIdentifier:SectionIdentifierError];
  syncer::SyncService::UserActionableError newErrorType =
      errorInfo ? errorInfo.errorType
                : syncer::SyncService::UserActionableError::kNone;

  if (newErrorType == syncer::SyncService::UserActionableError::kNone &&
      _diplayedAccountErrorType ==
          syncer::SyncService::UserActionableError::kNone) {
    DCHECK(!hadErrorSection);
    // Don't update if there is no error to indicate or to remove.
    return;
  }

  if (reloadViewIfNeeded && newErrorType == _diplayedAccountErrorType) {
    DCHECK(hadErrorSection);
    // Don't update if there is already a model and a view, and the state of
    // the model already matches the error that has to be indicated.
    return;
  }

  _diplayedAccountErrorType = newErrorType;
  _accountErrorUserActionableType = errorInfo.userActionableType;

  if (hadErrorSection) {
    // Remove the section from the model to either clear the error section
    // when there is no error or to update the type of error to indicate.
    NSUInteger index = [self.tableViewModel
        sectionForSectionIdentifier:SectionIdentifierError];
    [self.tableViewModel removeSectionWithIdentifier:SectionIdentifierError];

    if (errorInfo == nil) {
      // Delete the error section in the view when there is an error section
      // while there is no account error to indicate.
      if (reloadViewIfNeeded) {
        [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:index]
                      withRowAnimation:UITableViewRowAnimationAutomatic];
      }
      return;
    }
  }

  // Update the error section in the model to indicate the latest account
  // error.
  NSInteger sectionIndex =
      [self.tableViewModel
          sectionForSectionIdentifier:SectionIdentifierAccounts] +
      1;
  [self.tableViewModel insertSectionWithIdentifier:SectionIdentifierError
                                           atIndex:sectionIndex];
  [self.tableViewModel addItem:[self accountErrorMessageItemWithMessageID:
                                         errorInfo.messageID]
       toSectionWithIdentifier:SectionIdentifierError];
  [self.tableViewModel addItem:[self accountErrorButtonItemWithLabelID:
                                         errorInfo.buttonLabelID]
       toSectionWithIdentifier:SectionIdentifierError];

  if (reloadViewIfNeeded) {
    if (hadErrorSection) {
      // Only refresh the section if there was already an error section, where
      // there was a change in the type of error to indicate (excluding
      // kNone).
      [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                    withRowAnimation:UITableViewRowAnimationAutomatic];
    } else {
      [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
                    withRowAnimation:UITableViewRowAnimationAutomatic];
    }
  }
}

- (void)updateIdentityViewItem:(IdentityViewItem*)identityViewItem {
  TableViewAccountItem* item =
      base::apple::ObjCCastStrict<TableViewAccountItem>(
          [_identityMap objectForKey:identityViewItem.gaiaID]);
  if (!item) {
    return;
  }
  [self updateAccountItem:item withIdentityViewItem:identityViewItem];
  NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item];
  [self.tableView reloadRowsAtIndexPaths:@[ indexPath ]
                        withRowAnimation:UITableViewRowAnimationAutomatic];
}

@end