chromium/ios/chrome/browser/ui/authentication/account_menu/account_menu_view_controller.mm

// 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/authentication/account_menu/account_menu_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/check.h"
#import "base/check_op.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/policy/model/management_state.h"
#import "ios/chrome/browser/settings/model/sync/utils/account_error_ui_info.h"
#import "ios/chrome/browser/shared/ui/list_model/list_model.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/chrome_table_view_controller.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_styler.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/signin/model/constants.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_constants.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_data_source.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_mutator.h"
#import "ios/chrome/browser/ui/authentication/account_menu/account_menu_view_controller_presentation_delegate.h"
#import "ios/chrome/browser/ui/authentication/cells/central_account_view.h"
#import "ios/chrome/browser/ui/authentication/cells/table_view_account_item.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_cell.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

const char kEditAccountListIdentifier[] = "kEditAccountListIdentifier";
const char kManageYourGoogleAccountIdentifier[] =
    "kManageYourGoogleAccountIdentifier";

namespace {

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

// Height and width of the buttons.
constexpr CGFloat kButtonSize = 22;

constexpr CGFloat kHalfSheetCornerRadius = 20.0;

// Sections used in the account menu.
typedef NS_ENUM(NSUInteger, SectionIdentifier) {
  // Sync errors.
  SyncErrorsSectionIdentifier = kSectionIdentifierEnumZero,
  // List of accounts
  AccountsSectionIdentifier,
  // manage accounts, sign-out
  SignOutSectionIdentifier,
};

typedef NS_ENUM(NSUInteger, RowIdentifier) {
  // Error section
  RowIdentifierErrorExplanation = kItemTypeEnumZero,
  RowIdentifierErrorButton,
  // Signout section
  RowIdentifierSignOut,
  // Accounts section.
  RowIdentifierAddAccount,
  // The secondary account entries use the gaia ID as item identifier.
};
// Custom detent identifier for when the bottom sheet is minimized.
NSString* const kCustomMinimizedDetentIdentifier = @"customMinimizedDetent";

// Custom detent identifier for when the bottom sheet is expanded.
NSString* const kCustomExpandedDetentIdentifier = @"customExpandedDetent";

}  // namespace

@implementation AccountMenuViewController {
  UITableViewDiffableDataSource* _accountMenuDataSource;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.tableView.accessibilityIdentifier = kAccountMenuTableViewId;
  self.tableView.backgroundColor =
      [UIColor colorNamed:kGroupedPrimaryBackgroundColor];
  RegisterTableViewCell<TableViewAccountCell>(self.tableView);
  RegisterTableViewCell<SettingsImageDetailTextCell>(self.tableView);
  RegisterTableViewCell<TableViewTextCell>(self.tableView);
  [self setUpNavigationController];
  [self setUpTableContent];
  [self updatePrimaryAccount];
  [self resize];
}

- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];
  // Update the bottom sheet height.
  [self resize];
}

#pragma mark - Private

// Resizes the view for current content.
- (void)resize {
  // Update the bottom sheet height.
  [self.sheetPresentationController invalidateDetents];
  // Update the popover height.
  CGFloat height =
      [self.tableView
          systemLayoutSizeFittingSize:self.popoverPresentationController
                                          .containerView.bounds.size]
          .height;
  self.preferredContentSize = CGSize(self.preferredContentSize.width, height);
}

// Sets up the navigation controller’s buttons.
- (void)setUpNavigationController {
  // Stop button
  UIUserInterfaceIdiom idiom = [[UIDevice currentDevice] userInterfaceIdiom];
  if (idiom != UIUserInterfaceIdiomPad) {
    UIBarButtonItem* closeButton = [[UIBarButtonItem alloc]
        initWithBarButtonSystemItem:UIBarButtonSystemItemClose
                             target:self
                             action:@selector(userTappedOnClose)];
    closeButton.accessibilityIdentifier = kAccountMenuCloseButtonId;
    self.navigationItem.rightBarButtonItem = closeButton;
  }

  // Ellipsis button
  UIImageSymbolConfiguration* symbolConfiguration = [UIImageSymbolConfiguration
      configurationWithPointSize:kButtonSize
                          weight:UIImageSymbolWeightSemibold
                           scale:UIImageSymbolScaleMedium];
  UIAction* manageYourAccountAction = [UIAction
      actionWithTitle:
          l10n_util::GetNSString(
              IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_MANAGE_GOOGLE_ACCOUNT_ITEM)
                image:DefaultSymbolWithConfiguration(@"arrow.up.right.square",
                                                     symbolConfiguration)
           identifier:base::SysUTF8ToNSString(
                          kManageYourGoogleAccountIdentifier)
              handler:^(UIAction* action) {
                base::RecordAction(base::UserMetricsAction(
                    "Signin_AccountMenu_ManageAccount"));
                [self.delegate didTapManageYourGoogleAccount];
              }];
  // TODO(crbug.com/336719423): Add the primary account email as subtitle.

  UIAction* editAccountListAction = [UIAction
      actionWithTitle:l10n_util::GetNSString(
                          IDS_IOS_ACCOUNT_MENU_EDIT_ACCOUNT_LIST)
                image:DefaultSymbolWithConfiguration(@"pencil",
                                                     symbolConfiguration)
           identifier:base::SysUTF8ToNSString(kEditAccountListIdentifier)
              handler:^(UIAction* action) {
                base::RecordAction(base::UserMetricsAction(
                    "Signin_AccountMenu_EditAccountList"));
                [self.delegate didTapEditAccountList];
              }];

  UIMenu* ellipsisMenu = [UIMenu
      menuWithChildren:@[ manageYourAccountAction, editAccountListAction ]];
  UIImage* ellipsisImage = SymbolWithPalette(
      DefaultSymbolWithConfiguration(@"ellipsis.circle.fill",
                                     symbolConfiguration),
      @[
        [UIColor colorNamed:kGrey500Color], [UIColor colorNamed:kGrey300Color]
      ]);
  UIBarButtonItem* ellipsisButton =
      [[UIBarButtonItem alloc] initWithImage:ellipsisImage menu:ellipsisMenu];
  ellipsisButton.accessibilityIdentifier =
      kAccountMenuSecondaryActionMenuButtonId;
  self.navigationItem.leftBarButtonItem = ellipsisButton;
}

- (UITableViewCell*)cellForTableView:(UITableView*)tableView
                           indexPath:(NSIndexPath*)indexPath
                      itemIdentifier:(id)itemIdentifier {
  NSString* gaiaID = base::apple::ObjCCast<NSString>(itemIdentifier);
  if (gaiaID) {
    // `itemIdentifier` is a gaia id.
    TableViewAccountItem* item = [self.dataSource identityItemForGaiaID:gaiaID];
    TableViewAccountCell* cell =
        DequeueTableViewCell<TableViewAccountCell>(tableView);
    [item configureCell:cell withStyler:[[ChromeTableViewStyler alloc] init]];
    cell.accessibilityIdentifier = kAccountMenuSecondaryAccountButtonId;
    return cell;
  }

  // Otherwise `itemIdentifier` is a `RowIdentifier`.
  RowIdentifier rowIdentifier = static_cast<RowIdentifier>(
      base::apple::ObjCCastStrict<NSNumber>(itemIdentifier).integerValue);
  NSString* label = nil;
  NSString* accessibilityIdentifier = nil;
  switch (rowIdentifier) {
    case RowIdentifierErrorExplanation: {
      SettingsImageDetailTextCell* cell =
          DequeueTableViewCell<SettingsImageDetailTextCell>(tableView);
      SettingsImageDetailTextItem* item =
          [[SettingsImageDetailTextItem alloc] initWithType:0];
      item.detailText =
          l10n_util::GetNSString(self.dataSource.accountErrorUIInfo.messageID);
      item.image =
          DefaultSymbolWithPointSize(kErrorCircleFillSymbol, kErrorSymbolSize);
      item.imageViewTintColor = [UIColor colorNamed:kRed500Color];
      [item configureCell:cell withStyler:[[ChromeTableViewStyler alloc] init]];
      cell.selectionStyle = UITableViewCellSelectionStyleNone;
      cell.accessibilityIdentifier = kAccountMenuErrorMessageId;
      return cell;
    }
    case RowIdentifierErrorButton:
      label = l10n_util::GetNSString(
          self.dataSource.accountErrorUIInfo.buttonLabelID);
      accessibilityIdentifier = kAccountMenuErrorActionButtonId;
      break;
    case RowIdentifierAddAccount:
      label =
          l10n_util::GetNSString(IDS_IOS_OPTIONS_ACCOUNTS_ADD_ACCOUNT_BUTTON);
      accessibilityIdentifier = kAccountMenuAddAccountButtonId;
      break;
    case RowIdentifierSignOut:
      label =
          l10n_util::GetNSString(IDS_IOS_GOOGLE_ACCOUNT_SETTINGS_SIGN_OUT_ITEM);
      accessibilityIdentifier = kAccountMenuSignoutButtonId;
      break;
    default:
      NOTREACHED();
  }
  // If the function has not returned yet. This cell contains only text.

  TableViewTextItem* item = [[TableViewTextItem alloc] init];
  item.textColor = [UIColor colorNamed:kBlueColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  item.text = label;
  TableViewTextCell* cell = DequeueTableViewCell<TableViewTextCell>(tableView);
  [item configureCell:cell withStyler:[[ChromeTableViewStyler alloc] init]];
  cell.accessibilityIdentifier = accessibilityIdentifier;
  return cell;
}

- (void)setUpBottomSheetPresentationController {
  UISheetPresentationController* presentationController =
      self.sheetPresentationController;
  presentationController.prefersEdgeAttachedInCompactHeight = YES;
  presentationController.widthFollowsPreferredContentSizeWhenEdgeAttached = YES;
  presentationController.preferredCornerRadius = kHalfSheetCornerRadius;
  __weak __typeof(self) weakSelf = self;
  auto preferredHeightForContent = ^CGFloat(
      id<UISheetPresentationControllerDetentResolutionContext> context) {
    return [weakSelf preferredHeightForContent];
  };
  UISheetPresentationControllerDetent* customDetent =
      [UISheetPresentationControllerDetent
          customDetentWithIdentifier:kCustomMinimizedDetentIdentifier
                            resolver:preferredHeightForContent];
  presentationController.detents = @[ customDetent ];
  presentationController.selectedDetentIdentifier =
      kCustomMinimizedDetentIdentifier;
}

- (void)userTappedOnClose {
  base::RecordAction(base::UserMetricsAction("Signin_AccountMenu_Close"));
  [self.delegate viewControllerWantsToBeClosed:self];
}

- (void)setUpTableContent {
  // Configure the table items.
  __weak __typeof(self) weakSelf = self;
  _accountMenuDataSource = [[UITableViewDiffableDataSource alloc]
      initWithTableView:self.tableView
           cellProvider:^UITableViewCell*(UITableView* tableView,
                                          NSIndexPath* indexPath, id itemId) {
             return [weakSelf cellForTableView:tableView
                                     indexPath:indexPath
                                itemIdentifier:itemId];
           }];
  self.tableView.dataSource = _accountMenuDataSource;

  NSDiffableDataSourceSnapshot* snapshot =
      [[NSDiffableDataSourceSnapshot alloc] init];

  AccountErrorUIInfo* error = self.dataSource.accountErrorUIInfo;
  if (error) {
    [snapshot appendSectionsWithIdentifiers:@[
      @(SyncErrorsSectionIdentifier),
    ]];
    [snapshot appendItemsWithIdentifiers:@[
      @(RowIdentifierErrorExplanation), @(RowIdentifierErrorButton)
    ]
               intoSectionWithIdentifier:@(SyncErrorsSectionIdentifier)];
  }

  [snapshot appendSectionsWithIdentifiers:@[ @(AccountsSectionIdentifier) ]];
  NSMutableArray* accountsIdentifiers = [[NSMutableArray alloc] init];
  NSArray<NSString*>* gaiaIDs = self.dataSource.secondaryAccountsGaiaIDs;
  for (NSString* gaiaID in gaiaIDs) {
    [accountsIdentifiers addObject:gaiaID];
  }
  [accountsIdentifiers addObject:@(RowIdentifierAddAccount)];
  [snapshot appendItemsWithIdentifiers:accountsIdentifiers
             intoSectionWithIdentifier:@(AccountsSectionIdentifier)];

  [snapshot appendSectionsWithIdentifiers:@[ @(SignOutSectionIdentifier) ]];
  [snapshot appendItemsWithIdentifiers:@[ @(RowIdentifierSignOut) ]
             intoSectionWithIdentifier:@(SignOutSectionIdentifier)];

  [_accountMenuDataSource applySnapshot:snapshot animatingDifferences:YES];
}

// Returns the sheet presentation controller if it exists.
- (UISheetPresentationController*)sheetPresentationController {
  return self.navigationController.popoverPresentationController
      .adaptiveSheetPresentationController;
}

// Returns preferred height according to the container view width.
- (CGFloat)preferredHeightForContent {
  // Get the size of the container view which is the maximum available size.
  UIView* containerView = self.sheetPresentationController.containerView;
  CGSize fittingSize = containerView.bounds.size;
  CGFloat height =
      [self.tableView systemLayoutSizeFittingSize:fittingSize].height;
  // Add the navigation bar.
  height += self.navigationController.navigationBar.frame.size.height;
  return height;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  id itemIdentifier =
      [_accountMenuDataSource itemIdentifierForIndexPath:indexPath];
  NSString* gaiaID = base::apple::ObjCCast<NSString>(itemIdentifier);
  if (gaiaID) {
    // `itemIdentifier` is a gaiaID.
    base::RecordAction(
        base::UserMetricsAction("Signin_AccountMenu_SelectAccount"));
    CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath];
    [self.mutator accountTappedWithGaiaID:gaiaID targetRect:cellRect];
  } else {
    // Otherwise `itemIdentifier` is a `RowIdentifier`.
    RowIdentifier rowIdentifier = static_cast<RowIdentifier>(
        base::apple::ObjCCastStrict<NSNumber>(itemIdentifier).integerValue);
    switch (rowIdentifier) {
      case RowIdentifierAddAccount:
        base::RecordAction(
            base::UserMetricsAction("Signin_AccountMenu_AddAccount"));
        [self.delegate didTapAddAccount];
        break;
      case RowIdentifierErrorExplanation:
        break;
      case RowIdentifierErrorButton:
        base::RecordAction(
            base::UserMetricsAction("Signin_AccountMenu_ErrorButton"));
        [self.mutator didTapErrorButton];
        break;
      case RowIdentifierSignOut:
        base::RecordAction(
            base::UserMetricsAction("Signin_AccountMenu_Signout"));
        CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath];
        [self.delegate signOutFromTargetRect:cellRect callback:nil];
        break;
    }
  }
  [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark - AccountMenuConsumer

- (void)updatePrimaryAccount {
  CentralAccountView* identityAccountItem = [[CentralAccountView alloc]
        initWithFrame:CGRectMake(0, 0, self.tableView.frame.size.width, 0)
          avatarImage:self.dataSource.primaryAccountAvatar
                 name:self.dataSource.primaryAccountUserFullName
                email:self.dataSource.primaryAccountEmail
      managementState:self.dataSource.managementState];
  self.tableView.tableHeaderView = identityAccountItem;
  [self.tableView reloadData];
}

- (void)updateErrorSection:(AccountErrorUIInfo*)error {
  NSDiffableDataSourceSnapshot* snapshot = _accountMenuDataSource.snapshot;
  if (error == nil) {
    // The error disappeared.
    CHECK_EQ([snapshot indexOfSectionIdentifier:@(SyncErrorsSectionIdentifier)],
             0);
    [snapshot
        deleteSectionsWithIdentifiers:@[ @(SyncErrorsSectionIdentifier) ]];
  } else if ([snapshot
                 indexOfSectionIdentifier:@(SyncErrorsSectionIdentifier)] ==
             NSNotFound) {
    // The error appeared.
    [snapshot insertSectionsWithIdentifiers:@[ @(SyncErrorsSectionIdentifier) ]
                beforeSectionWithIdentifier:@(AccountsSectionIdentifier)];
    [snapshot appendItemsWithIdentifiers:@[
      @(RowIdentifierErrorExplanation), @(RowIdentifierErrorButton)
    ]
               intoSectionWithIdentifier:@(SyncErrorsSectionIdentifier)];
  } else {
    // The error changed. No need to change the sections, only their content.
  }
  [_accountMenuDataSource applySnapshot:snapshot animatingDifferences:YES];
}

- (void)updateAccountListWithGaiaIDsToAdd:(NSArray<NSString*>*)indicesToAdd
                          gaiaIDsToRemove:(NSArray<NSString*>*)gaiaIDsToRemove {
  NSDiffableDataSourceSnapshot* snapshot = _accountMenuDataSource.snapshot;

  NSMutableArray* accountsIdentifiersToAdd = [[NSMutableArray alloc] init];
  for (NSString* gaiaID in indicesToAdd) {
    [accountsIdentifiersToAdd addObject:gaiaID];
  }
  [snapshot insertItemsWithIdentifiers:accountsIdentifiersToAdd
              beforeItemWithIdentifier:@(RowIdentifierAddAccount)];

  NSMutableArray* accountsIdentifiersToRemove = [[NSMutableArray alloc] init];
  for (NSString* gaiaID in gaiaIDsToRemove) {
    [accountsIdentifiersToRemove addObject:gaiaID];
  }
  [snapshot deleteItemsWithIdentifiers:accountsIdentifiersToRemove];
  [_accountMenuDataSource applySnapshot:snapshot animatingDifferences:YES];
}

#pragma mark - UIResponder

// To always be able to register key commands via -keyCommands, the VC must be
// able to become first responder.
- (BOOL)canBecomeFirstResponder {
  return YES;
}

- (NSArray<UIKeyCommand*>*)keyCommands {
  return @[ UIKeyCommand.cr_close ];
}

- (void)keyCommand_close {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
  [self.delegate viewControllerWantsToBeClosed:self];
}

@end