chromium/ios/chrome/browser/ui/settings/password/password_manager_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/password/password_manager_view_controller.h"

#import <UIKit/UIKit.h>

#import <optional>
#import <utility>
#import <vector>

#import "base/apple/foundation_util.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/ranges/algorithm.h"
#import "base/strings/sys_string_conversions.h"
#import "components/google/core/common/google_util.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/password_manager_constants.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/browser/password_ui_utils.h"
#import "components/password_manager/core/browser/ui/affiliated_group.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/browser/ui/password_check_referrer.h"
#import "components/password_manager/core/common/password_manager_pref_names.h"
#import "components/prefs/pref_service.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/sync/service/sync_user_settings.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/passwords/model/password_checkup_metrics.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/ui/elements/branded_navigation_item_title_view.h"
#import "ios/chrome/browser/shared/ui/elements/home_waiting_view.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_detail_icon_item.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_image_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_info_button_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_info_button_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_switch_cell.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_switch_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/cells/table_view_url_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_favicon_data_source.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_illustrated_empty_view.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_navigation_controller_constants.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_observer_bridge.h"
#import "ios/chrome/browser/ui/settings/cells/inline_promo_cell.h"
#import "ios/chrome/browser/ui/settings/cells/inline_promo_item.h"
#import "ios/chrome/browser/ui/settings/cells/settings_check_cell.h"
#import "ios/chrome/browser/ui/settings/cells/settings_check_item.h"
#import "ios/chrome/browser/ui/settings/elements/enterprise_info_popover_view_controller.h"
#import "ios/chrome/browser/ui/settings/password/create_password_manager_title_view.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller+Testing.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_items.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_presentation_delegate.h"
#import "ios/chrome/browser/ui/settings/password/passwords_consumer.h"
#import "ios/chrome/browser/ui/settings/password/passwords_settings_commands.h"
#import "ios/chrome/browser/ui/settings/password/passwords_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_view_controller+toolbar_add.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_view_controller+toolbar_settings.h"
#import "ios/chrome/browser/ui/settings/settings_root_table_view_controller.h"
#import "ios/chrome/browser/ui/settings/utils/password_utils.h"
#import "ios/chrome/common/string_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/popover_label_view_controller.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.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/device_form_factor.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

using base::UmaHistogramEnumeration;
using password_manager::metrics_util::PasswordCheckInteraction;

namespace {

// Height of empty footer below the manage account header.
// This ammount added to the internal padding of the manage account header (8pt)
// and the height of the empty header of the next section (10pt) achieves the
// desired vertical spacing (20pt) between the manager account header's text and
// the first item of the next section.
constexpr CGFloat kManageAccountHeaderSectionFooterHeight = 2;

// The maximum width the view can have for the widget promo cell to be
// configured with its narrow layout. When the view's width is above that, the
// cell's layout should be switched to the wide one.
constexpr CGFloat kWidgetPromoLayoutThreshold = 500;

typedef NS_ENUM(NSInteger, ItemType) {
  // Section: SectionIdentifierManageAccountHeader
  ItemTypeLinkHeader = kItemTypeEnumZero,
  // Section: SectionIdentifierWidgetPromo
  ItemTypeWidgetPromo,
  // Section: SectionIdentifierPasswordCheck
  ItemTypePasswordCheckStatus,
  ItemTypeCheckForProblemsButton,
  ItemTypeLastCheckTimestampFooter,
  // Section: SectionIdentifierSavedPasswords
  ItemTypeHeader,
  ItemTypeSavedPassword,  // This is a repeated item type.
  // Section: SectionIdentifierBlocked
  ItemTypeBlocked,  // This is a repeated item type.
  // Section: SectionIdentifierAddPasswordButton
  ItemTypeAddPasswordButton,
};

// Helper method to determine whether the Password Check cell is tappable or
// not.
bool IsPasswordCheckTappable(PasswordCheckUIState passwordCheckState) {
  switch (passwordCheckState) {
    case PasswordCheckStateUnmutedCompromisedPasswords:
    case PasswordCheckStateReusedPasswords:
    case PasswordCheckStateWeakPasswords:
    case PasswordCheckStateDismissedWarnings:
    case PasswordCheckStateSafe:
      return true;
    case PasswordCheckStateDefault:
    case PasswordCheckStateRunning:
    case PasswordCheckStateDisabled:
    case PasswordCheckStateError:
    case PasswordCheckStateSignedOut:
      return false;
  }
}

// TODO(crbug.com/40261300): Remove when CredentialUIEntry operator== is fixed.
template <typename T>
bool AreNotesEqual(const T& lhs, const T& rhs) {
  return base::ranges::equal(lhs, rhs, {},
                             &password_manager::CredentialUIEntry::note,
                             &password_manager::CredentialUIEntry::note);
}

bool AreNotesEqual(const std::vector<password_manager::AffiliatedGroup>& lhs,
                   const std::vector<password_manager::AffiliatedGroup>& rhs) {
  return base::ranges::equal(
      lhs, rhs,
      AreNotesEqual<base::span<const password_manager::CredentialUIEntry>>,
      &password_manager::AffiliatedGroup::GetCredentials,
      &password_manager::AffiliatedGroup::GetCredentials);
}

template <typename T>
bool AreStoresEqual(const T& lhs, const T& rhs) {
  return base::ranges::equal(lhs, rhs, {},
                             &password_manager::CredentialUIEntry::stored_in,
                             &password_manager::CredentialUIEntry::stored_in);
}

bool AreStoresEqual(const std::vector<password_manager::AffiliatedGroup>& lhs,
                    const std::vector<password_manager::AffiliatedGroup>& rhs) {
  return base::ranges::equal(
      lhs, rhs,
      AreStoresEqual<base::span<const password_manager::CredentialUIEntry>>,
      &password_manager::AffiliatedGroup::GetCredentials,
      &password_manager::AffiliatedGroup::GetCredentials);
}

template <typename T>
bool AreIssuesEqual(const T& lhs, const T& rhs) {
  return base::ranges::equal(
      lhs, rhs, {}, &password_manager::CredentialUIEntry::password_issues,
      &password_manager::CredentialUIEntry::password_issues);
}

bool AreIssuesEqual(const std::vector<password_manager::AffiliatedGroup>& lhs,
                    const std::vector<password_manager::AffiliatedGroup>& rhs) {
  return base::ranges::equal(
      lhs, rhs,
      AreIssuesEqual<base::span<const password_manager::CredentialUIEntry>>,
      &password_manager::AffiliatedGroup::GetCredentials,
      &password_manager::AffiliatedGroup::GetCredentials);
}

}  // namespace

@interface PasswordManagerViewController () <
    ChromeAccountManagerServiceObserver,
    PopoverLabelViewControllerDelegate,
    TableViewIllustratedEmptyViewDelegate>

// Current passwords search term.
@property(nonatomic, copy) NSString* searchTerm;

// The scrim view that covers the table view when search bar is focused with
// empty search term. Tapping on the scrim view will dismiss the search bar.
@property(nonatomic, strong) UIControl* scrimView;

// The loading spinner background which appears when loading passwords.
@property(nonatomic, strong) HomeWaitingView* spinnerView;

// Current state of the Password Check.
@property(nonatomic, assign) PasswordCheckUIState passwordCheckState;

// Number of insecure passwords.
@property(assign) NSInteger insecurePasswordsCount;

// Stores the item which has form attribute's username and site equivalent to
// that of `mostRecentlyUpdatedCred`.
@property(nonatomic, weak) TableViewItem* mostRecentlyUpdatedItem;

// YES, if the user triggered a password check by tapping on the "Check Now"
// button.
@property(nonatomic, assign) BOOL checkWasTriggeredManually;

// Return YES if the search bar should be enabled.
@property(nonatomic, assign) BOOL shouldEnableSearchBar;

// The search controller used in this view. This may be added/removed from the
// navigation controller, but the instance will persist here.
@property(nonatomic, strong) UISearchController* searchController;

// Settings button for the toolbar.
@property(nonatomic, strong) UIBarButtonItem* settingsButtonInToolbar;

// Add button for the toolbar.
@property(nonatomic, strong) UIBarButtonItem* addButtonInToolbar;

// Indicates whether the check button should be shown or not.
@property(nonatomic, assign) BOOL shouldShowCheckButton;

// The PrefService passed to this instance.
@property(nonatomic, assign) PrefService* prefService;

// The header for save passwords switch section.
@property(nonatomic, readonly)
    TableViewLinkHeaderFooterItem* manageAccountLinkItem;

// The item related to the password check status.
@property(nonatomic, readonly) SettingsCheckItem* passwordProblemsItem;

// The button to start password check.
@property(nonatomic, readonly) TableViewTextItem* checkForProblemsItem;

// The button to add a password.
@property(nonatomic, readonly) TableViewTextItem* addPasswordItem;

// The item used to present the Password Manager widget promo.
@property(nonatomic, readonly) InlinePromoItem* widgetPromoItem;

// Deleting passwords updates the SavedPasswordsPresenter, resulting in an
// observer callback, which handles general data updates with a `reloadData`.
// Visually, it is better to handle user-initiated changes with more specific
// actions such as inserting or removing items/sections, instead of waiting on a
// data reload. This boolean is used to stop the observer callback from acting
// on user-initiated changes.
@property(nonatomic, readwrite, assign) BOOL deletionInProgress;

@end

@implementation PasswordManagerViewController {
  // Boolean indicating that passwords are being saved in an account if YES,
  // and locally if NO.
  BOOL _savingPasswordsToAccount;
  // The list of the user's blocked sites.
  std::vector<password_manager::CredentialUIEntry> _blockedSites;
  // The list of the user's saved grouped passwords.
  std::vector<password_manager::AffiliatedGroup> _affiliatedGroups;
  // AcountManagerService Observer.
  std::unique_ptr<ChromeAccountManagerServiceObserverBridge>
      _accountManagerServiceObserver;
  // Boolean indicating if password forms have been received for the first time.
  // Used to show a loading indicator while waiting for the store response.
  BOOL _didReceivePasswords;
  // Whether -viewDidAppear was called.
  BOOL _hasViewAppeared;
  // Whether the table view is in search mode. That is, it only has the search
  // bar potentially saved passwords and blocked sites.
  BOOL _tableIsInSearchMode;
  // Whether the favicon metric was already logged.
  BOOL _faviconMetricLogged;
  // Whether the search controller should be set as active when the view is
  // presented.
  BOOL _shouldOpenInSearchMode;
  // Whether or not a search user action was recorded for the current search
  // session.
  BOOL _searchPasswordsUserActionWasRecorded;
  // Whether or not the Password Manager widget promo should be shown.
  BOOL _shouldShowPasswordManagerWidgetPromo;
  // Stores the most recently created or updated password form.
  std::optional<password_manager::CredentialUIEntry> _mostRecentlyUpdatedCred;
}

@synthesize manageAccountLinkItem = _manageAccountLinkItem;
@synthesize widgetPromoItem = _widgetPromoItem;
@synthesize passwordProblemsItem = _passwordProblemsItem;
@synthesize checkForProblemsItem = _checkForProblemsItem;
@synthesize addPasswordItem = _addPasswordItem;

#pragma mark - Initialization

- (instancetype)initWithChromeAccountManagerService:
                    (ChromeAccountManagerService*)accountManagerService
                                        prefService:(PrefService*)prefService
                             shouldOpenInSearchMode:
                                 (BOOL)shouldOpenInSearchMode {
  self = [super initWithStyle:ChromeTableViewStyle()];
  if (self) {
    _prefService = prefService;
    _accountManagerServiceObserver =
        std::make_unique<ChromeAccountManagerServiceObserverBridge>(
            self, accountManagerService);

    self.shouldDisableDoneButtonOnEdit = YES;
    self.searchTerm = @"";

    // Default behavior: search bar is enabled.
    self.shouldEnableSearchBar = YES;
    _shouldOpenInSearchMode = shouldOpenInSearchMode;

    [self updateUIForEditState];
  }
  return self;
}

- (void)dealloc {
  // Not an invariant due to possible race conditions. DCHECKing for debugging
  // purposes. See crbug.com/40067451.
  DCHECK(!_accountManagerServiceObserver.get());
}

- (void)setReauthenticationModule:
    (ReauthenticationModule*)reauthenticationModule {
  _reauthenticationModule = reauthenticationModule;
}

- (void)setMostRecentlyUpdatedPasswordDetails:
    (const password_manager::CredentialUIEntry&)credential {
  _mostRecentlyUpdatedCred = credential;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  [self setUpTitle];

  self.tableView.allowsMultipleSelectionDuringEditing = YES;
  self.tableView.accessibilityIdentifier = kPasswordsTableViewID;

  // With no header on first appearance, UITableView adds a 35 points space at
  // the beginning of the table view. This space remains after this table view
  // reloads with headers. Setting a small tableHeaderView avoids this.
  self.tableView.tableHeaderView =
      [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, CGFLOAT_MIN)];

  // SearchController Configuration.
  // Init the searchController with nil so the results are displayed on the same
  // TableView.
  UISearchController* searchController =
      [[UISearchController alloc] initWithSearchResultsController:nil];
  self.searchController = searchController;
  searchController.obscuresBackgroundDuringPresentation = NO;
  searchController.delegate = self;

  UISearchBar* searchBar = searchController.searchBar;
  searchBar.delegate = self;
  searchBar.backgroundColor = UIColor.clearColor;
  searchBar.accessibilityIdentifier = kPasswordsSearchBarID;

  // UIKit needs to know which controller will be presenting the
  // searchController. If we don't add this trying to dismiss while
  // SearchController is active will fail.
  self.definesPresentationContext = YES;

  // Place the search bar in the navigation bar.
  self.navigationItem.searchController = searchController;
  self.navigationItem.hidesSearchBarWhenScrolling = NO;

  self.scrimView = [[UIControl alloc] init];
  self.scrimView.alpha = 0.0f;
  self.scrimView.backgroundColor = [UIColor colorNamed:kScrimBackgroundColor];
  self.scrimView.translatesAutoresizingMaskIntoConstraints = NO;
  self.scrimView.accessibilityIdentifier = kPasswordsScrimViewID;
  [self.scrimView addTarget:self
                     action:@selector(dismissSearchController:)
           forControlEvents:UIControlEventTouchUpInside];

  [self loadModel];

  if (!_didReceivePasswords) {
    [self showLoadingSpinnerBackground];
  }
}

- (void)viewWillAppear:(BOOL)animated {
  [super viewWillAppear:animated];
  self.navigationController.toolbarHidden = NO;
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  _hasViewAppeared = YES;
  [self maybeFocusSearchBar];
}

- (void)viewWillDisappear:(BOOL)animated {
  [super viewWillDisappear:animated];

  // viewWillDisappear is also called if you drag the sheet down then release
  // without actually closing.
  if (!_faviconMetricLogged) {
    [self logPercentageMetricForFavicons];
    _faviconMetricLogged = YES;
  }
}

- (void)viewDidDisappear:(BOOL)animated {
  [super viewDidDisappear:animated];

  // Dismiss the search bar if presented; otherwise UIKit may retain it and
  // cause a memory leak. If this dismissal happens before viewWillDisappear
  // (e.g., settingsWillBeDismissed) an internal UIKit crash occurs. See also:
  // crbug.com/947417, crbug.com/1350625. Dismissing in viewDidDisappear to make
  // sure that it happens when the view is well and truly gone.
  if (self.navigationItem.searchController.active == YES) {
    self.navigationItem.searchController.active = NO;
  }
}

- (void)didMoveToParentViewController:(UIViewController*)parent {
  [super didMoveToParentViewController:parent];
  if (!parent) {
    [self.presentationDelegate PasswordManagerViewControllerDismissed];
  }
}

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
  BOOL viewWasInEditMode = self.editing;
  [super setEditing:editing animated:animated];

  // The UI needs to be updated only when we are switching between editing
  // states (i.e. no-edit -> edit and edit -> no-edit). Updating the UI using
  // batchUpdate (or equivalent) when the view is already in edit mode, causes
  // the view to forcibly exit edit mode.
  if (viewWasInEditMode == editing) {
    return;
  }

  [self setSearchBarEnabled:self.shouldEnableSearchBar];
  [self setWidgetPromoItemEnabled:!editing];
  [self updatePasswordCheckButtonWithState:self.passwordCheckState];
  [self updatePasswordCheckStatusLabelWithState:self.passwordCheckState];
  [self setAddPasswordButtonEnabled:!editing];
  if ([self.navigationController.topViewController
          isKindOfClass:[PasswordManagerViewController class]]) {
    [self updateUIForEditState];
  }
  [self reconfigurePasswordCheckSectionCellsWithState:self.passwordCheckState];
}

- (BOOL)hasPasswords {
  return !_affiliatedGroups.empty();
}

- (void)viewWillLayoutSubviews {
  [super viewWillLayoutSubviews];
  [self updateWidgetPromoCellLayoutIfNeeded];
}

#pragma mark - SettingsRootTableViewController

- (void)loadModel {
  [super loadModel];

  if (!_didReceivePasswords) {
    return;
  }

  [self showOrHideEmptyView];

  // If we don't have data or settings to show, add an empty state, then
  // stop so that we don't add anything that overlaps the illustrated
  // background.
  if ([self shouldShowEmptyStateView]) {
    return;
  }

  TableViewModel* model = self.tableViewModel;

  // Don't show sections hidden when search controller is displayed.
  if (!_tableIsInSearchMode) {
    // Manage account header.
    [model addSectionWithIdentifier:SectionIdentifierManageAccountHeader];
    [model setHeader:self.manageAccountLinkItem
        forSectionWithIdentifier:SectionIdentifierManageAccountHeader];

    // Widget promo.
    if (_shouldShowPasswordManagerWidgetPromo) {
      [model addSectionWithIdentifier:SectionIdentifierWidgetPromo];
      [model addItem:self.widgetPromoItem
          toSectionWithIdentifier:SectionIdentifierWidgetPromo];
    }

    // Password check.
    [model addSectionWithIdentifier:SectionIdentifierPasswordCheck];

    [self updatePasswordCheckStatusLabelWithState:_passwordCheckState];
    [model addItem:self.passwordProblemsItem
        toSectionWithIdentifier:SectionIdentifierPasswordCheck];

    [self updatePasswordCheckButtonWithState:_passwordCheckState];

    // Only add check button if the current PasswordCheckUIState requires the
    // button to be shown.
    if (self.shouldShowCheckButton) {
      [model addItem:self.checkForProblemsItem
          toSectionWithIdentifier:SectionIdentifierPasswordCheck];
    }

    // Add Password button.
    if ([self allowsAddPassword]) {
      [model addSectionWithIdentifier:SectionIdentifierAddPasswordButton];
      [model addItem:self.addPasswordItem
          toSectionWithIdentifier:SectionIdentifierAddPasswordButton];
    }
  }

  // Saved passwords.
  if ([self hasPasswords]) {
    [model addSectionWithIdentifier:SectionIdentifierSavedPasswords];
    TableViewTextHeaderFooterItem* headerItem =
        [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader];
    headerItem.text =
        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_SAVED_HEADING);
    [model setHeader:headerItem
        forSectionWithIdentifier:SectionIdentifierSavedPasswords];
  }

  // Blocked passwords.
  if (!_blockedSites.empty()) {
    [model addSectionWithIdentifier:SectionIdentifierBlocked];
    TableViewTextHeaderFooterItem* headerItem =
        [[TableViewTextHeaderFooterItem alloc] initWithType:ItemTypeHeader];
    headerItem.text =
        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORDS_EXCEPTIONS_HEADING);
    [model setHeader:headerItem
        forSectionWithIdentifier:SectionIdentifierBlocked];
  }

  [self filterItems:self.searchTerm];
}

// Returns YES if the array of index path contains a saved password. This is to
// determine if we need to show the user the alert dialog.
- (BOOL)indexPathsContainsSavedPassword:(NSArray<NSIndexPath*>*)indexPaths {
  for (NSIndexPath* indexPath : indexPaths) {
    if ([self.tableViewModel itemTypeForIndexPath:indexPath] ==
        ItemTypeSavedPassword) {
      return YES;
    }
  }
  return NO;
}

- (void)deleteItems:(NSArray<NSIndexPath*>*)indexPaths {
  // Only show the user the alert dialog if the index path array contain at
  // least one saved password.
  if ([self indexPathsContainsSavedPassword:indexPaths]) {
    // Show password delete dialog before deleting the passwords.
    NSMutableArray<NSString*>* origins = [[NSMutableArray alloc] init];
    for (NSIndexPath* indexPath : indexPaths) {
      NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
      if (itemType == ItemTypeSavedPassword) {
        password_manager::AffiliatedGroup affiliatedGroup =
            base::apple::ObjCCastStrict<AffiliatedGroupTableViewItem>(
                [self.tableViewModel itemAtIndexPath:indexPath])
                .affiliatedGroup;
        [origins addObject:base::SysUTF8ToNSString(
                               affiliatedGroup.GetDisplayName())];
      }
    }
    [self.handler
        showPasswordDeleteDialogWithOrigins:origins
                                 completion:^{
                                   [self deleteItemAtIndexPaths:indexPaths];
                                 }];
  } else {
    // Do not call super as this also deletes the section if it is empty.
    [self deleteItemAtIndexPaths:indexPaths];
  }
}

- (BOOL)editButtonEnabled {
  return [self hasPasswords] || !_blockedSites.empty();
}

- (BOOL)shouldHideToolbar {
  return NO;
}

- (BOOL)shouldShowEditDoneButton {
  // The "Done" button in the navigation bar closes the sheet.
  return NO;
}

- (void)updateUIForEditState {
  [super updateUIForEditState];
  [self updatedToolbarForEditState];
}

- (void)editButtonPressed {
  // Disable search bar if the user is bulk editing (edit mode). (Reverse logic
  // because parent method -editButtonPressed is calling setEditing to change
  // the state).
  self.shouldEnableSearchBar = self.tableView.editing;
  [super editButtonPressed];
}

- (UIBarButtonItem*)customLeftToolbarButton {
  return self.tableView.isEditing ? nil : self.settingsButtonInToolbar;
}

- (UIBarButtonItem*)customRightToolbarButton {
  if (!self.tableView.isEditing) {
    // Display Add button on the right side of the toolbar when the empty state
    // is displayed. The Settings button will be on the left. When the tableView
    // is not empty, the Add button is displayed in a row.
    if ([self shouldShowEmptyStateView]) {
      return self.addButtonInToolbar;
    }
  }
  return nil;
}

#pragma mark - SettingsControllerProtocol

- (void)reportDismissalUserAction {
  base::RecordAction(base::UserMetricsAction("MobilePasswordsSettingsClose"));
  _accountManagerServiceObserver.reset();
}

- (void)reportBackUserAction {
  base::RecordAction(base::UserMetricsAction("MobilePasswordsSettingsBack"));
  _accountManagerServiceObserver.reset();
}

- (void)settingsWillBeDismissed {
  CHECK(self.prefService);
  _accountManagerServiceObserver.reset();
  self.prefService = nullptr;
}

#pragma mark - Items
- (TableViewLinkHeaderFooterItem*)manageAccountLinkItem {
  if (_manageAccountLinkItem) {
    return _manageAccountLinkItem;
  }

  _manageAccountLinkItem =
      [[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeLinkHeader];

  if (_savingPasswordsToAccount) {
    _manageAccountLinkItem.text =
        l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT_HEADER);

    _manageAccountLinkItem.urls = @[ [[CrURL alloc]
        initWithGURL:
            google_util::AppendGoogleLocaleParam(
                GURL(password_manager::kPasswordManagerHelpCenteriOSURL),
                GetApplicationContext()->GetApplicationLocale())] ];
  } else {
    _manageAccountLinkItem.text =
        l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER_HEADER_NOT_SYNCING);
    _manageAccountLinkItem.urls = @[];
  }

  return _manageAccountLinkItem;
}

- (InlinePromoItem*)widgetPromoItem {
  if (_widgetPromoItem) {
    return _widgetPromoItem;
  }

  _widgetPromoItem = [[InlinePromoItem alloc] initWithType:ItemTypeWidgetPromo];
  _widgetPromoItem.promoImage = [UIImage imageNamed:WidgetPromoImageName()];
  _widgetPromoItem.promoText =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER_WIDGET_PROMO_TEXT);
  _widgetPromoItem.moreInfoButtonTitle = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_MANAGER_WIDGET_PROMO_BUTTON_TITLE);
  _widgetPromoItem.shouldHaveWideLayout =
      [self shouldWidgetPromoCellHaveWideLayout];
  _widgetPromoItem.accessibilityIdentifier = kWidgetPromoID;
  return _widgetPromoItem;
}

- (SettingsCheckItem*)passwordProblemsItem {
  if (_passwordProblemsItem) {
    return _passwordProblemsItem;
  }

  _passwordProblemsItem =
      [[SettingsCheckItem alloc] initWithType:ItemTypePasswordCheckStatus];
  _passwordProblemsItem.enabled = NO;
  _passwordProblemsItem.text = l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP);
  _passwordProblemsItem.detailText =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_DESCRIPTION);
  _passwordProblemsItem.accessibilityTraits = UIAccessibilityTraitHeader;
  return _passwordProblemsItem;
}

- (TableViewTextItem*)checkForProblemsItem {
  if (_checkForProblemsItem) {
    return _checkForProblemsItem;
  }

  _checkForProblemsItem =
      [[TableViewTextItem alloc] initWithType:ItemTypeCheckForProblemsButton];
  _checkForProblemsItem.text =
      l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS_NOW_BUTTON);
  _checkForProblemsItem.textColor = [UIColor colorNamed:kTextSecondaryColor];
  _checkForProblemsItem.accessibilityTraits = UIAccessibilityTraitButton;
  return _checkForProblemsItem;
}

- (TableViewTextItem*)addPasswordItem {
  if (_addPasswordItem) {
    return _addPasswordItem;
  }

  _addPasswordItem =
      [[TableViewTextItem alloc] initWithType:ItemTypeAddPasswordButton];
  _addPasswordItem.text = l10n_util::GetNSString(IDS_IOS_ADD_PASSWORD);
  _addPasswordItem.accessibilityIdentifier = kAddPasswordButtonID;
  _addPasswordItem.accessibilityTraits = UIAccessibilityTraitButton;
  _addPasswordItem.textColor = [UIColor colorNamed:kBlueColor];
  return _addPasswordItem;
}

- (AffiliatedGroupTableViewItem*)savedFormItemForAffiliatedGroup:
    (const password_manager::AffiliatedGroup&)affiliatedGroup {
  AffiliatedGroupTableViewItem* passwordItem =
      [[AffiliatedGroupTableViewItem alloc] initWithType:ItemTypeSavedPassword];
  passwordItem.affiliatedGroup = affiliatedGroup;
  passwordItem.showLocalOnlyIcon =
      [self.delegate shouldShowLocalOnlyIconForGroup:affiliatedGroup];
  passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
  passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

  if (_mostRecentlyUpdatedCred) {
    // Find the affiliated group item with a credential on the same sign-on
    // realm as the most recently updated credential.
    auto groupCreds = affiliatedGroup.GetCredentials();
    auto mostRecentlyUpdatedCred = *_mostRecentlyUpdatedCred;
    auto pred = [&mostRecentlyUpdatedCred](
                    const password_manager::CredentialUIEntry& entry) {
      return mostRecentlyUpdatedCred.GetFirstSignonRealm() ==
             entry.GetFirstSignonRealm();
    };
    if (auto it = std::find_if(groupCreds.begin(), groupCreds.end(), pred);
        it != groupCreds.end()) {
      self.mostRecentlyUpdatedItem = passwordItem;
      _mostRecentlyUpdatedCred = std::nullopt;
    }
  }
  return passwordItem;
}

- (BlockedSiteTableViewItem*)blockedSiteItem:
    (const password_manager::CredentialUIEntry&)credential {
  BlockedSiteTableViewItem* passwordItem =
      [[BlockedSiteTableViewItem alloc] initWithType:ItemTypeBlocked];
  passwordItem.credential = credential;
  passwordItem.accessibilityTraits |= UIAccessibilityTraitButton;
  passwordItem.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  return passwordItem;
}

#pragma mark - PopoverLabelViewControllerDelegate

- (void)didTapLinkURL:(NSURL*)URL {
  [self view:nil didTapLinkURL:[[CrURL alloc] initWithNSURL:URL]];
}

#pragma mark - Actions

// Called when user tapped on the information button of the password check
// item. Shows popover with detailed description of an error.
- (void)didTapPasswordCheckInfoButton:(UIButton*)buttonView {
  NSAttributedString* info = [self.delegate passwordCheckErrorInfo];
  // If no info returned by mediator handle this tap as tap on a cell.
  if (!info) {
    [self showPasswordCheckupPage];
    return;
  }

  PopoverLabelViewController* errorInfoPopover =
      [[PopoverLabelViewController alloc] initWithPrimaryAttributedString:info
                                                secondaryAttributedString:nil];
  errorInfoPopover.delegate = self;

  errorInfoPopover.popoverPresentationController.sourceView = buttonView;
  errorInfoPopover.popoverPresentationController.sourceRect = buttonView.bounds;
  errorInfoPopover.popoverPresentationController.permittedArrowDirections =
      UIPopoverArrowDirectionAny;
  [self presentViewController:errorInfoPopover animated:YES completion:nil];
}

- (void)didTapWidgetPromoCloseButton {
  UmaHistogramEnumeration(kPasswordManagerWidgetPromoActionHistogram,
                          PasswordManagerWidgetPromoAction::kClose);

  [self clearSectionWithIdentifier:SectionIdentifierWidgetPromo
                  withRowAnimation:UITableViewRowAnimationFade];
  [self.delegate notifyFETOfPasswordManagerWidgetPromoDismissal];
}

- (void)didTapWidgetPromoMoreInfoButton {
  UmaHistogramEnumeration(kPasswordManagerWidgetPromoActionHistogram,
                          PasswordManagerWidgetPromoAction::kOpenInstructions);

  [self.presentationDelegate showPasswordManagerWidgetPromoInstructions];
}

#pragma mark - PasswordsConsumer

- (void)setPasswordCheckUIState:(PasswordCheckUIState)state
         insecurePasswordsCount:(NSInteger)insecureCount {
  self.insecurePasswordsCount = insecureCount;
  // Update password check status and check button with new state.
  [self updatePasswordCheckButtonWithState:state];
  [self updatePasswordCheckStatusLabelWithState:state];

  // During searching Password Check section is hidden so cells should not be
  // reconfigured.
  if (_tableIsInSearchMode) {
    _passwordCheckState = state;
    return;
  }

  [self reconfigurePasswordCheckSectionCellsWithState:state];

  _passwordCheckState = state;
}

- (void)setSavingPasswordsToAccount:(BOOL)savingPasswordsToAccount {
  if (_savingPasswordsToAccount == savingPasswordsToAccount) {
    return;
  }
  _savingPasswordsToAccount = savingPasswordsToAccount;
  [self reloadData];
}

- (void)setAffiliatedGroups:
            (const std::vector<password_manager::AffiliatedGroup>&)
                affiliatedGroups
               blockedSites:
                   (const std::vector<password_manager::CredentialUIEntry>&)
                       blockedSites {
  if (self.deletionInProgress) {
    return;
  }

  if (!_didReceivePasswords) {
    _blockedSites = blockedSites;
    _affiliatedGroups = affiliatedGroups;
    [self hideLoadingSpinnerBackground];
  } else {
    // The AffiliatedGroup equality operator ignores the password stores, but
    // this UI cares, see password_manager::ShouldShowLocalOnlyIcon().
    // The AffiliatedGroup equality operator ignores password notes, but the UI
    // should be updated so that any changes to just notes are visible.
    if (_affiliatedGroups == affiliatedGroups &&
        _blockedSites == blockedSites &&
        AreStoresEqual(_affiliatedGroups, affiliatedGroups) &&
        AreIssuesEqual(_affiliatedGroups, affiliatedGroups) &&
        AreNotesEqual(_affiliatedGroups, affiliatedGroups)) {
      return;
    }

    _blockedSites = blockedSites;
    _affiliatedGroups = affiliatedGroups;

    [self updatePasswordManagerUI];
  }
}

- (void)updatePasswordManagerUI {
  if ([self shouldShowEmptyStateView]) {
    // Force UI update, as setting table view's editing state to disabled might
    // not update the UI.
    [self updateUIForEditState];
    [self setEditing:NO animated:YES];
    [self reloadData];
    return;
  }

  // Update the UI for the edit state to make sure it reflects the content in
  // the table as the content may have changed since the view controller was
  // created.
  if ([self.navigationController.topViewController
          isKindOfClass:[PasswordManagerViewController class]]) {
    [self updateUIForEditState];
  }

  TableViewModel* model = self.tableViewModel;
  NSMutableIndexSet* sectionIdentifiersToUpdate = [NSMutableIndexSet indexSet];

  // Hold in reverse order of section indexes (bottom up of section
  // displayed). If we don't we'll cause a crash.
  std::vector<PasswordSectionIdentifier> sections = {
      SectionIdentifierBlocked, SectionIdentifierSavedPasswords};

  for (const auto& section : sections) {
    bool hasSection = [model hasSectionForSectionIdentifier:section];
    bool needsSection = section == SectionIdentifierBlocked
                            ? !_blockedSites.empty()
                            : [self hasPasswords];

    // If section exists but it shouldn't - gracefully remove it with
    // animation.
    if (!needsSection && hasSection) {
      [self clearSectionWithIdentifier:section
                      withRowAnimation:UITableViewRowAnimationAutomatic];
    }
    // If section exists and it should - reload it.
    else if (needsSection && hasSection) {
      [sectionIdentifiersToUpdate addIndex:section];
    }
    // If section doesn't exist but it should - add it.
    else if (needsSection && !hasSection) {
      // This is very rare condition, in this case just reload all data and
      // update the toolbar UI.
      //  We want to update the toolbar only if the current view is the Password
      //  Manager.
      if ([self.navigationController.topViewController
              isKindOfClass:[PasswordManagerViewController class]]) {
        [self updateUIForEditState];
      }
      [self reloadData];
      return;
    }
  }

  // After deleting any sections, calculate the indices of sections to be
  // updated. Doing this before deleting sections will lead to incorrect indices
  // and possible crashes.
  NSMutableIndexSet* sectionsToUpdate = [NSMutableIndexSet indexSet];
  [sectionIdentifiersToUpdate
      enumerateIndexesUsingBlock:^(NSUInteger sectionIdentifier, BOOL* stop) {
        [sectionsToUpdate
            addIndex:[model sectionForSectionIdentifier:sectionIdentifier]];
      }];

  // Reload items in sections.
  if (sectionsToUpdate.count > 0) {
    [self filterItems:self.searchTerm];
    [self.tableView reloadSections:sectionsToUpdate
                  withRowAnimation:UITableViewRowAnimationAutomatic];
    [self scrollToLastUpdatedItem];
  } else if (_affiliatedGroups.empty() && _blockedSites.empty()) {
    [self setEditing:NO animated:YES];
  }
}

- (void)setShouldShowPasswordManagerWidgetPromo:
    (BOOL)shouldShowPasswordManagerWidgetPromo {
  _shouldShowPasswordManagerWidgetPromo = shouldShowPasswordManagerWidgetPromo;

  // Reload data to display the promo. No to need to reload before the view is
  // loaded, as loading the view triggers a data reload.
  if (self.viewLoaded) {
    [self reloadData];
  }
}

#pragma mark - UISearchControllerDelegate

- (void)willPresentSearchController:(UISearchController*)searchController {
  // This is needed to remove the transparency of the navigation bar at scroll
  // edge in iOS 15+ to prevent the following UITableViewRowAnimationTop
  // animations from being visible through the navigation bar.
  self.navigationController.navigationBar.backgroundColor =
      [UIColor colorNamed:kGroupedPrimaryBackgroundColor];

  [self showScrim];
  // Remove save passwords switch section, password check section and
  // on device encryption.

  _tableIsInSearchMode = YES;
  [self
      performBatchTableViewUpdates:^{
        // Sections must be removed from bottom to top, otherwise it crashes
        [self clearSectionWithIdentifier:SectionIdentifierAddPasswordButton
                        withRowAnimation:UITableViewRowAnimationTop];

        [self clearSectionWithIdentifier:SectionIdentifierPasswordCheck
                        withRowAnimation:UITableViewRowAnimationTop];

        [self clearSectionWithIdentifier:SectionIdentifierWidgetPromo
                        withRowAnimation:UITableViewRowAnimationTop];

        [self clearSectionWithIdentifier:SectionIdentifierManageAccountHeader
                        withRowAnimation:UITableViewRowAnimationTop];

        // Hide the toolbar when the search controller is presented.
        self.navigationController.toolbarHidden = YES;
      }
                        completion:nil];
}

- (void)willDismissSearchController:(UISearchController*)searchController {
  _searchPasswordsUserActionWasRecorded = false;

  // This is needed to restore the transparency of the navigation bar at
  // scroll edge in iOS 15+.
  self.navigationController.navigationBar.backgroundColor = nil;

  // No need to restore UI if the Password Manager is being dismissed or if a
  // previous call to `willDismissSearchController` already restored the UI.
  if (self.navigationController.isBeingDismissed || !_tableIsInSearchMode) {
    return;
  }

  [self hideScrim];
  [self searchForTerm:@""];
  // Recover save passwords switch section.
  TableViewModel* model = self.tableViewModel;
  [self.tableView
      performBatchUpdates:^{
        int sectionIndex = 0;
        NSMutableArray<NSIndexPath*>* rowsIndexPaths =
            [[NSMutableArray alloc] init];

        // Add manage account header.
        [model insertSectionWithIdentifier:SectionIdentifierManageAccountHeader
                                   atIndex:sectionIndex];
        [model setHeader:self.manageAccountLinkItem
            forSectionWithIdentifier:SectionIdentifierManageAccountHeader];
        [self.tableView
              insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
            withRowAnimation:UITableViewRowAnimationTop];

        sectionIndex++;
        // Add widget promo section.
        if (_shouldShowPasswordManagerWidgetPromo) {
          [model insertSectionWithIdentifier:SectionIdentifierWidgetPromo
                                     atIndex:sectionIndex];
          [self.tableView
                insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
              withRowAnimation:UITableViewRowAnimationTop];
          [model addItem:self.widgetPromoItem
              toSectionWithIdentifier:SectionIdentifierWidgetPromo];

          sectionIndex++;
        }

        // Add "Password check" section.
        [model insertSectionWithIdentifier:SectionIdentifierPasswordCheck
                                   atIndex:sectionIndex];
        NSInteger checkSection =
            [model sectionForSectionIdentifier:SectionIdentifierPasswordCheck];

        [self.tableView
              insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
            withRowAnimation:UITableViewRowAnimationTop];
        [model addItem:self.passwordProblemsItem
            toSectionWithIdentifier:SectionIdentifierPasswordCheck];
        [rowsIndexPaths addObject:[NSIndexPath indexPathForRow:0
                                                     inSection:checkSection]];
        // Only add check button if the current PasswordCheckUIState requires
        // the button to be shown.
        if (self.shouldShowCheckButton) {
          [model addItem:self.checkForProblemsItem
              toSectionWithIdentifier:SectionIdentifierPasswordCheck];

          [rowsIndexPaths addObject:[NSIndexPath indexPathForRow:1
                                                       inSection:checkSection]];
        }
        sectionIndex++;

        // Add "Add Password" button.
        if ([self allowsAddPassword]) {
          [model insertSectionWithIdentifier:SectionIdentifierAddPasswordButton
                                     atIndex:sectionIndex];
          [self.tableView
                insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]
              withRowAnimation:UITableViewRowAnimationTop];
          [model addItem:self.addPasswordItem
              toSectionWithIdentifier:SectionIdentifierAddPasswordButton];
          [rowsIndexPaths
              addObject:
                  [NSIndexPath
                      indexPathForRow:0
                            inSection:
                                [model
                                    sectionForSectionIdentifier:
                                        SectionIdentifierAddPasswordButton]]];
          sectionIndex++;
        }

        [self.tableView insertRowsAtIndexPaths:rowsIndexPaths
                              withRowAnimation:UITableViewRowAnimationTop];

        //  We want to restart the toolbar (display it) when the search bar is
        //  dismissed only if the current view is the Password Manager.
        if ([self.navigationController.topViewController
                isKindOfClass:[PasswordManagerViewController class]]) {
          self.navigationController.toolbarHidden = NO;
        }

        _tableIsInSearchMode = NO;
      }
               completion:nil];
}

#pragma mark - UISearchBarDelegate

- (void)searchBar:(UISearchBar*)searchBar textDidChange:(NSString*)searchText {
  if (searchText.length == 0 && self.navigationItem.searchController.active) {
    [self showScrim];
  } else {
    [self hideScrim];
  }

  [self searchForTerm:searchText];

  // Only record a search user action once per search session.
  if (!_searchPasswordsUserActionWasRecorded) {
    base::RecordAction(
        base::UserMetricsAction("MobilePasswordManagerSearchPasswords"));
    _searchPasswordsUserActionWasRecorded = YES;
  }
}

#pragma mark - Private methods

// Shows loading spinner background view.
- (void)showLoadingSpinnerBackground {
  if (!self.spinnerView) {
    self.spinnerView =
        [[HomeWaitingView alloc] initWithFrame:self.tableView.bounds
                               backgroundColor:UIColor.clearColor];
    [self.spinnerView startWaiting];
  }
  self.navigationItem.searchController.searchBar.userInteractionEnabled = NO;
  self.tableView.backgroundView = self.spinnerView;
}

// Hide the loading spinner if it is showing.
- (void)hideLoadingSpinnerBackground {
  DCHECK(self.spinnerView);
  __weak __typeof(self) weakSelf = self;
  [self.spinnerView stopWaitingWithCompletion:^{
    [UIView animateWithDuration:0.2
        animations:^{
          self.spinnerView.alpha = 0.0;
        }
        completion:^(BOOL finished) {
          [weakSelf didHideSpinner];
        }];
  }];
}

// Called after the loading spinner hiding animation finished. Updates
// `tableViewModel` and then the view hierarchy.
- (void)didHideSpinner {
  // Remove spinner view after animation finished.
  self.navigationItem.searchController.searchBar.userInteractionEnabled = YES;
  self.tableView.backgroundView = nil;
  self.spinnerView = nil;
  // Update model and view hierarchy.
  _didReceivePasswords = YES;
  [self updateUIForEditState];
  [self reloadData];
}

// Dismisses the search controller when there's a touch event on the scrim.
- (void)dismissSearchController:(UIControl*)sender {
  if (self.navigationItem.searchController.active) {
    self.navigationItem.searchController.active = NO;
  }
}

// Shows scrim overlay and hide toolbar.
- (void)showScrim {
  if (self.scrimView.alpha < 1.0f) {
    self.scrimView.alpha = 0.0f;
    [self.tableView addSubview:self.scrimView];
    // We attach our constraints to the superview because the tableView is
    // a scrollView and it seems that we get an empty frame when attaching to
    // it.
    AddSameConstraints(self.scrimView, self.view.superview);
    self.tableView.accessibilityElementsHidden = YES;
    self.tableView.scrollEnabled = NO;
    [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
                     animations:^{
                       self.scrimView.alpha = 1.0f;
                       [self.view layoutIfNeeded];
                     }];
  }
}

// Hides scrim and restore toolbar.
- (void)hideScrim {
  if (self.scrimView.alpha > 0.0f) {
    [UIView animateWithDuration:kTableViewNavigationScrimFadeDuration
        animations:^{
          self.scrimView.alpha = 0.0f;
        }
        completion:^(BOOL finished) {
          [self.scrimView removeFromSuperview];
          self.tableView.accessibilityElementsHidden = NO;
          self.tableView.scrollEnabled = YES;
        }];
  }
}

- (void)searchForTerm:(NSString*)searchTerm {
  self.searchTerm = searchTerm;
  [self filterItems:searchTerm];

  TableViewModel* model = self.tableViewModel;
  NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
  if ([model hasSectionForSectionIdentifier:SectionIdentifierSavedPasswords]) {
    NSInteger passwordSection =
        [model sectionForSectionIdentifier:SectionIdentifierSavedPasswords];
    [indexSet addIndex:passwordSection];
  }
  if ([model hasSectionForSectionIdentifier:SectionIdentifierBlocked]) {
    NSInteger blockedSection =
        [model sectionForSectionIdentifier:SectionIdentifierBlocked];
    [indexSet addIndex:blockedSection];
  }
  if (indexSet.count > 0) {
    BOOL animationsWereEnabled = [UIView areAnimationsEnabled];
    [UIView setAnimationsEnabled:NO];
    [self.tableView reloadSections:indexSet
                  withRowAnimation:UITableViewRowAnimationAutomatic];
    [UIView setAnimationsEnabled:animationsWereEnabled];
  }
}

// Adds filtered list of saved passwords.
- (void)addPasswordsSectionWithSearchTerm:(NSString*)searchTerm {
  const std::string searchTermStr =
      searchTerm.length == 0
          ? std::string()
          : base::ToLowerASCII(base::SysNSStringToUTF8(searchTerm));
  for (const auto& affiliatedGroup : _affiliatedGroups) {
    if (searchTermStr.empty() || password_manager::MatchAffiliatedGroupsForTerm(
                                     affiliatedGroup, searchTermStr)) {
      AffiliatedGroupTableViewItem* item =
          [self savedFormItemForAffiliatedGroup:affiliatedGroup];
      [self.tableViewModel addItem:item
           toSectionWithIdentifier:SectionIdentifierSavedPasswords];
    }
  }
}

// Adds filtered list of blocked sites.
- (void)addBlockedSitesSectionWithSearchTerm:(NSString*)searchTerm {
  const std::string searchTermStr =
      searchTerm.length == 0
          ? std::string()
          : base::ToLowerASCII(base::SysNSStringToUTF8(searchTerm));
  for (const auto& credential : _blockedSites) {
    if (searchTermStr.empty() ||
        password_manager::MatchCredentialForTerm(credential, searchTermStr)) {
      BlockedSiteTableViewItem* item = [self blockedSiteItem:credential];
      [self.tableViewModel addItem:item
           toSectionWithIdentifier:SectionIdentifierBlocked];
    }
  }
}

// Rebuilds the filtered list of passwords/blocked based on given
// `searchTerm`.
- (void)filterItems:(NSString*)searchTerm {
  TableViewModel* model = self.tableViewModel;

  if ([self hasPasswords]) {
    [model deleteAllItemsFromSectionWithIdentifier:
               SectionIdentifierSavedPasswords];
    [self addPasswordsSectionWithSearchTerm:searchTerm];
  }

  if (!_blockedSites.empty()) {
    [model deleteAllItemsFromSectionWithIdentifier:SectionIdentifierBlocked];
    [self addBlockedSitesSectionWithSearchTerm:searchTerm];
  }
}

// Updates password check button according to provided state.
- (void)updatePasswordCheckButtonWithState:(PasswordCheckUIState)state {
  self.checkForProblemsItem.text =
      l10n_util::GetNSString(IDS_IOS_CHECK_PASSWORDS_NOW_BUTTON);

  if (self.editing) {
    [self setCheckForProblemsItemEnabled:NO];
    return;
  }

  switch (state) {
    case PasswordCheckStateSafe:
    case PasswordCheckStateUnmutedCompromisedPasswords:
    case PasswordCheckStateReusedPasswords:
    case PasswordCheckStateWeakPasswords:
    case PasswordCheckStateDismissedWarnings:
    case PasswordCheckStateRunning:
      self.shouldShowCheckButton = NO;
      break;
    case PasswordCheckStateDefault:
    case PasswordCheckStateError:
      self.shouldShowCheckButton = YES;
      [self setCheckForProblemsItemEnabled:YES];
      break;
    case PasswordCheckStateSignedOut:
      self.shouldShowCheckButton = YES;
      [self setCheckForProblemsItemEnabled:NO];
      break;
    // Fall through.
    case PasswordCheckStateDisabled:
      self.shouldShowCheckButton = YES;
      [self setCheckForProblemsItemEnabled:NO];
      break;
  }
}

// Updates password check status label according to provided state.
- (void)updatePasswordCheckStatusLabelWithState:(PasswordCheckUIState)state {
  self.passwordProblemsItem.trailingImage = nil;
  self.passwordProblemsItem.trailingImageTintColor = nil;
  self.passwordProblemsItem.enabled = !self.editing;
  self.passwordProblemsItem.indicatorHidden = YES;
  self.passwordProblemsItem.infoButtonHidden = YES;
  self.passwordProblemsItem.accessoryType =
      IsPasswordCheckTappable(state)
          ? UITableViewCellAccessoryDisclosureIndicator
          : UITableViewCellAccessoryNone;
  self.passwordProblemsItem.text =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP);
  self.passwordProblemsItem.detailText =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_DESCRIPTION);

  switch (state) {
    case PasswordCheckStateRunning: {
      self.passwordProblemsItem.text =
          l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_ONGOING);
      self.passwordProblemsItem.detailText =
          base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
              IDS_IOS_PASSWORD_CHECKUP_SITES_AND_APPS_COUNT,
              _affiliatedGroups.size()));
      self.passwordProblemsItem.indicatorHidden = NO;
      break;
    }
    case PasswordCheckStateDisabled: {
      self.passwordProblemsItem.enabled = NO;
      break;
    }
    case PasswordCheckStateUnmutedCompromisedPasswords: {
      self.passwordProblemsItem.detailText =
          base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
              IDS_IOS_PASSWORD_CHECKUP_COMPROMISED_COUNT,
              self.insecurePasswordsCount));
      self.passwordProblemsItem.warningState = WarningState::kSevereWarning;
      break;
    }
    case PasswordCheckStateReusedPasswords: {
      self.passwordProblemsItem.detailText = l10n_util::GetNSStringF(
          IDS_IOS_PASSWORD_CHECKUP_REUSED_COUNT,
          base::NumberToString16(self.insecurePasswordsCount));
      self.passwordProblemsItem.warningState = WarningState::kWarning;
      break;
    }
    case PasswordCheckStateWeakPasswords: {
      self.passwordProblemsItem.detailText = base::SysUTF16ToNSString(
          l10n_util::GetPluralStringFUTF16(IDS_IOS_PASSWORD_CHECKUP_WEAK_COUNT,
                                           self.insecurePasswordsCount));
      self.passwordProblemsItem.warningState = WarningState::kWarning;
      break;
    }
    case PasswordCheckStateDismissedWarnings: {
      self.passwordProblemsItem.detailText =
          base::SysUTF16ToNSString(l10n_util::GetPluralStringFUTF16(
              IDS_IOS_PASSWORD_CHECKUP_DISMISSED_COUNT,
              self.insecurePasswordsCount));
      self.passwordProblemsItem.warningState = WarningState::kWarning;
      break;
    }
    case PasswordCheckStateSafe: {
      self.passwordProblemsItem.detailText =
          [self.delegate formattedElapsedTimeSinceLastCheck];
      self.passwordProblemsItem.warningState = WarningState::kSafe;
      break;
    }
    case PasswordCheckStateDefault:
      break;
    case PasswordCheckStateError:
    case PasswordCheckStateSignedOut: {
      self.passwordProblemsItem.detailText =
          l10n_util::GetNSString(IDS_IOS_PASSWORD_CHECKUP_ERROR);
      self.passwordProblemsItem.infoButtonHidden = NO;
      break;
    }
  }

  // Notify accessibility to focus on the Password Check Status cell if needed.
  if ([self shouldFocusAccessibilityOnPasswordCheckStatusForState:state]) {
    [self focusAccessibilityOnPasswordCheckStatus];
    self.checkWasTriggeredManually = NO;
  }

  // Apply the changes to the "Password Check" cell.
  [self reconfigureCellsForItems:@[ self.passwordProblemsItem ]];
}

// Enables or disables the `widgetPromoItem`.
- (void)setWidgetPromoItemEnabled:(BOOL)enabled {
  if (self.widgetPromoItem.enabled == enabled) {
    return;
  }

  self.widgetPromoItem.enabled = enabled;
  self.widgetPromoItem.promoImage =
      [UIImage imageNamed:enabled ? WidgetPromoImageName()
                                  : WidgetPromoDisabledImageName()];
  [self reconfigureCellsForItems:@[ self.widgetPromoItem ]];
}

// Enables or disables the `checkForProblemsItem` and sets it up accordingly.
- (void)setCheckForProblemsItemEnabled:(BOOL)enabled {
  self.checkForProblemsItem.enabled = enabled;
  if (enabled) {
    self.checkForProblemsItem.textColor = [UIColor colorNamed:kBlueColor];
    self.checkForProblemsItem.accessibilityTraits &=
        ~UIAccessibilityTraitNotEnabled;
  } else {
    self.checkForProblemsItem.textColor =
        [UIColor colorNamed:kTextSecondaryColor];
    self.checkForProblemsItem.accessibilityTraits |=
        UIAccessibilityTraitNotEnabled;
  }
}

- (void)setAddPasswordButtonEnabled:(BOOL)enabled {
  if (enabled) {
    self.addPasswordItem.textColor = [UIColor colorNamed:kBlueColor];
    self.addPasswordItem.accessibilityTraits &= ~UIAccessibilityTraitNotEnabled;
  } else {
    self.addPasswordItem.textColor = [UIColor colorNamed:kTextSecondaryColor];
    self.addPasswordItem.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
  }
  [self reconfigureCellsForItems:@[ self.addPasswordItem ]];
}

// Removes the given section if it exists.
- (void)clearSectionWithIdentifier:(NSInteger)sectionIdentifier
                  withRowAnimation:(UITableViewRowAnimation)animation {
  TableViewModel* model = self.tableViewModel;
  if ([model hasSectionForSectionIdentifier:sectionIdentifier]) {
    NSInteger section = [model sectionForSectionIdentifier:sectionIdentifier];
    [model removeSectionWithIdentifier:sectionIdentifier];
    [[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:section]
                    withRowAnimation:animation];
  }
}

- (void)deleteItemAtIndexPaths:(NSArray<NSIndexPath*>*)indexPaths {
  self.deletionInProgress = YES;

  std::vector<password_manager::CredentialUIEntry> credentialsToDelete;
  for (NSIndexPath* indexPath in indexPaths) {
    // Only form items are editable.
    NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
    TableViewItem* item = [self.tableViewModel itemAtIndexPath:indexPath];

    // Remove affiliated group.
    if (itemType == ItemTypeSavedPassword) {
      password_manager::AffiliatedGroup affiliatedGroup =
          base::apple::ObjCCastStrict<AffiliatedGroupTableViewItem>(item)
              .affiliatedGroup;

      // Remove from local cache.
      auto iterator = base::ranges::find(_affiliatedGroups, affiliatedGroup);
      if (iterator != _affiliatedGroups.end())
        _affiliatedGroups.erase(iterator);

      // Add to the credentials to delete vector to remove from store.
      credentialsToDelete.insert(credentialsToDelete.end(),
                                 affiliatedGroup.GetCredentials().begin(),
                                 affiliatedGroup.GetCredentials().end());
    } else if (itemType == ItemTypeBlocked) {
      password_manager::CredentialUIEntry credential =
          base::apple::ObjCCastStrict<BlockedSiteTableViewItem>(item)
              .credential;

      auto removeCredential =
          [](std::vector<password_manager::CredentialUIEntry>& credentials,
             const password_manager::CredentialUIEntry& credential) {
            auto iterator = base::ranges::find(credentials, credential);
            if (iterator != credentials.end())
              credentials.erase(iterator);
          };
      removeCredential(_blockedSites, credential);
      credentialsToDelete.push_back(std::move(credential));
    }
  }

  // Remove empty sections.
  __weak PasswordManagerViewController* weakSelf = self;
  [self.tableView
      performBatchUpdates:^{
        PasswordManagerViewController* strongSelf = weakSelf;
        if (!strongSelf)
          return;

        [strongSelf removeFromModelItemAtIndexPaths:indexPaths];
        [strongSelf.tableView
            deleteRowsAtIndexPaths:indexPaths
                  withRowAnimation:UITableViewRowAnimationAutomatic];

        // Delete in reverse order of section indexes (bottom up of section
        // displayed), so that indexes in model matches those in the view.  if
        // we don't we'll cause a crash.
        if (strongSelf->_blockedSites.empty()) {
          [strongSelf
              clearSectionWithIdentifier:SectionIdentifierBlocked
                        withRowAnimation:UITableViewRowAnimationAutomatic];
        }
        if (![strongSelf hasPasswords]) {
          [strongSelf
              clearSectionWithIdentifier:SectionIdentifierSavedPasswords
                        withRowAnimation:UITableViewRowAnimationAutomatic];
        }
      }
      completion:^(BOOL finished) {
        PasswordManagerViewController* strongSelf = weakSelf;
        if (!strongSelf)
          return;
        // If both lists are empty, exit editing mode.
        if (![strongSelf hasPasswords] && strongSelf->_blockedSites.empty()) {
          [strongSelf setEditing:NO animated:YES];
          // An illustrated empty state is required, so reload the whole model.
          [strongSelf reloadData];
        }
        [strongSelf updateUIForEditState];
        strongSelf.deletionInProgress = NO;
      }];
  [self.delegate deleteCredentials:credentialsToDelete];
}

// Notifies the handler to show the Password Checkup homepage if the state of
// the Password Check cell allows it.
- (void)showPasswordCheckupPage {
  if (!IsPasswordCheckTappable(self.passwordCheckState)) {
    return;
  }
  base::RecordAction(
      base::UserMetricsAction("MobilePasswordManagerOpenPasswordCheckup"));
  [self.handler showPasswordCheckup];
}

// Scrolls the password lists such that most recently updated
// SavedFormContentItem is in the top of the screen.
- (void)scrollToLastUpdatedItem {
  if (self.mostRecentlyUpdatedItem) {
    NSIndexPath* indexPath =
        [self.tableViewModel indexPathForItem:self.mostRecentlyUpdatedItem];
    [self.tableView scrollToRowAtIndexPath:indexPath
                          atScrollPosition:UITableViewScrollPositionTop
                                  animated:NO];
    self.mostRecentlyUpdatedItem = nil;
  }
}

// Returns YES if accessibility should focus on the Password Check Status cell.
- (BOOL)shouldFocusAccessibilityOnPasswordCheckStatusForState:
    (PasswordCheckUIState)state {
  if (!UIAccessibilityIsVoiceOverRunning()) {
    return false;
  }

  BOOL passwordCheckStateIsValid = state != PasswordCheckStateDefault &&
                                   state != PasswordCheckStateRunning &&
                                   state != PasswordCheckStateDisabled;

  // Accessibility should focus on the Password Check Status cell when:
  // 1. The password check was triggered manually (because the "Check Now"
  // button dissapears afterwards, so the focus should move to the status cell).
  // OR
  // 2. The focus was already on the Password Check Status cell. AND
  // 3. The password check state changed to insecure (compromised, reused, weak
  // or dismissed warnings), safe or error (i.e., any state other than default,
  // running and disabled).
  return self.checkWasTriggeredManually ||
         ([self isPasswordCheckStatusFocusedByVoiceOver] &&
          passwordCheckStateIsValid);
}

// Returns YES if the Password Check Staus cell is currently focused by
// accessibility.
- (BOOL)isPasswordCheckStatusFocusedByVoiceOver {
  if (![self.tableViewModel
          hasItemForItemType:ItemTypePasswordCheckStatus
           sectionIdentifier:SectionIdentifierPasswordCheck]) {
    return false;
  }

  // Get the Password Check Status cell.
  NSIndexPath* indexPath =
      [self.tableViewModel indexPathForItemType:ItemTypePasswordCheckStatus
                              sectionIdentifier:SectionIdentifierPasswordCheck];
  UITableViewCell* passwordCheckStatusCell =
      [self.tableView cellForRowAtIndexPath:indexPath];

  // Get the view element that is currently focused.
  UIAccessibilityElement* focusedElement = UIAccessibilityFocusedElement(
      UIAccessibilityNotificationVoiceOverIdentifier);

  return [passwordCheckStatusCell.accessibilityLabel
      isEqualToString:focusedElement.accessibilityLabel];
}

// Notifies accessibility to focus on the Password Check Status cell when its
// layout changed.
- (void)focusAccessibilityOnPasswordCheckStatus {
  if ([self.tableViewModel hasItemForItemType:ItemTypePasswordCheckStatus
                            sectionIdentifier:SectionIdentifierPasswordCheck]) {
    NSIndexPath* indexPath = [self.tableViewModel
        indexPathForItemType:ItemTypePasswordCheckStatus
           sectionIdentifier:SectionIdentifierPasswordCheck];
    UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
    UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
                                    cell);
  }
}

- (void)setPasswordProblemsItemAccessibilityLabelForSafeState {
  NSIndexPath* indexPath =
      [self.tableViewModel indexPathForItemType:ItemTypePasswordCheckStatus
                              sectionIdentifier:SectionIdentifierPasswordCheck];
  UITableViewCell* passwordProblemsCell =
      [self.tableView cellForRowAtIndexPath:indexPath];
  passwordProblemsCell.accessibilityLabel = [NSString
      stringWithFormat:
          @"%@. %@", passwordProblemsCell.accessibilityLabel,
          l10n_util::GetNSString(
              IDS_IOS_PASSWORD_CHECKUP_SAFE_STATE_ACCESSIBILITY_LABEL)];
}

// Logs favicon percentage metric for the Password Manager.
- (void)logPercentageMetricForFavicons {
  DCHECK(!_faviconMetricLogged);

  int n_monograms = 0;
  int n_images = 0;
  std::vector sections_and_types = {
      std::pair{SectionIdentifierSavedPasswords, ItemTypeSavedPassword},
      std::pair{SectionIdentifierBlocked, ItemTypeBlocked}};
  for (auto [section, type] : sections_and_types) {
    if (![self.tableViewModel hasSectionForSectionIdentifier:section]) {
      continue;
    }

    NSArray<NSIndexPath*>* indexPaths =
        [self.tableViewModel indexPathsForItemType:type
                                 sectionIdentifier:section];
    for (NSIndexPath* indexPath : indexPaths) {
      PasswordFormContentCell* cell =
          [self.tableView cellForRowAtIndexPath:indexPath];
      if (!cell) {
        // Cell not queued for displaying yet.
        continue;
      }

      switch (cell.faviconTypeForMetrics) {
        case FaviconTypeNotLoaded:
          continue;
        case FaviconTypeMonogram:
          n_monograms++;
          break;
        case FaviconTypeImage:
          n_images++;
          break;
      }
    }
  }

  if (n_images + n_monograms > 0) {
    base::UmaHistogramPercentage("IOS.PasswordManager.Favicons.Percentage",
                                 100.0f * n_images / (n_images + n_monograms));
  }
}

- (bool)allowsAddPassword {
  if (!self.prefService) {
    return NO;
  }
  // If the settings are managed by enterprise policy and the password manager
  // is not enabled, there won't be any add functionality.
  const char* prefName = password_manager::prefs::kCredentialsEnableService;
  return !self.prefService->IsManagedPreference(prefName) ||
         self.prefService->GetBoolean(prefName);
}

// Configures the title of this ViewController.
- (void)setUpTitle {
  self.title = l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER);

  self.navigationItem.titleView =
      password_manager::CreatePasswordManagerTitleView(/*title=*/self.title);
}

// Don't focus the searchBar before the view has loaded or if the empty state
// view is displayed. It's possible for the view to load before the model or
// vice versa.
- (void)maybeFocusSearchBar {
  if ([self shouldShowEmptyStateView]) {
    return;
  }

  if (!_hasViewAppeared) {
    return;
  }

  if (_shouldOpenInSearchMode) {
    // Queue search bar focus so the keyboard animation doesn't collide with
    // other animations.
    __weak __typeof(self.searchController.searchBar) weakSearchBar =
        self.searchController.searchBar;
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(^{
          [weakSearchBar becomeFirstResponder];
        }));

    _shouldOpenInSearchMode = NO;
  }
}

// Shows the empty state view when there is no content to display in the
// tableView, otherwise hides the empty state view if one is being displayed.
- (void)showOrHideEmptyView {
  if ([self shouldShowEmptyStateView]) {
    NSString* title =
        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_EMPTY_TITLE);

    NSDictionary* textAttributes =
        [TableViewIllustratedEmptyView defaultTextAttributesForSubtitle];
    NSURL* linkURL = net::NSURLWithGURL(google_util::AppendGoogleLocaleParam(
        GURL(password_manager::kPasswordManagerHelpCenteriOSURL),
        GetApplicationContext()->GetApplicationLocale()));
    NSDictionary* linkAttributes = @{
      NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor],
      NSLinkAttributeName : linkURL,
    };
    NSAttributedString* subtitle = AttributedStringFromStringWithLink(
        l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORDS_MANAGE_ACCOUNT_HEADER),
        textAttributes, linkAttributes);

    [self addEmptyTableViewWithImage:[UIImage imageNamed:@"passwords_empty"]
                               title:title
                  attributedSubtitle:subtitle
                            delegate:self];
    self.navigationItem.searchController = nil;
    self.tableView.alwaysBounceVertical = NO;
  } else {
    [self removeEmptyTableView];
    self.navigationItem.searchController = self.searchController;
    self.tableView.alwaysBounceVertical = YES;
    [self maybeFocusSearchBar];
  }
}

// Private accessor to `_didReceivePasswords` only exposed to unit tests.
- (BOOL)didReceivePasswords {
  return _didReceivePasswords;
}

- (void)settingsButtonCallback {
  [self.presentationDelegate showPasswordSettingsSubmenu];
}

- (void)addButtonCallback {
  [self.handler showAddPasswordSheet];
}

- (UIBarButtonItem*)settingsButtonInToolbar {
  if (!_settingsButtonInToolbar) {
    _settingsButtonInToolbar =
        [self settingsButtonWithAction:@selector(settingsButtonCallback)];
  }

  return _settingsButtonInToolbar;
}

- (UIBarButtonItem*)addButtonInToolbar {
  if (!_addButtonInToolbar) {
    _addButtonInToolbar =
        [self addButtonWithAction:@selector(addButtonCallback)];
  }
  return _addButtonInToolbar;
}

// Helper method determining if the empty state view should be displayed.
- (BOOL)shouldShowEmptyStateView {
  return ![self hasPasswords] && _blockedSites.empty();
}

- (void)deleteItemAtIndexPathsForTesting:(NSArray<NSIndexPath*>*)indexPaths {
  [self deleteItemAtIndexPaths:indexPaths];
}

// Reconfigures the cells of the Password Check section. Adds or removes the
// check button from the table view if needed.
- (void)reconfigurePasswordCheckSectionCellsWithState:
    (PasswordCheckUIState)state {
  if (![self.tableViewModel
          hasSectionForSectionIdentifier:SectionIdentifierPasswordCheck]) {
    return;
  }

  __weak __typeof(self) weakSelf = self;
  [self.tableView
      performBatchUpdates:^{
        if (weakSelf.passwordProblemsItem) {
          [weakSelf
              reconfigureCellsForItems:@[ weakSelf.passwordProblemsItem ]];
          // When in safe state, a custom accessibility label needs to be set
          // for the Password Checkup cell.
          if (state == PasswordCheckStateSafe) {
            [weakSelf setPasswordProblemsItemAccessibilityLabelForSafeState];
          }
        }
        if (weakSelf.checkForProblemsItem) {
          BOOL checkForProblemsItemIsInModel = [weakSelf.tableViewModel
              hasItemForItemType:ItemTypeCheckForProblemsButton
               sectionIdentifier:SectionIdentifierPasswordCheck];
          // Check if the check button should be removed from the table view.
          if (!weakSelf.shouldShowCheckButton &&
              checkForProblemsItemIsInModel) {
            NSIndexPath* checkButtonIndexPath = [weakSelf checkButtonIndexPath];
            [weakSelf
                removeFromModelItemAtIndexPaths:@[ checkButtonIndexPath ]];
            [weakSelf.tableView
                deleteRowsAtIndexPaths:@[ checkButtonIndexPath ]
                      withRowAnimation:UITableViewRowAnimationAutomatic];
          } else if (weakSelf.shouldShowCheckButton) {
            [weakSelf
                reconfigureCellsForItems:@[ weakSelf.checkForProblemsItem ]];
            // Check if the check button should be added to the table view.
            if (!checkForProblemsItemIsInModel) {
              [weakSelf.tableViewModel addItem:weakSelf.checkForProblemsItem
                       toSectionWithIdentifier:SectionIdentifierPasswordCheck];
              [weakSelf.tableView
                  insertRowsAtIndexPaths:@[ [weakSelf checkButtonIndexPath] ]
                        withRowAnimation:UITableViewRowAnimationAutomatic];
            }
          }
        }
        [weakSelf.tableView layoutIfNeeded];
      }
               completion:nil];
}

- (NSIndexPath*)checkButtonIndexPath {
  return
      [self.tableViewModel indexPathForItemType:ItemTypeCheckForProblemsButton
                              sectionIdentifier:SectionIdentifierPasswordCheck];
}

- (void)showDetailedViewPageForItem:(TableViewItem*)item {
  base::RecordAction(
      base::UserMetricsAction("MobilePasswordManagerOpenPasswordDetails"));
  [self.handler
      showDetailedViewForAffiliatedGroup:base::apple::ObjCCastStrict<
                                             AffiliatedGroupTableViewItem>(item)
                                             .affiliatedGroup];
}

// Returns whether or not the widget promo cell should be configured with its
// wide layout. Should return `YES` when the view's width is greater than the
// established threshold.
- (BOOL)shouldWidgetPromoCellHaveWideLayout {
  return self.view.frame.size.width > kWidgetPromoLayoutThreshold;
}

// Updates the layout of the widget promo cell when needed. Disables the
// animation while updating to prevent having a weird animation from
// `beginUpdates` and `endUpdates`. `beginUpdates` and `endUpdates` are needed
// to ensure that the cell will be correctly resized when switching from one
// layout to the other.
- (void)updateWidgetPromoCellLayoutIfNeeded {
  BOOL shouldHaveWideLayout = [self shouldWidgetPromoCellHaveWideLayout];

  if (_shouldShowPasswordManagerWidgetPromo &&
      shouldHaveWideLayout != self.widgetPromoItem.shouldHaveWideLayout) {
    [UIView setAnimationsEnabled:NO];
    [self.tableView beginUpdates];
    self.widgetPromoItem.shouldHaveWideLayout = shouldHaveWideLayout;
    [self reconfigureCellsForItems:@[ self.widgetPromoItem ]];
    [self.tableView endUpdates];
    [UIView setAnimationsEnabled:YES];
  }
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  [super tableView:tableView didSelectRowAtIndexPath:indexPath];

  // Actions should only take effect when not in editing mode.
  if (self.editing) {
    self.deleteButton.enabled = YES;
    return;
  }

  TableViewModel* model = self.tableViewModel;
  ItemType itemType =
      static_cast<ItemType>([model itemTypeForIndexPath:indexPath]);
  switch (itemType) {
    case ItemTypePasswordCheckStatus:
      [self showPasswordCheckupPage];
      break;
    case ItemTypeSavedPassword: {
      DCHECK_EQ(SectionIdentifierSavedPasswords,
                [model sectionIdentifierForSectionIndex:indexPath.section]);
      [self showDetailedViewPageForItem:[model itemAtIndexPath:indexPath]];
      break;
    }
    case ItemTypeBlocked: {
      DCHECK_EQ(SectionIdentifierBlocked,
                [model sectionIdentifierForSectionIndex:indexPath.section]);
      password_manager::CredentialUIEntry credential =
          base::apple::ObjCCastStrict<BlockedSiteTableViewItem>(
              [model itemAtIndexPath:indexPath])
              .credential;
      base::RecordAction(
          base::UserMetricsAction("MobilePasswordManagerOpenPasswordDetails"));
      [self.handler showDetailedViewForCredential:credential];
      break;
    }
    case ItemTypeCheckForProblemsButton:
      if (self.passwordCheckState != PasswordCheckStateRunning) {
        [self.delegate startPasswordCheck];
        password_manager::LogStartPasswordCheckManually();
        self.checkWasTriggeredManually = YES;
      }
      break;
    case ItemTypeAddPasswordButton: {
      [self.handler showAddPasswordSheet];
      break;
    }
    case ItemTypeLastCheckTimestampFooter:
    case ItemTypeLinkHeader:
    case ItemTypeHeader:
    case ItemTypeWidgetPromo:
      NOTREACHED_IN_MIGRATION();
  }
  [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

- (void)tableView:(UITableView*)tableView
    didDeselectRowAtIndexPath:(NSIndexPath*)indexPath {
  [super tableView:tableView didDeselectRowAtIndexPath:indexPath];
  if (!self.editing) {
    return;
  }

  if (self.tableView.indexPathsForSelectedRows.count == 0) {
    self.deleteButton.enabled = NO;
  }
}

- (BOOL)tableView:(UITableView*)tableView
    shouldHighlightRowAtIndexPath:(NSIndexPath*)indexPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  switch (itemType) {
    case ItemTypePasswordCheckStatus:
      return IsPasswordCheckTappable(self.passwordCheckState);
    case ItemTypeCheckForProblemsButton:
      return self.checkForProblemsItem.isEnabled;
    case ItemTypeAddPasswordButton:
      return [self allowsAddPassword];
    case ItemTypeWidgetPromo:
      return NO;
  }
  return YES;
}

- (UIView*)tableView:(UITableView*)tableView
    viewForHeaderInSection:(NSInteger)section {
  UIView* view = [super tableView:tableView viewForHeaderInSection:section];

  if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
      SectionIdentifierManageAccountHeader) {
    // This is the text at the top of the page with a link. Attach as a delegate
    // to ensure clicks on the link are handled.
    TableViewLinkHeaderFooterView* linkView =
        base::apple::ObjCCastStrict<TableViewLinkHeaderFooterView>(view);
    linkView.delegate = self;
  }

  return view;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  // Customize height of emtpy footer for manage account header section to
  // achieve desired vertical spacing to next item.
  if ([self.tableViewModel sectionIdentifierForSectionIndex:section] ==
      SectionIdentifierManageAccountHeader) {
    return kManageAccountHeaderSectionFooterHeight;
  }

  return [super tableView:tableView heightForFooterInSection:section];
}

#pragma mark - UITableViewDataSource

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  // Only password cells are editable.
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  return itemType == ItemTypeSavedPassword || itemType == ItemTypeBlocked;
}

- (void)tableView:(UITableView*)tableView
    commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
     forRowAtIndexPath:(NSIndexPath*)indexPath {
  if (editingStyle != UITableViewCellEditingStyleDelete)
    return;
  [self deleteItemAtIndexPaths:@[ indexPath ]];
}

// TODO(crbug.com/40282917): Stop downcasting cells to configure them.
- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  UITableViewCell* cell = [super tableView:tableView
                     cellForRowAtIndexPath:indexPath];
  switch ([self.tableViewModel itemTypeForIndexPath:indexPath]) {
    case ItemTypeWidgetPromo: {
      InlinePromoCell* widgetPromoCell =
          base::apple::ObjCCastStrict<InlinePromoCell>(cell);
      [widgetPromoCell.closeButton
                 addTarget:self
                    action:@selector(didTapWidgetPromoCloseButton)
          forControlEvents:UIControlEventTouchUpInside];
      [widgetPromoCell.moreInfoButton
                 addTarget:self
                    action:@selector(didTapWidgetPromoMoreInfoButton)
          forControlEvents:UIControlEventTouchUpInside];
      widgetPromoCell.closeButton.accessibilityIdentifier =
          kWidgetPromoCloseButtonID;
      widgetPromoCell.promoImageView.accessibilityIdentifier =
          kWidgetPromoImageID;
      break;
    }
    case ItemTypePasswordCheckStatus: {
      SettingsCheckCell* passwordCheckCell =
          base::apple::ObjCCastStrict<SettingsCheckCell>(cell);
      [passwordCheckCell.infoButton
                 addTarget:self
                    action:@selector(didTapPasswordCheckInfoButton:)
          forControlEvents:UIControlEventTouchUpInside];
      break;
    }
    case ItemTypeSavedPassword:
    case ItemTypeBlocked: {
      // Load the favicon from cache.
      [base::apple::ObjCCastStrict<PasswordFormContentCell>(cell)
          loadFavicon:self.imageDataSource];
      break;
    }
  }
  return cell;
}

#pragma mark Helper methods

// Enables/disables search bar.
- (void)setSearchBarEnabled:(BOOL)enabled {
  if (enabled) {
    self.navigationItem.searchController.searchBar.userInteractionEnabled = YES;
    self.navigationItem.searchController.searchBar.alpha = 1.0;
  } else {
    self.navigationItem.searchController.searchBar.userInteractionEnabled = NO;
    self.navigationItem.searchController.searchBar.alpha =
        kTableViewNavigationAlphaForDisabledSearchBar;
  }
}

#pragma mark - ChromeAccountManagerServiceObserver

- (void)identityListChanged {
  [self reloadData];
}

#pragma mark - UIAdaptivePresentationControllerDelegate

- (void)presentationControllerDidDismiss:
    (UIPresentationController*)presentationController {
  base::RecordAction(
      base::UserMetricsAction("IOSPasswordsSettingsCloseWithSwipe"));
  _accountManagerServiceObserver.reset();
}

#pragma mark - TableViewIllustratedEmptyViewDelegate

- (void)tableViewIllustratedEmptyView:(TableViewIllustratedEmptyView*)view
                   didTapSubtitleLink:(NSURL*)URL {
  [self didTapLinkURL:URL];
}

@end