[go: nahoru, domu]

blob: ff4354ed3bfc634c2415a0af2a18d92eb055bcc5 [file] [log] [blame]
// 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/accounts_table_view_controller.h"
#import "base/apple/foundation_util.h"
#import "base/feature_list.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/signin/public/identity_manager/objc/identity_manager_observer_bridge.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync/base/features.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/service/sync_service_utils.h"
#import "ios/chrome/browser/net/crurl.h"
#import "ios/chrome/browser/settings/sync/utils/account_error_ui_info.h"
#import "ios/chrome/browser/settings/sync/utils/identity_error_util.h"
#import "ios/chrome/browser/settings/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/browser_state/chrome_browser_state.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/browsing_data_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/public/features/features.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/authentication_service.h"
#import "ios/chrome/browser/signin/authentication_service_factory.h"
#import "ios/chrome/browser/signin/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/chrome_account_manager_service_observer_bridge.h"
#import "ios/chrome/browser/signin/identity_manager_factory.h"
#import "ios/chrome/browser/signin/system_identity.h"
#import "ios/chrome/browser/signin/system_identity_manager.h"
#import "ios/chrome/browser/sync/sync_observer_bridge.h"
#import "ios/chrome/browser/sync/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_coordinator.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/google_services/accounts_table_view_controller_constants.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_chromium_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "net/base/mac/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, SectionIdentifier) {
SectionIdentifierAccounts = kSectionIdentifierEnumZero,
SectionIdentifierError,
SectionIdentifierSignOut,
};
typedef NS_ENUM(NSInteger, ItemType) {
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 AccountsTableViewController () <
ChromeAccountManagerServiceObserver,
IdentityManagerObserverBridgeDelegate,
SignoutActionSheetCoordinatorDelegate,
SyncObserverModelBridge> {
Browser* _browser;
BOOL _closeSettingsOnAddAccount;
std::unique_ptr<ChromeAccountManagerServiceObserverBridge>
_accountManagerServiceObserver;
std::unique_ptr<signin::IdentityManagerObserverBridge>
_identityManagerObserver;
// 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;
std::unique_ptr<SyncObserverBridge> _syncObserver;
// 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;
}
// 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;
// AccountManager Service used to retrive identities.
@property(nonatomic, assign) ChromeAccountManagerService* accountManagerService;
@end
@implementation AccountsTableViewController {
// Callback to dismiss MyGoogle (Account Detail).
DismissViewCallback _dismissAccountDetailsViewController;
}
- (instancetype)initWithBrowser:(Browser*)browser
closeSettingsOnAddAccount:(BOOL)closeSettingsOnAddAccount {
DCHECK(browser);
DCHECK(!browser->GetBrowserState()->IsOffTheRecord());
self = [super initWithStyle:ChromeTableViewStyle()];
if (self) {
_browser = browser;
_closeSettingsOnAddAccount = closeSettingsOnAddAccount;
_accountManagerService =
ChromeAccountManagerServiceFactory::GetForBrowserState(
_browser->GetBrowserState());
_identityManagerObserver =
std::make_unique<signin::IdentityManagerObserverBridge>(
IdentityManagerFactory::GetForBrowserState(
_browser->GetBrowserState()),
self);
_accountManagerServiceObserver =
std::make_unique<ChromeAccountManagerServiceObserverBridge>(
self, _accountManagerService);
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(_browser->GetBrowserState());
DCHECK(syncService);
_syncObserver = std::make_unique<SyncObserverBridge>(self, syncService);
_diplayedAccountErrorType = syncer::SyncService::UserActionableError::kNone;
_accountErrorUserActionableType = AccountErrorUserActionableType::kNoAction;
}
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.accessibilityIdentifier = kSettingsAccountsTableViewId;
[self loadModel];
}
#pragma mark - SettingsControllerProtocol
- (void)reportDismissalUserAction {
base::RecordAction(base::UserMetricsAction("MobileAccountsSettingsClose"));
}
- (void)reportBackUserAction {
base::RecordAction(base::UserMetricsAction("MobileAccountsSettingsBack"));
}
- (void)settingsWillBeDismissed {
[self dismissRemoveOrMyGoogleChooserAlert];
[self.signoutCoordinator stop];
self.signoutCoordinator = nil;
[self dismissRemoveAccountCoordinator];
_identityManagerObserver.reset();
_accountManagerServiceObserver.reset();
_syncObserver.reset();
_browser = nullptr;
_accountManagerService = 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);
NSString* title = nil;
if (authenticatedIdentity) {
title = authenticatedIdentity.userFullName;
if (!title) {
title = authenticatedIdentity.userEmail;
}
}
if ([self isAccountSignedInNotSyncing] ||
base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos)) {
title = l10n_util::GetNSString(
IDS_IOS_GOOGLE_ACCOUNTS_MANAGEMENT_FROM_ACCOUNT_SETTINGS_TITLE);
}
self.title = 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];
signin::IdentityManager* identityManager =
IdentityManagerFactory::GetForBrowserState(_browser->GetBrowserState());
NSString* authenticatedEmail = authenticatedIdentity.userEmail;
for (const auto& account : identityManager->GetAccountsWithRefreshTokens()) {
id<SystemIdentity> identity =
self.accountManagerService->GetIdentityWithGaiaID(account.gaia);
if (!identity) {
// 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/1081274): This re-ordering will be redundant once we
// apply ordering changes to the account reconciler.
TableViewItem* item = [self accountItem:identity];
if ([identity.userEmail isEqualToString:authenticatedEmail]) {
[model insertItem:item
inSectionWithIdentifier:SectionIdentifierAccounts
atIndex:0];
} else {
[model addItem:item toSectionWithIdentifier:SectionIdentifierAccounts];
}
[mutableIdentityMap setObject:item forKey:identity.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/1462552): 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*)accountItem:(id<SystemIdentity>)identity {
TableViewAccountItem* item =
[[TableViewAccountItem alloc] initWithType:ItemTypeAccount];
[self updateAccountItem:item withIdentity:identity];
return item;
}
- (void)updateAccountItem:(TableViewAccountItem*)item
withIdentity:(id<SystemIdentity>)identity {
item.image = self.accountManagerService->GetIdentityAvatarWithIdentity(
identity, IdentityAvatarSize::TableViewIcon);
item.text = identity.userEmail;
item.identity = identity;
item.accessibilityIdentifier = identity.userEmail;
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 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;
}
// 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 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;
}
syncer::SyncService* syncService =
SyncServiceFactory::GetForBrowserState(_browser->GetBrowserState());
DCHECK(syncService);
AccountErrorUIInfo* errorInfo = GetAccountErrorUIInfo(syncService);
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];
}
}
}
#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];
ItemType itemType = static_cast<ItemType>(
[self.tableViewModel itemTypeForIndexPath:indexPath]);
switch (itemType) {
case ItemTypeAccount: {
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: {
[self showAddAccount];
break;
}
case ItemTypeSignOut: {
if ([self isAccountSignedInNotSyncing]) {
[self signOut];
break;
}
UIView* itemView =
[[tableView cellForRowAtIndexPath:indexPath] contentView];
[self showSignOutWithItemView:itemView];
break;
}
case ItemTypeAccountErrorButton: {
[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();
break;
}
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
#pragma mark - SyncObserverModelBridge
- (void)onSyncStateChanged {
[self updateErrorSectionModelAndReloadViewIfNeeded:YES];
}
#pragma mark - IdentityManagerObserverBridgeDelegate
- (void)onEndBatchOfRefreshTokenStateChanges {
DCHECK(_browser) << "-onEndBatchOfRefreshTokenStateChanges called after "
"-settingsWillBeDismissed";
[self reloadData];
// Only attempt to pop the top-most view controller once the account list
// has been dismissed.
[self popViewIfSignedOut];
}
#pragma mark - Authentication operations
- (void)showAddAccount {
DCHECK(!self.removeOrMyGoogleChooserAlertCoordinator);
_authenticationOperationInProgress = YES;
// TODO(crbug.com/1338990): 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];
}];
[self.applicationCommandsHandler showSignin:command baseViewController:self];
}
- (void)handleDidAddAccount:(BOOL)success {
// TODO(crbug.com/1338990): Remove the following line when todo bug will be
// fixed.
[self allowUserInteraction];
[self handleAuthenticationOperationDidFinish];
if (success && _closeSettingsOnAddAccount) {
[self.applicationCommandsHandler closeSettingsUI];
}
}
- (void)showAccountDetails:(id<SystemIdentity>)identity
itemView:(UIView*)itemView {
DCHECK(!self.removeOrMyGoogleChooserAlertCoordinator);
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:^{
[weakSelf handleManageGoogleAccountWithIdentity:identity];
[weakSelf dismissRemoveOrMyGoogleChooserAlert];
}
style:UIAlertActionStyleDefault];
[self.removeOrMyGoogleChooserAlertCoordinator
addItemWithTitle:l10n_util::GetNSString(
IDS_IOS_REMOVE_GOOGLE_ACCOUNT_TITLE)
action:^{
[weakSelf handleRemoveSecondaryAccountWithIdentity:identity];
[weakSelf dismissRemoveOrMyGoogleChooserAlert];
}
style:UIAlertActionStyleDestructive];
[self.removeOrMyGoogleChooserAlertCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
action:^() {
[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;
_dismissAccountDetailsViewController =
GetApplicationContext()
->GetSystemIdentityManager()
->PresentAccountDetailsController(identity, self,
/*animated=*/YES);
}
// Handles the secondary account remove action from
// `self.removeOrMyGoogleChooserAlertCoordinator`. Action sheet created in
// `showAccountDetails:itemView:`
- (void)handleRemoveSecondaryAccountWithIdentity:(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:^{
weakSelf.removeAccountCoordinator = nil;
[weakSelf dismissRemoveAccountCoordinator];
}
style:UIAlertActionStyleCancel];
[self.removeAccountCoordinator
addItemWithTitle:l10n_util::GetNSString(IDS_IOS_REMOVE_ACCOUNT_LABEL)
action:^{
[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 AccountsTableViewController* 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 || self.signoutDismissalByParentCoordinator) {
return;
}
_isBeingDismissed = YES;
__weak __typeof(self) weakSelf = self;
void (^popAccountsTableViewController)() = ^() {
[base::apple::ObjCCastStrict<SettingsNavigationController>(
weakSelf.navigationController)
popViewControllerOrCloseSettingsAnimated:YES];
};
if (!_dismissAccountDetailsViewController.is_null()) {
DCHECK(self.presentedViewController);
DCHECK(!self.removeOrMyGoogleChooserAlertCoordinator);
DCHECK(!self.removeAccountCoordinator);
DCHECK(!self.signoutCoordinator);
// TODO(crbug.com/1221066): 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(_dismissAccountDetailsViewController).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 - ChromeAccountManagerServiceObserver
- (void)identityUpdated:(id<SystemIdentity>)identity {
TableViewAccountItem* item =
base::apple::ObjCCastStrict<TableViewAccountItem>(
[_identityMap objectForKey:identity.gaiaID]);
if (!item) {
return;
}
[self updateAccountItem:item withIdentity:identity];
NSIndexPath* indexPath = [self.tableViewModel indexPathForItem:item];
[self.tableView reloadRowsAtIndexPaths:@[ indexPath ]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
#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];
[self.applicationCommandsHandler 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 {
syncer::TrustedVaultUserActionTriggerForUMA trigger =
syncer::TrustedVaultUserActionTriggerForUMA::kSettings;
[self.applicationCommandsHandler
showTrustedVaultReauthForFetchKeysFromViewController:self
trigger:trigger];
}
// Opens the trusted vault reauth dialog for degraded recoverability.
- (void)openTrustedVaultReauthForDegradedRecoverability {
syncer::TrustedVaultUserActionTriggerForUMA trigger =
syncer::TrustedVaultUserActionTriggerForUMA::kSettings;
[self.applicationCommandsHandler
showTrustedVaultReauthForDegradedRecoverabilityFromViewController:self
trigger:
trigger];
}
// 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);
// TODO(crbug.com/1045047): Use HandlerForProtocol after commands protocol
// clean up.
controllerToPush.dispatcher = static_cast<
id<ApplicationCommands, BrowserCommands, BrowsingDataCommands>>(
_browser->GetCommandDispatcher());
[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;
}
// Returns YES if the account is signed in not syncing, NO otherwise.
- (BOOL)isAccountSignedInNotSyncing {
// TODO(crbug.com/1462552): Simplify once kSync becomes unreachable or is
// deleted from the codebase. See ConsentLevel::kSync documentation for
// details.
return base::FeatureList::IsEnabled(
syncer::kReplaceSyncPromosWithSignInPromos) &&
!SyncServiceFactory::GetForBrowserState(_browser->GetBrowserState())
->HasSyncConsent();
}
// Signs out without showing action sheet.
// Used when the user is signed in not syncing.
- (void)signOut {
if (![self authService]->HasPrimaryIdentity(signin::ConsentLevel::kSignin)) {
// This could happen if the account somehow got removed after the UI was
// created.
return;
}
CHECK([self isAccountSignedInNotSyncing]);
if (_authenticationOperationInProgress) {
return;
}
_authenticationOperationInProgress = YES;
[self preventUserInteraction];
signin_metrics::RecordSignoutUserAction(/*force_clear_data=*/false);
__weak AccountsTableViewController* weakSelf = self;
ProceduralBlock signOutCompletion = ^() {
__strong AccountsTableViewController* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
[strongSelf allowUserInteraction];
[strongSelf handleAuthenticationOperationDidFinish];
};
[self authService]->SignOut(
signin_metrics::ProfileSignout::kUserClickedSignoutSettings,
/*force_clear_browsing_data=*/NO, signOutCompletion);
}
@end