chromium/ios/chrome/browser/ui/settings/password/password_details/add_password_view_controller.mm

// Copyright 2022 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_details/add_password_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/ios/ios_util.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "components/password_manager/core/browser/password_manager_metrics_util.h"
#import "components/password_manager/core/common/password_manager_constants.h"
#import "ios/chrome/browser/keyboard/ui_bundled/UIKeyCommand+Chrome.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.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_multi_line_text_edit_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_multi_line_text_edit_item_delegate.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_edit_item_delegate.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/settings/cells/settings_image_detail_text_item.h"
#import "ios/chrome/browser/ui/settings/password/password_details/add_password_view_controller_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_details/credential_details.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_constants.h"
#import "ios/chrome/browser/ui/settings/password/passwords_table_view_constants.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_protocol.h"
#import "ios/chrome/common/ui/table_view/table_view_cells_constants.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {

using password_manager::constants::kMaxPasswordNoteLength;
using password_manager::metrics_util::PasswordCheckInteraction;

typedef NS_ENUM(NSInteger, SectionIdentifier) {
  SectionIdentifierPassword = kSectionIdentifierEnumZero,
  SectionIdentifierSite,
  SectionIdentifierDuplicate,
  SectionIdentifierFooter,
  SectionIdentifierTLDFooter,
  SectionIdentifierNoteFooter
};

typedef NS_ENUM(NSInteger, ItemType) {
  ItemTypeWebsite = kItemTypeEnumZero,
  ItemTypeUsername,
  ItemTypePassword,
  ItemTypeFooter,
  ItemTypeNote,
  ItemTypeDuplicateCredentialButton,
  ItemTypeDuplicateCredentialMessage
};

// Size of the symbols.
const CGFloat kSymbolSize = 15;
// Minimal amount of characters in password note to display the warning.
const int kMinNoteCharAmountForWarning = 901;

}  // namespace

@interface AddPasswordViewController () <TableViewTextEditItemDelegate,
                                         TableViewMultiLineTextEditItemDelegate>

// Whether the password is shown in plain text form or in masked form.
@property(nonatomic, assign, getter=isPasswordShown) BOOL passwordShown;

// The text item related to the site value.
@property(nonatomic, strong) TableViewTextEditItem* websiteTextItem;

// The text item related to the username value.
@property(nonatomic, strong) TableViewTextEditItem* usernameTextItem;

// The text item related to the password value.
@property(nonatomic, strong) TableViewTextEditItem* passwordTextItem;

// The text item related to the password note value.
@property(nonatomic, strong) TableViewMultiLineTextEditItem* noteTextItem;

// The view used to anchor error alert which is shown for the username. This is
// image icon in the `usernameTextItem` cell.
@property(nonatomic, weak) UIView* usernameErrorAnchorView;

// If YES, denotes that the credential with the same website/username
// combination already exists. Used when creating a new credential.
@property(nonatomic, assign) BOOL isDuplicatedCredential;

// Denotes that the save button in the add credential view can be enabled after
// basic validation of data on all the fields. Does not account for whether the
// duplicate credential exists or not.
@property(nonatomic, assign) BOOL shouldEnableSave;

// Yes, when the message for top-level domain missing is shown.
@property(nonatomic, assign) BOOL isTLDMissingMessageShown;

// Yes, when the footer informing about the max note length is shown.
@property(nonatomic, assign) BOOL isNoteFooterShown;

// Yes, when the note's length is less or equal than
// `password_manager::constants::kMaxPasswordNoteLength`.
@property(nonatomic, assign) BOOL isNoteValid;

// If YES, the password details are shown without requiring any authentication.
@property(nonatomic, assign) BOOL showPasswordWithoutAuth;

// The account where passwords are being saved to, or nil if passwords are only
// being saved locally.
@property(nonatomic, strong) NSString* accountSavingPasswords;

// Stores the user current typed password. (Used for testing).
@property(nonatomic, strong) NSString* passwordForTesting;

@end

@implementation AddPasswordViewController

#pragma mark - ViewController Life Cycle.

- (instancetype)init {
  self = [super initWithStyle:ChromeTableViewStyle()];
  if (self) {
    _isDuplicatedCredential = NO;
    _shouldEnableSave = NO;
    _showPasswordWithoutAuth = NO;
    _isTLDMissingMessageShown = NO;
    _isNoteFooterShown = NO;
    _isNoteValid = YES;
  }
  return self;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];
  self.tableView.accessibilityIdentifier = kPasswordDetailsViewControllerID;
  self.tableView.allowsSelectionDuringEditing = YES;

  self.navigationItem.title = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_SETTINGS_ADD_PASSWORD_MANUALLY_TITLE);

  // Adds 'Cancel' and 'Save' buttons to Navigation bar.
  self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
      initWithTitle:l10n_util::GetNSString(IDS_IOS_NAVIGATION_BAR_CANCEL_BUTTON)
              style:UIBarButtonItemStylePlain
             target:self
             action:@selector(didTapCancelButton:)];
  self.navigationItem.leftBarButtonItem.accessibilityIdentifier =
      kPasswordsAddPasswordCancelButtonID;

  self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
      initWithTitle:l10n_util::GetNSString(
                        IDS_IOS_PASSWORD_SETTINGS_SAVE_BUTTON)
              style:UIBarButtonItemStyleDone
             target:self
             action:@selector(didTapSaveButton:)];
  self.navigationItem.rightBarButtonItem.enabled = NO;
  self.navigationItem.rightBarButtonItem.accessibilityIdentifier =
      kPasswordsAddPasswordSaveButtonID;

  password_manager::metrics_util::
      LogUserInteractionsWhenAddingCredentialFromSettings(
          password_manager::metrics_util::
              AddCredentialFromSettingsUserInteractions::kAddDialogOpened);

  [self loadModel];
}

- (void)viewDidDisappear:(BOOL)animated {
  password_manager::metrics_util::
      LogUserInteractionsWhenAddingCredentialFromSettings(
          password_manager::metrics_util::
              AddCredentialFromSettingsUserInteractions::kAddDialogClosed);
  [super viewDidDisappear:animated];
}

- (void)loadModel {
  [super loadModel];

  TableViewModel* model = self.tableViewModel;

  self.websiteTextItem = [self websiteItem];

  [model addSectionWithIdentifier:SectionIdentifierSite];

  [model addItem:self.websiteTextItem
      toSectionWithIdentifier:SectionIdentifierSite];

  [model addSectionWithIdentifier:SectionIdentifierTLDFooter];

  [model addSectionWithIdentifier:SectionIdentifierPassword];

  self.usernameTextItem = [self usernameItem];
  [model addItem:self.usernameTextItem
      toSectionWithIdentifier:SectionIdentifierPassword];

  self.passwordTextItem = [self passwordItem];
  [model addItem:self.passwordTextItem
      toSectionWithIdentifier:SectionIdentifierPassword];

  self.noteTextItem = [self noteItem];
  [model addItem:self.noteTextItem
      toSectionWithIdentifier:SectionIdentifierPassword];
  [model addSectionWithIdentifier:SectionIdentifierNoteFooter];

  [model addSectionWithIdentifier:SectionIdentifierFooter];
  [model setFooter:[self footerItem]
      forSectionWithIdentifier:SectionIdentifierFooter];
}

- (BOOL)showCancelDuringEditing {
  return YES;
}

#pragma mark - Items

- (TableViewTextEditItem*)websiteItem {
  TableViewTextEditItem* item =
      [[TableViewTextEditItem alloc] initWithType:ItemTypeWebsite];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_SITE);
  item.textFieldEnabled = YES;
  item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
  item.hideIcon = NO;
  item.keyboardType = UIKeyboardTypeURL;
  item.textFieldPlaceholder = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_SETTINGS_WEBSITE_PLACEHOLDER_TEXT);
  item.delegate = self;
  return item;
}

- (TableViewTextEditItem*)usernameItem {
  TableViewTextEditItem* item =
      [[TableViewTextEditItem alloc] initWithType:ItemTypeUsername];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_USERNAME);
  item.textFieldEnabled = YES;
  item.hideIcon = NO;
  item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
  item.delegate = self;
  item.textFieldPlaceholder = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_SETTINGS_USERNAME_PLACEHOLDER_TEXT);
  return item;
}

- (TableViewTextEditItem*)passwordItem {
  TableViewTextEditItem* item =
      [[TableViewTextEditItem alloc] initWithType:ItemTypePassword];
  item.textFieldBackgroundColor = [UIColor clearColor];
  item.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_PASSWORD);
  item.textFieldSecureTextEntry = ![self isPasswordShown];
  item.textFieldEnabled = YES;
  item.hideIcon = NO;
  item.autoCapitalizationType = UITextAutocapitalizationTypeNone;
  item.keyboardType = UIKeyboardTypeURL;
  item.returnKeyType = UIReturnKeyDone;
  item.delegate = self;
  item.textFieldPlaceholder = l10n_util::GetNSString(
      IDS_IOS_PASSWORD_SETTINGS_PASSWORD_PLACEHOLDER_TEXT);

  // During editing password is exposed so eye icon shouldn't be shown.
  if (!self.tableView.editing) {
    UIImage* image =
        [self isPasswordShown]
            ? DefaultSymbolWithPointSize(kHideActionSymbol, kSymbolSize)
            : DefaultSymbolWithPointSize(kShowActionSymbol, kSymbolSize);
    item.identifyingIcon = image;
    item.identifyingIconEnabled = YES;
    item.identifyingIconAccessibilityLabel = l10n_util::GetNSString(
        [self isPasswordShown] ? IDS_IOS_SETTINGS_PASSWORD_HIDE_BUTTON
                               : IDS_IOS_SETTINGS_PASSWORD_SHOW_BUTTON);
  }
  return item;
}

- (TableViewMultiLineTextEditItem*)noteItem {
  TableViewMultiLineTextEditItem* item =
      [[TableViewMultiLineTextEditItem alloc] initWithType:ItemTypeNote];
  item.label = l10n_util::GetNSString(IDS_IOS_SHOW_PASSWORD_VIEW_NOTE);
  item.editingEnabled = YES;
  item.delegate = self;
  return item;
}

- (TableViewTextItem*)duplicatePasswordViewButtonItem {
  TableViewTextItem* item = [[TableViewTextItem alloc]
      initWithType:ItemTypeDuplicateCredentialButton];
  item.text =
      l10n_util::GetNSString(IDS_IOS_PASSWORD_SETTINGS_VIEW_PASSWORD_BUTTON);
  item.textColor = [UIColor colorNamed:kBlueColor];
  item.accessibilityTraits = UIAccessibilityTraitButton;
  return item;
}

- (SettingsImageDetailTextItem*)duplicatePasswordMessageItem {
  SettingsImageDetailTextItem* item = [[SettingsImageDetailTextItem alloc]
      initWithType:ItemTypeDuplicateCredentialMessage];
  if (self.usernameTextItem &&
      [self.usernameTextItem.textFieldValue length] > 0) {
    item.detailText = l10n_util::GetNSStringF(
        IDS_IOS_SETTINGS_PASSWORDS_DUPLICATE_SECTION_ALERT_DESCRIPTION,
        base::SysNSStringToUTF16(self.usernameTextItem.textFieldValue),
        base::SysNSStringToUTF16(self.websiteTextItem.textFieldValue));
  } else {
    item.detailText = l10n_util::GetNSStringF(
        IDS_IOS_SETTINGS_PASSWORDS_DUPLICATE_SECTION_ALERT_DESCRIPTION_WITHOUT_USERNAME,
        base::SysNSStringToUTF16(self.websiteTextItem.textFieldValue));
  }
  item.image = DefaultSymbolWithPointSize(kErrorCircleFillSymbol, kSymbolSize);
  item.imageViewTintColor = [UIColor colorNamed:kRedColor];
  return item;
}

- (TableViewLinkHeaderFooterItem*)footerItem {
  TableViewLinkHeaderFooterItem* item =
      [[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeFooter];
  item.text =
      [NSString stringWithFormat:@"%@\n\n%@",
                                 l10n_util::GetNSString(
                                     IDS_IOS_SETTINGS_ADD_PASSWORD_DESCRIPTION),
                                 [self footerText]];
  return item;
}

- (TableViewLinkHeaderFooterItem*)TLDMessageFooterItem {
  TableViewLinkHeaderFooterItem* item =
      [[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeFooter];
  item.text = l10n_util::GetNSStringF(
      IDS_IOS_SETTINGS_PASSWORDS_MISSING_TLD_DESCRIPTION,
      base::SysNSStringToUTF16([self.websiteTextItem.textFieldValue
          stringByAppendingString:@".com"]));
  return item;
}

- (TableViewLinkHeaderFooterItem*)tooLongNoteMessageFooterItem {
  TableViewLinkHeaderFooterItem* item =
      [[TableViewLinkHeaderFooterItem alloc] initWithType:ItemTypeFooter];
  item.text = l10n_util::GetNSStringF(
      IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
      base::NumberToString16(
          password_manager::constants::kMaxPasswordNoteLength));
  return item;
}

- (NSString*)footerText {
  if (self.accountSavingPasswords) {
    return l10n_util::GetNSStringF(
        IDS_IOS_SETTINGS_ADD_PASSWORD_FOOTER_BRANDED,
        base::SysNSStringToUTF16(self.accountSavingPasswords));
  }

  return l10n_util::GetNSString(IDS_IOS_SAVE_PASSWORD_FOOTER_NOT_SYNCING);
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  TableViewModel* model = self.tableViewModel;
  NSInteger itemType = [model itemTypeForIndexPath:indexPath];
  if (itemType == ItemTypeNote) {
    UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
    TableViewMultiLineTextEditCell* textFieldCell =
        base::apple::ObjCCastStrict<TableViewMultiLineTextEditCell>(cell);
    [textFieldCell.textView becomeFirstResponder];
    return;
  }

  if (itemType != ItemTypeDuplicateCredentialButton) {
    return;
  }

  password_manager::metrics_util::
      LogUserInteractionsWhenAddingCredentialFromSettings(
          password_manager::metrics_util::
              AddCredentialFromSettingsUserInteractions::
                  kDuplicateCredentialViewed);

    NSString* usernameTextValue = _usernameTextItem.textFieldValue;
    [_delegate showExistingCredential:usernameTextValue];
}

- (UITableViewCellEditingStyle)tableView:(UITableView*)tableView
           editingStyleForRowAtIndexPath:(NSIndexPath*)indexPath {
  return UITableViewCellEditingStyleNone;
}

- (BOOL)tableView:(UITableView*)tableview
    shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath*)indexPath {
  return NO;
}

- (BOOL)tableView:(UITableView*)tableView
    shouldHighlightRowAtIndexPath:(NSIndexPath*)indexPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  return itemType == ItemTypeDuplicateCredentialButton ||
         itemType == ItemTypeNote;
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForHeaderInSection:(NSInteger)section {
  NSInteger sectionIdentifier =
      [self.tableViewModel sectionIdentifierForSectionIndex:section];

  if (sectionIdentifier == SectionIdentifierFooter ||
      sectionIdentifier == SectionIdentifierTLDFooter) {
    return 0;
  }
  return [super tableView:tableView heightForHeaderInSection:section];
}

- (CGFloat)tableView:(UITableView*)tableView
    heightForFooterInSection:(NSInteger)section {
  NSInteger sectionIdentifier =
      [self.tableViewModel sectionIdentifierForSectionIndex:section];
  if (sectionIdentifier == SectionIdentifierSite) {
    return 0;
  }
  if ((sectionIdentifier == SectionIdentifierPassword &&
       !self.isDuplicatedCredential) ||
      sectionIdentifier == SectionIdentifierDuplicate) {
    return 0;
  }
  return [super tableView:tableView heightForFooterInSection:section];
}

#pragma mark - UITableViewDataSource

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  UITableViewCell* cell = [super tableView:tableView
                     cellForRowAtIndexPath:indexPath];

  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  cell.tag = itemType;
  cell.selectionStyle = UITableViewCellSelectionStyleDefault;

  switch (itemType) {
    case ItemTypeUsername: {
      TableViewTextEditCell* textFieldCell =
          base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
      textFieldCell.textField.delegate = self;
      break;
    }
    case ItemTypePassword: {
      TableViewTextEditCell* textFieldCell =
          base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
      textFieldCell.textField.delegate = self;
      [textFieldCell.identifyingIconButton
                 addTarget:self
                    action:@selector(didTapShowHideButton:)
          forControlEvents:UIControlEventTouchUpInside];
      break;
    }
    case ItemTypeWebsite: {
      TableViewTextEditCell* textFieldCell =
          base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
      textFieldCell.textField.delegate = self;
      break;
    }
    case ItemTypeDuplicateCredentialButton: {
      cell.selectionStyle = UITableViewCellSelectionStyleNone;
      break;
    }
    case ItemTypeNote: {
      cell.selectionStyle = UITableViewCellSelectionStyleNone;
      break;
    }
    case ItemTypeDuplicateCredentialMessage:
    case ItemTypeFooter:
      break;
  }
  return cell;
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  switch (itemType) {
    case ItemTypeWebsite:
    case ItemTypeFooter:
    case ItemTypeDuplicateCredentialMessage:
    case ItemTypeDuplicateCredentialButton:
      return NO;
    case ItemTypeUsername:
    case ItemTypePassword:
    case ItemTypeNote:
      return YES;
  }
  return NO;
}

#pragma mark - AddPasswordDetailsConsumer

- (void)setAccountSavingPasswords:(NSString*)accountSavingPasswords {
  _accountSavingPasswords = accountSavingPasswords;
}

- (void)onDuplicateCheckCompletion:(BOOL)duplicateFound {
  if (duplicateFound == self.isDuplicatedCredential) {
    return;
  }

  self.isDuplicatedCredential = duplicateFound;
  [self toggleNavigationBarRightButtonItem];
  TableViewModel* model = self.tableViewModel;
  if (duplicateFound) {
    password_manager::metrics_util::
        LogUserInteractionsWhenAddingCredentialFromSettings(
            password_manager::metrics_util::
                AddCredentialFromSettingsUserInteractions::
                    kDuplicatedCredentialEntered);
    [self
        performBatchTableViewUpdates:^{
          NSUInteger passwordSectionIndex = [self.tableViewModel
              sectionForSectionIdentifier:SectionIdentifierPassword];
          [model insertSectionWithIdentifier:SectionIdentifierDuplicate
                                     atIndex:passwordSectionIndex + 1];
          [self.tableView
                insertSections:[NSIndexSet
                                   indexSetWithIndex:passwordSectionIndex + 1]
              withRowAnimation:UITableViewRowAnimationTop];
          [model addItem:[self duplicatePasswordMessageItem]
              toSectionWithIdentifier:SectionIdentifierDuplicate];
          [model addItem:[self duplicatePasswordViewButtonItem]
              toSectionWithIdentifier:SectionIdentifierDuplicate];
          if (self.usernameTextItem &&
              [self.usernameTextItem.textFieldValue length] > 0) {
            self.usernameTextItem.hasValidText = NO;
            [self reconfigureCellsForItems:@[ self.usernameTextItem ]];
          } else {
            self.websiteTextItem.hasValidText = NO;
            [self reconfigureCellsForItems:@[ self.websiteTextItem ]];
          }
        }
                          completion:nil];
  } else {
    [self
        performBatchTableViewUpdates:^{
          [self removeSectionWithIdentifier:SectionIdentifierDuplicate
                           withRowAnimation:UITableViewRowAnimationTop];
          self.usernameTextItem.hasValidText = YES;
          self.websiteTextItem.hasValidText = YES;
          [self reconfigureCellsForItems:@[
            self.websiteTextItem, self.usernameTextItem
          ]];
        }
                          completion:nil];
  }
}

#pragma mark - TableViewTextEditItemDelegate

- (void)tableViewItemDidBeginEditing:(TableViewTextEditItem*)tableViewItem {
  [self reconfigureCellsForItems:@[
    self.websiteTextItem, self.usernameTextItem, self.passwordTextItem
  ]];
}

- (void)tableViewItemDidChange:(TableViewTextEditItem*)tableViewItem {
  if (tableViewItem == self.websiteTextItem) {
    [self.delegate setWebsiteURL:self.websiteTextItem.textFieldValue];
    if (self.isTLDMissingMessageShown) {
      self.isTLDMissingMessageShown = NO;
      [self
          performBatchTableViewUpdates:^{
            [self.tableViewModel setFooter:nil
                  forSectionWithIdentifier:SectionIdentifierTLDFooter];
            NSUInteger index = [self.tableViewModel
                sectionForSectionIdentifier:SectionIdentifierTLDFooter];
            [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:index]
                          withRowAnimation:UITableViewRowAnimationNone];
          }
                            completion:nil];
    }
  }

  BOOL siteValid = [self checkIfValidSite];
  BOOL passwordValid = [self checkIfValidPassword];

  self.shouldEnableSave = (siteValid && passwordValid && self.isNoteValid);
  [self toggleNavigationBarRightButtonItem];

  [self.delegate checkForDuplicates:self.usernameTextItem.textFieldValue];
}

- (void)tableViewItemDidEndEditing:(TableViewTextEditItem*)tableViewItem {
  if (tableViewItem == self.websiteTextItem) {
    if (!self.isDuplicatedCredential) {
      self.websiteTextItem.hasValidText = [self checkIfValidSite];
    }
    if ([self.websiteTextItem.textFieldValue length] > 0 &&
        [self.delegate isTLDMissing]) {
      [self showTLDMissingSection];
      self.websiteTextItem.hasValidText = NO;
    }
    [self reconfigureCellsForItems:@[ self.websiteTextItem ]];
  } else if (tableViewItem == self.usernameTextItem) {
    [self reconfigureCellsForItems:@[ self.usernameTextItem ]];
  } else if (tableViewItem == self.passwordTextItem) {
    self.passwordTextItem.hasValidText = [self checkIfValidPassword];
    [self reconfigureCellsForItems:@[ self.passwordTextItem ]];
  }
}

#pragma mark - TableViewMultiLineTextEditItemDelegate

- (void)textViewItemDidChange:(TableViewMultiLineTextEditItem*)tableViewItem {
  DCHECK(tableViewItem == self.noteTextItem);

  // Update save button state based on the note's length and validity of other
  // input fields.
  BOOL noteValid = tableViewItem.text.length <= kMaxPasswordNoteLength;
  if (self.isNoteValid != noteValid) {
    self.isNoteValid = noteValid;
    tableViewItem.validText = noteValid;

    self.shouldEnableSave =
        noteValid && [self checkIfValidSite] && [self checkIfValidPassword];
    [self toggleNavigationBarRightButtonItem];
  }

  // Notify that the note character limit has been reached via VoiceOver.
  if (!noteValid) {
    NSString* tooLongNoteMessage = l10n_util::GetNSStringF(
        IDS_IOS_SETTINGS_PASSWORDS_TOO_LONG_NOTE_DESCRIPTION,
        base::NumberToString16(kMaxPasswordNoteLength));
    UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
                                    tooLongNoteMessage);
  }

  // Update note footer based on the note's length.
  BOOL shouldDisplayNoteFooter =
      tableViewItem.text.length >= kMinNoteCharAmountForWarning;
  if (self.isNoteFooterShown != shouldDisplayNoteFooter) {
    self.isNoteFooterShown = shouldDisplayNoteFooter;
    [self
        performBatchTableViewUpdates:^{
          [self.tableViewModel
                             setFooter:shouldDisplayNoteFooter
                                           ? [self tooLongNoteMessageFooterItem]
                                           : nil
              forSectionWithIdentifier:SectionIdentifierNoteFooter];
          NSUInteger index = [self.tableViewModel
              sectionForSectionIdentifier:SectionIdentifierNoteFooter];
          [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:index]
                        withRowAnimation:UITableViewRowAnimationNone];
        }
                          completion:nil];
  }

  [self reconfigureCellsForItems:@[ tableViewItem ]];

  // Refresh the cells' height.
  [self.tableView beginUpdates];
  [self.tableView endUpdates];
}

#pragma mark - Actions

// Dimisses this view controller when Cancel button is tapped.
- (void)didTapCancelButton:(id)sender {
  [self.delegate didCancelAddPasswordDetails];
}

// Handles Save button tap on adding new credentials.
- (void)didTapSaveButton:(id)sender {
  if ([self.websiteTextItem.textFieldValue length] > 0 &&
      [self.delegate isTLDMissing]) {
    [self showTLDMissingSection];
    return;
  }
  password_manager::metrics_util::
      LogUserInteractionsWhenAddingCredentialFromSettings(
          password_manager::metrics_util::
              AddCredentialFromSettingsUserInteractions::kCredentialAdded);
  base::RecordAction(
      base::UserMetricsAction("MobilePasswordManagerAddPassword"));
  if (self.noteTextItem.text.length != 0) {
    password_manager::metrics_util::LogPasswordNoteActionInSettings(
        password_manager::metrics_util::PasswordNoteAction::
            kNoteAddedInAddDialog);
  }
  [self.delegate addPasswordViewController:self
                     didAddPasswordDetails:self.usernameTextItem.textFieldValue
                                  password:self.passwordTextItem.textFieldValue
                                      note:self.noteTextItem.text];
}

#pragma mark - SettingsRootTableViewController

- (BOOL)shouldHideToolbar {
  return YES;
}

#pragma mark - AutofillEditTableViewController

- (BOOL)isItemAtIndexPathTextEditCell:(NSIndexPath*)cellPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:cellPath];
  switch (static_cast<ItemType>(itemType)) {
    case ItemTypeUsername:
    case ItemTypePassword:
    case ItemTypeWebsite:
      return YES;
    case ItemTypeDuplicateCredentialMessage:
    case ItemTypeDuplicateCredentialButton:
    case ItemTypeFooter:
    case ItemTypeNote:
      return NO;
  };
}

#pragma mark - Private

- (BOOL)checkIfValidSite {
  BOOL siteEmpty = [self.websiteTextItem.textFieldValue length] == 0;
  if (!siteEmpty && !self.isTLDMissingMessageShown &&
      !self.isDuplicatedCredential) {
    self.websiteTextItem.hasValidText = YES;
    [self reconfigureCellsForItems:@[ self.websiteTextItem ]];
  }
  return !siteEmpty;
}

// Checks if the password is valid and updates item accordingly.
- (BOOL)checkIfValidPassword {
  BOOL passwordEmpty = [self.passwordTextItem.textFieldValue length] == 0;
  if (!passwordEmpty) {
    self.passwordTextItem.hasValidText = YES;
    [self reconfigureCellsForItems:@[ self.passwordTextItem ]];
  }

  return !passwordEmpty;
}

// Removes the given section if it exists.
- (void)removeSectionWithIdentifier:(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];
  }
}

// Enables/Disables the right bar button item in the navigation bar.
- (void)toggleNavigationBarRightButtonItem {
  self.navigationItem.rightBarButtonItem.enabled =
      !self.isDuplicatedCredential && self.shouldEnableSave &&
      [self.delegate isURLValid] && !self.isTLDMissingMessageShown;
}

// Shows the section with the error message for top-level domain missing.
- (void)showTLDMissingSection {
  if (self.isTLDMissingMessageShown) {
    return;
  }

  self.navigationItem.rightBarButtonItem.enabled = NO;
  self.isTLDMissingMessageShown = YES;
  [self
      performBatchTableViewUpdates:^{
        [self.tableViewModel setFooter:[self TLDMessageFooterItem]
              forSectionWithIdentifier:SectionIdentifierTLDFooter];
        NSUInteger index = [self.tableViewModel
            sectionForSectionIdentifier:SectionIdentifierTLDFooter];
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:index]
                      withRowAnimation:UITableViewRowAnimationNone];
      }
                        completion:nil];
}

#pragma mark - Actions

// Called when the user tapped on the show/hide button near password.
- (void)didTapShowHideButton:(UIButton*)buttonView {
  [self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow
                                animated:NO];
  if (self.isPasswordShown) {
    self.passwordShown = NO;
    self.passwordTextItem.textFieldSecureTextEntry = YES;
    // Only change the textFieldValue for tests.
    if (self.passwordForTesting) {
      self.passwordTextItem.textFieldValue = kMaskedPassword;
    }
    self.passwordTextItem.identifyingIcon =
        DefaultSymbolWithPointSize(kShowActionSymbol, kSymbolSize);
    self.passwordTextItem.identifyingIconAccessibilityLabel =
        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_SHOW_BUTTON);
    [self reconfigureCellsForItems:@[ self.passwordTextItem ]];
  } else {
    self.passwordTextItem.textFieldSecureTextEntry = NO;
    self.passwordShown = YES;
    // Only change the textFieldValue for tests.
    if (self.passwordForTesting) {
      self.passwordTextItem.textFieldValue = self.passwordForTesting;
    }
    self.passwordTextItem.identifyingIcon =
        DefaultSymbolWithPointSize(kHideActionSymbol, kSymbolSize);
    self.passwordTextItem.identifyingIconAccessibilityLabel =
        l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_HIDE_BUTTON);
    [self reconfigureCellsForItems:@[ self.passwordTextItem ]];
  }
}

// Called when the user tap error info icon in the username input.
- (void)didTapUsernameErrorInfo:(UIButton*)buttonView {
  NSString* text = l10n_util::GetNSString(IDS_IOS_USERNAME_ALREADY_USED);

  NSAttributedString* attributedText = [[NSAttributedString alloc]
      initWithString:text
          attributes:@{
            NSForegroundColorAttributeName :
                [UIColor colorNamed:kTextSecondaryColor],
            NSFontAttributeName :
                [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]
          }];

  PopoverLabelViewController* errorInfoPopover =
      [[PopoverLabelViewController alloc]
          initWithPrimaryAttributedString:attributedText
                secondaryAttributedString:nil];

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

#pragma mark - ForTesting

- (void)setPassword:(NSString*)password {
  NSIndexPath* indexPath =
      [self.tableViewModel indexPathForItem:self.passwordTextItem];
  TableViewTextEditItem* item = static_cast<TableViewTextEditItem*>(
      [self.tableViewModel itemAtIndexPath:indexPath]);
  self.passwordForTesting = password;
  item.textFieldValue = [self isPasswordShown] || self.tableView.editing
                            ? self.passwordForTesting
                            : kMaskedPassword;
}

#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*)keyCommands {
  return @[ UIKeyCommand.cr_close ];
}

- (void)keyCommand_close {
  base::RecordAction(base::UserMetricsAction("MobileKeyCommandClose"));
  [self didTapCancelButton:nil];
}

@end