chromium/ios/chrome/browser/ui/settings/autofill/autofill_credit_card_edit_table_view_controller.mm

// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/settings/autofill/autofill_credit_card_edit_table_view_controller.h"

#import "base/apple/foundation_util.h"
#import "base/feature_list.h"
#import "base/format_macros.h"
#import "base/ios/block_types.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/core/browser/autofill_data_util.h"
#import "components/autofill/core/browser/data_model/credit_card.h"
#import "components/autofill/core/browser/field_types.h"
#import "components/autofill/core/browser/payments/payments_service_url.h"
#import "components/autofill/core/browser/payments_data_manager.h"
#import "components/autofill/core/browser/personal_data_manager.h"
#import "components/autofill/core/common/credit_card_network_identifiers.h"
#import "components/autofill/core/common/credit_card_number_validation.h"
#import "components/autofill/ios/browser/credit_card_util.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/autofill/ui_bundled/autofill_credit_card_ui_type.h"
#import "ios/chrome/browser/autofill/ui_bundled/autofill_credit_card_util.h"
#import "ios/chrome/browser/autofill/ui_bundled/autofill_ui_type_util.h"
#import "ios/chrome/browser/autofill/ui_bundled/cells/autofill_credit_card_edit_item.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/features/features.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/table_view_utils.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/settings/autofill/autofill_settings_constants.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

namespace {
using ::AutofillTypeFromAutofillUITypeForCard;

typedef NS_ENUM(NSInteger, SectionIdentifier) {
  SectionIdentifierFields = kSectionIdentifierEnumZero,
};

typedef NS_ENUM(NSInteger, ItemType) {
  ItemTypeCardholderName = kItemTypeEnumZero,
  ItemTypeCardNumber,
  ItemTypeExpirationMonth,
  ItemTypeExpirationYear,
  ItemTypeNickname,
};

}  // namespace

@interface AutofillCreditCardEditTableViewController () <
    TableViewTextEditItemDelegate>
@end

@implementation AutofillCreditCardEditTableViewController {
  raw_ptr<autofill::PersonalDataManager> _personalDataManager;  // weak
  autofill::CreditCard _creditCard;
}

#pragma mark - Initialization

- (instancetype)initWithCreditCard:(const autofill::CreditCard&)creditCard
               personalDataManager:(autofill::PersonalDataManager*)dataManager {
  self = [super initWithStyle:ChromeTableViewStyle()];
  if (self) {
    DCHECK(dataManager);

    _personalDataManager = dataManager;
    _creditCard = creditCard;

    [self setTitle:l10n_util::GetNSString(IDS_IOS_AUTOFILL_EDIT_CREDIT_CARD)];
  }

  return self;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  self.tableView.allowsSelectionDuringEditing = YES;
  self.tableView.accessibilityIdentifier = kAutofillCreditCardEditTableViewId;
  [self loadModel];
}

#pragma mark - SettingsRootTableViewController

- (void)editButtonPressed {
  // Check if the card should be edited from the Payments web page.
  if ([AutofillCreditCardUtil shouldEditCardFromPaymentsWebPage:&_creditCard]) {
    GURL paymentsURL =
        autofill::payments::GetManageInstrumentUrl(_creditCard.instrument_id());
    OpenNewTabCommand* command =
        [OpenNewTabCommand commandWithURLFromChrome:paymentsURL];
    [self.applicationHandler closeSettingsUIAndOpenURL:command];

    return;
  }

  [super editButtonPressed];

  if (!self.tableView.editing) {
    TableViewModel* model = self.tableViewModel;
    NSInteger itemCount =
        [model numberOfItemsInSection:
                   [model sectionForSectionIdentifier:SectionIdentifierFields]];

    // Reads the values from the fields and updates the local copy of the
    // card accordingly.
    NSInteger section =
        [model sectionForSectionIdentifier:SectionIdentifierFields];
    for (NSInteger itemIndex = 0; itemIndex < itemCount; ++itemIndex) {
      NSIndexPath* path = [NSIndexPath indexPathForItem:itemIndex
                                              inSection:section];
      AutofillCreditCardEditItem* item =
          base::apple::ObjCCastStrict<AutofillCreditCardEditItem>(
              [model itemAtIndexPath:path]);
      if ([self.tableViewModel itemTypeForIndexPath:path] == ItemTypeNickname) {
        NSString* trimmedNickname = [item.textFieldValue
            stringByTrimmingCharactersInSet:
                [NSCharacterSet whitespaceAndNewlineCharacterSet]];
        _creditCard.SetNickname(base::SysNSStringToUTF16(trimmedNickname));
      } else {
        _creditCard.SetInfo(
            autofill::AutofillType(AutofillTypeFromAutofillUITypeForCard(
                item.autofillCreditCardUIType)),
            base::SysNSStringToUTF16(item.textFieldValue),
            GetApplicationContext()->GetApplicationLocale());
      }
    }

    _personalDataManager->payments_data_manager().UpdateCreditCard(_creditCard);
  }

  // Reload the model.
  [self loadModel];
  // Update the cells.
  [self reconfigureCellsForItems:
            [self.tableViewModel
                itemsInSectionWithIdentifier:SectionIdentifierFields]];
}

#pragma mark - LegacyChromeTableViewController

- (void)loadModel {
  [super loadModel];
  TableViewModel<TableViewItem*>* model = self.tableViewModel;

  BOOL isEditing = self.tableView.editing;

  NSArray<AutofillCreditCardEditItem*>* editItems = @[
    [self cardNumberItem:isEditing],
    [self expirationMonthItem:isEditing],
    [self expirationYearItem:isEditing],
    [self cardholderNameItem:isEditing],
    [self nicknameItem:isEditing],
  ];

  [model addSectionWithIdentifier:SectionIdentifierFields];
  for (AutofillCreditCardEditItem* item in editItems) {
    [model addItem:item toSectionWithIdentifier:SectionIdentifierFields];
  }
}

#pragma mark - TableViewTextEditItemDelegate

- (void)tableViewItemDidBeginEditing:
    (TableViewTextEditItem*)tableViewTextEditItem {
  // No op.
}

- (void)tableViewItemDidChange:(TableViewTextEditItem*)tableViewTextEditItem {
  if ([tableViewTextEditItem
          isKindOfClass:[AutofillCreditCardEditItem class]]) {
    self.navigationItem.rightBarButtonItem.enabled = [self isValidCreditCard];

    AutofillCreditCardEditItem* item =
        (AutofillCreditCardEditItem*)tableViewTextEditItem;

    // If the user is typing in the credit card number field, update the card
    // type icon (e.g. "Visa") to reflect the number being typed.
    if (item.autofillCreditCardUIType == AutofillCreditCardUIType::kNumber) {
      const char* network = autofill::GetCardNetwork(
          base::SysNSStringToUTF16(item.textFieldValue));
      item.identifyingIcon = [self cardTypeIconFromNetwork:network];
      [self reconfigureCellsForItems:@[ item ]];
    }

    if (item.type == ItemTypeNickname) {
      NSString* trimmedText = [item.textFieldValue
          stringByTrimmingCharactersInSet:
              [NSCharacterSet whitespaceAndNewlineCharacterSet]];
      BOOL newNicknameIsValid = autofill::CreditCard::IsNicknameValid(
          base::SysNSStringToUTF16(trimmedText));
      self.navigationItem.rightBarButtonItem.enabled = newNicknameIsValid;
      [item setHasValidText:newNicknameIsValid];
      [self reconfigureCellsForItems:@[ item ]];
    }
  }
}

- (void)tableViewItemDidEndEditing:
    (TableViewTextEditItem*)tableViewTextEditItem {
  // If the table view has already been dismissed or the editing stopped
  // ignore call as the item might no longer be in the cells (crbug/1125094).
  if (!self.tableView.isEditing) {
    return;
  }

  if ([tableViewTextEditItem
          isKindOfClass:[AutofillCreditCardEditItem class]]) {
    AutofillCreditCardEditItem* item =
        (AutofillCreditCardEditItem*)tableViewTextEditItem;

    switch (item.type) {
      case ItemTypeCardNumber:
        tableViewTextEditItem.hasValidText = [AutofillCreditCardUtil
            isValidCreditCardNumber:item.textFieldValue
                           appLocal:GetApplicationContext()
                                        ->GetApplicationLocale()];
        break;
      case ItemTypeExpirationMonth:
        tableViewTextEditItem.hasValidText = [AutofillCreditCardUtil
            isValidCreditCardExpirationMonth:item.textFieldValue];
        break;
      case ItemTypeExpirationYear:
        tableViewTextEditItem.hasValidText = [AutofillCreditCardUtil
            isValidCreditCardExpirationYear:item.textFieldValue
                                   appLocal:GetApplicationContext()
                                                ->GetApplicationLocale()];
        break;
      case ItemTypeNickname:
        tableViewTextEditItem.hasValidText =
            [AutofillCreditCardUtil isValidCardNickname:item.textFieldValue];
        break;
      case ItemTypeCardholderName:
      default:
        // For the 'Name on card' textfield.
        tableViewTextEditItem.hasValidText = YES;
        break;
    }

    // Reconfigure to trigger appropiate icon change.
    [self reconfigureCellsForItems:@[ item ]];
  }
}

#pragma mark - UITableViewDataSource

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

  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:indexPath];
  TableViewTextEditCell* editCell =
      base::apple::ObjCCast<TableViewTextEditCell>(cell);
  editCell.textField.delegate = self;
  switch (itemType) {
    case ItemTypeCardholderName:
    case ItemTypeCardNumber:
    case ItemTypeExpirationMonth:
    case ItemTypeExpirationYear:
    case ItemTypeNickname:
      break;
    default:
      break;
  }

  return cell;
}

- (BOOL)tableView:(UITableView*)tableView
    canEditRowAtIndexPath:(NSIndexPath*)indexPath {
  // Items in this table view are not deletable, so should not be seen as
  // editable by the table view.
  return NO;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  if (self.tableView.editing) {
    UITableViewCell* cell = [self.tableView cellForRowAtIndexPath:indexPath];
    TableViewTextEditCell* textFieldCell =
        base::apple::ObjCCastStrict<TableViewTextEditCell>(cell);
    [textFieldCell.textField becomeFirstResponder];
  }
}

#pragma mark - UIAdaptivePresentationControllerDelegate

- (BOOL)presentationControllerShouldDismiss:
    (UIPresentationController*)presentationController {
  return !self.tableView.editing;
}

#pragma mark - AutofillEditTableViewController

- (BOOL)isItemAtIndexPathTextEditCell:(NSIndexPath*)cellPath {
  NSInteger itemType = [self.tableViewModel itemTypeForIndexPath:cellPath];
  switch (itemType) {
    case ItemTypeCardholderName:
    case ItemTypeCardNumber:
    case ItemTypeExpirationMonth:
    case ItemTypeExpirationYear:
    case ItemTypeNickname:
      return YES;
  }
  NOTREACHED_IN_MIGRATION();
  return NO;
}

#pragma mark - Actions

- (void)buttonTapped:(UIButton*)button {
  // TODO(crbug.com/40939195): Remove this method and button entirely; it should
  // no longer be possible to have it visible.

  // Reset the copy of the card data used for display immediately.
  _creditCard.set_record_type(
      autofill::CreditCard::RecordType::kMaskedServerCard);
  _creditCard.SetNumber(_creditCard.LastFourDigits());
  [self reloadData];
}

#pragma mark - Private

- (UIImage*)cardTypeIconFromNetwork:(const char*)network {
  if (network != autofill::kGenericCard) {
    int resourceID =
        autofill::data_util::GetPaymentRequestData(network).icon_resource_id;
    // Return the card issuer network icon.
    return NativeImage(resourceID);
  } else {
    return nil;
  }
}

- (AutofillCreditCardEditItem*)cardholderNameItem:(bool)isEditing {
  AutofillCreditCardEditItem* cardholderNameItem =
      [[AutofillCreditCardEditItem alloc] initWithType:ItemTypeCardholderName];
  cardholderNameItem.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_AUTOFILL_CARDHOLDER);
  cardholderNameItem.textFieldValue = autofill::GetCreditCardName(
      _creditCard, GetApplicationContext()->GetApplicationLocale());
  cardholderNameItem.textFieldEnabled = isEditing;
  cardholderNameItem.autofillCreditCardUIType =
      AutofillCreditCardUIType::kFullName;
  cardholderNameItem.hideIcon = !isEditing;
  return cardholderNameItem;
}

- (AutofillCreditCardEditItem*)cardNumberItem:(bool)isEditing {
  AutofillCreditCardEditItem* cardNumberItem =
      [[AutofillCreditCardEditItem alloc] initWithType:ItemTypeCardNumber];
  cardNumberItem.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_AUTOFILL_CARD_NUMBER);
  // Never show full card number for Wallet cards, even if copied locally.
  cardNumberItem.textFieldValue =
      autofill::IsCreditCardLocal(_creditCard)
          ? base::SysUTF16ToNSString(_creditCard.number())
          : base::SysUTF16ToNSString(_creditCard.NetworkAndLastFourDigits());
  cardNumberItem.textFieldEnabled = isEditing;
  cardNumberItem.autofillCreditCardUIType = AutofillCreditCardUIType::kNumber;
  cardNumberItem.keyboardType = UIKeyboardTypeNumberPad;
  cardNumberItem.hideIcon = !isEditing;
  cardNumberItem.delegate = self;
  // Hide credit card icon when editing.
  if (!isEditing) {
    cardNumberItem.identifyingIcon =
        [self cardTypeIconFromNetwork:_creditCard.network().c_str()];
  }
  return cardNumberItem;
}

- (AutofillCreditCardEditItem*)expirationMonthItem:(bool)isEditing {
  AutofillCreditCardEditItem* expirationMonthItem =
      [[AutofillCreditCardEditItem alloc] initWithType:ItemTypeExpirationMonth];
  expirationMonthItem.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_AUTOFILL_EXP_MONTH);
  expirationMonthItem.textFieldValue =
      [NSString stringWithFormat:@"%02d", _creditCard.expiration_month()];
  expirationMonthItem.textFieldEnabled = isEditing;
  expirationMonthItem.autofillCreditCardUIType =
      AutofillCreditCardUIType::kExpMonth;
  expirationMonthItem.keyboardType = UIKeyboardTypeNumberPad;
  expirationMonthItem.hideIcon = !isEditing;
  expirationMonthItem.delegate = self;
  return expirationMonthItem;
}

- (AutofillCreditCardEditItem*)expirationYearItem:(bool)isEditing {
  // Expiration year.
  AutofillCreditCardEditItem* expirationYearItem =
      [[AutofillCreditCardEditItem alloc] initWithType:ItemTypeExpirationYear];
  expirationYearItem.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_AUTOFILL_EXP_YEAR);
  expirationYearItem.textFieldValue =
      [NSString stringWithFormat:@"%04d", _creditCard.expiration_year()];
  expirationYearItem.textFieldEnabled = isEditing;
  expirationYearItem.autofillCreditCardUIType =
      AutofillCreditCardUIType::kExpYear;
  expirationYearItem.keyboardType = UIKeyboardTypeNumberPad;
  expirationYearItem.returnKeyType = UIReturnKeyDone;
  expirationYearItem.hideIcon = !isEditing;
  expirationYearItem.delegate = self;
  return expirationYearItem;
}

- (AutofillCreditCardEditItem*)nicknameItem:(bool)isEditing {
  AutofillCreditCardEditItem* nicknameItem =
      [[AutofillCreditCardEditItem alloc] initWithType:ItemTypeNickname];
  nicknameItem.fieldNameLabelText =
      l10n_util::GetNSString(IDS_IOS_AUTOFILL_NICKNAME);
  nicknameItem.textFieldValue =
      autofill::GetCreditCardNicknameString(_creditCard);
  nicknameItem.textFieldPlaceholder =
      l10n_util::GetNSString(IDS_IOS_AUTOFILL_DIALOG_PLACEHOLDER_NICKNAME);
  nicknameItem.textFieldEnabled = isEditing;
  nicknameItem.keyboardType = UIKeyboardTypeDefault;
  nicknameItem.hideIcon = !isEditing;
  nicknameItem.delegate = self;
  return nicknameItem;
}

// Returns YES if the data entered in the fields represent a valid credit card.
- (BOOL)isValidCreditCard {
  NSString* cardNumber = [self textfieldValueForItemType:ItemTypeCardNumber];
  NSString* expirationMonth =
      [self textfieldValueForItemType:ItemTypeExpirationMonth];
  NSString* expirationYear =
      [self textfieldValueForItemType:ItemTypeExpirationYear];
  NSString* nickname = [self textfieldValueForItemType:ItemTypeNickname];
  return [AutofillCreditCardUtil
      isValidCreditCard:cardNumber
        expirationMonth:expirationMonth
         expirationYear:expirationYear
           cardNickname:nickname
               appLocal:GetApplicationContext()->GetApplicationLocale()];
}

// Returns the value in the field corresponding to the `itemType`.
- (NSString*)textfieldValueForItemType:(ItemType)itemType {
  NSIndexPath* indexPath =
      [self.tableViewModel indexPathForItemType:itemType
                              sectionIdentifier:SectionIdentifierFields];
  AutofillCreditCardEditItem* item =
      base::apple::ObjCCastStrict<AutofillCreditCardEditItem>(
          [self.tableViewModel itemAtIndexPath:indexPath]);
  return item.textFieldValue;
}

@end