chromium/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_view_controller.mm

// Copyright 2023 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/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_view_controller.h"

#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "build/branding_buildflags.h"
#import "components/autofill/core/browser/data_model/credit_card.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "components/grit/components_scaled_resources.h"
#import "components/url_formatter/elide_url.h"
#import "ios/chrome/browser/autofill/model/credit_card/credit_card_data.h"
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_delegate.h"
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/payments_suggestion_bottom_sheet_handler.h"
#import "ios/chrome/browser/shared/ui/bottom_sheet/table_view_bottom_sheet_view_controller+subclassing.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/util/uikit_ui_util.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"

namespace {

// Credit Card icon corner radius.
CGFloat const kCreditCardIconCornerRadius = 5;

// Default spacing used for the views in the bottom sheet.
CGFloat const kSpacing = 10;

// Spacing use for the spacing before the logo title in the bottom sheet.
CGFloat const kSpacingBeforeImage = 16;

// Spacing use for the spacing after the logo title in the bottom sheet.
CGFloat const kSpacingAfterImage = 4;

// Height of the logo used as the title of the bottom sheet.
CGFloat const kTitleLogoHeight = 32;

}  // namespace

@interface PaymentsSuggestionBottomSheetViewController () <
    ConfirmationAlertActionHandler,
    UITableViewDataSource> {
  // List of credit cards and icon for the bottom sheet.
  NSArray<CreditCardData*>* _creditCardData;

  // URL of the current page the bottom sheet is being displayed on.
  GURL _URL;
}

// The payments controller handler used to open the payments options.
@property(nonatomic, weak) id<PaymentsSuggestionBottomSheetHandler> handler;

// YES if the GPay logo should be shown to the user.
@property(nonatomic, assign) BOOL showGooglePayLogo;

// Whether the bottom sheet will be disabled on exit. Default is YES.
@property(nonatomic, assign) BOOL disableBottomSheetOnExit;

@end

@implementation PaymentsSuggestionBottomSheetViewController

- (instancetype)initWithHandler:
                    (id<PaymentsSuggestionBottomSheetHandler>)handler
                            URL:(const GURL&)URL {
  self = [super init];
  if (self) {
    self.handler = handler;
    _URL = URL;
    self.disableBottomSheetOnExit = YES;
  }
  return self;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  self.image = [self titleImage];
  self.imageViewAccessibilityLabel = [NSString
      stringWithFormat:@"%@. %@",
                       l10n_util::GetNSString(
                           self.showGooglePayLogo
                               ? IDS_IOS_AUTOFILL_WALLET_SERVER_NAME
                               : IDS_IOS_PRODUCT_NAME),
                       l10n_util::GetNSString(
                           IDS_IOS_PAYMENT_BOTTOM_SHEET_SELECT_PAYMENT_METHOD)];
  self.customSpacingBeforeImageIfNoNavigationBar = kSpacingBeforeImage;
  self.customSpacingAfterImage = kSpacingAfterImage;
  self.subtitleTextStyle = UIFontTextStyleFootnote;
  std::u16string formattedURL =
      url_formatter::FormatUrlForDisplayOmitSchemePathAndTrivialSubdomains(
          _URL);
  self.subtitleString = l10n_util::GetNSStringF(
      IDS_IOS_PAYMENT_BOTTOM_SHEET_SUBTITLE, formattedURL);
  self.customSpacing = kSpacing;

  // Set the properties read by the super when constructing the
  // views in `-[ConfirmationAlertViewController viewDidLoad]`.
  self.actionHandler = self;

  self.primaryActionString =
      l10n_util::GetNSString(IDS_IOS_PAYMENT_BOTTOM_SHEET_CONTINUE);
  self.secondaryActionString =
      l10n_util::GetNSString(IDS_IOS_PAYMENT_BOTTOM_SHEET_USE_KEYBOARD);
  self.secondaryActionImage =
      DefaultSymbolWithPointSize(kKeyboardSymbol, kSymbolActionPointSize);

  [super viewDidLoad];

  [self adjustTransactionsPrimaryActionButtonHorizontalConstraints];
}

- (void)viewDidAppear:(BOOL)animated {
  [super viewDidAppear:animated];
  UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification,
                                  self.imageViewAccessibilityLabel);
}

- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
  [super traitCollectionDidChange:previousTraitCollection];

  if (self.traitCollection.userInterfaceStyle !=
      previousTraitCollection.userInterfaceStyle) {
    // Make sure the GPay logo matches the new trait collection.
    self.image = [self titleImage];
  }
}

- (void)viewDidDisappear:(BOOL)animated {
  [super viewDidDisappear:animated];
  if (self.disableBottomSheetOnExit) {
    [self.delegate disableBottomSheetAndRefocus:YES];
  }
  [self.handler viewDidDisappear];
}

#pragma mark - PaymentsSuggestionBottomSheetConsumer

- (void)setCreditCardData:(NSArray<CreditCardData*>*)creditCardData
        showGooglePayLogo:(BOOL)showGooglePayLogo {
  BOOL requiresUpdate = (_creditCardData != nil);
  _creditCardData = creditCardData;
  self.showGooglePayLogo = showGooglePayLogo;
  if (requiresUpdate) {
    [self reloadTableViewData];
  }
}

- (void)dismiss {
  [self dismissViewControllerAnimated:NO completion:NULL];
}

#pragma mark - UITableViewDelegate

// Long press open context menu.
- (UIContextMenuConfiguration*)tableView:(UITableView*)tableView
    contextMenuConfigurationForRowAtIndexPath:(NSIndexPath*)indexPath
                                        point:(CGPoint)point {
  __weak __typeof(self) weakSelf = self;
  UIContextMenuActionProvider actionProvider = ^(
      NSArray<UIMenuElement*>* suggestedActions) {
    NSMutableArray<UIMenu*>* menuElements =
        [[NSMutableArray alloc] initWithArray:suggestedActions];

    PaymentsSuggestionBottomSheetViewController* strongSelf = weakSelf;
    if (strongSelf) {
      [menuElements
          addObject:[UIMenu menuWithTitle:@""
                                    image:nil
                               identifier:nil
                                  options:UIMenuOptionsDisplayInline
                                 children:@[
                                   [strongSelf openPaymentMethodsAction]
                                 ]]];
      [menuElements
          addObject:[UIMenu menuWithTitle:@""
                                    image:nil
                               identifier:nil
                                  options:UIMenuOptionsDisplayInline
                                 children:@[
                                   [strongSelf
                                       openPaymentDetailsForIndexPath:indexPath]
                                 ]]];
    }
    return [UIMenu menuWithTitle:@"" children:menuElements];
  };

  return
      [UIContextMenuConfiguration configurationWithIdentifier:nil
                                              previewProvider:nil
                                               actionProvider:actionProvider];
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView*)tableView
    numberOfRowsInSection:(NSInteger)section {
  return [self rowCount];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView {
  return 1;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  TableViewDetailIconCell* cell =
      [tableView dequeueReusableCellWithIdentifier:@"cell"];
  return [self layoutCell:cell
        forTableViewWidth:tableView.frame.size.width
              atIndexPath:indexPath];
}

#pragma mark - ConfirmationAlertActionHandler

- (void)confirmationAlertPrimaryAction {
  self.disableBottomSheetOnExit = NO;

  base::RecordAction(
      base::UserMetricsAction("BottomSheet_CreditCard_SuggestionAccepted"));
  NSInteger index = [self selectedRow];
  base::UmaHistogramSparse(
      "Autofill.UserAcceptedSuggestionAtIndex.CreditCard.BottomSheet", index);
  [self.handler primaryButtonTappedForCard:_creditCardData[index]
                                   atIndex:index];

  if ([self rowCount] > 1) {
    base::UmaHistogramCounts100("Autofill.TouchToFill.CreditCard.SelectedIndex",
                                (int)index);
  }
}

- (void)confirmationAlertSecondaryAction {
  [self.handler secondaryButtonTapped];
}

#pragma mark - TableViewBottomSheetViewController

- (UITableView*)createTableView {
  UITableView* tableView = [super createTableView];

  tableView.dataSource = self;
  [tableView registerClass:TableViewDetailIconCell.class
      forCellReuseIdentifier:@"cell"];

  return tableView;
}

- (NSUInteger)rowCount {
  return _creditCardData.count;
}

- (CGFloat)computeTableViewCellHeightAtIndex:(NSUInteger)index {
  TableViewDetailIconCell* cell = [[TableViewDetailIconCell alloc] init];
  // Setup UI same as real cell.
  CGFloat tableWidth = [self tableViewWidth];
  cell = [self layoutCell:cell
        forTableViewWidth:tableWidth
              atIndexPath:[NSIndexPath indexPathForRow:index inSection:0]];
  return [cell systemLayoutSizeFittingSize:CGSizeMake(tableWidth, 1)].height;
}

#pragma mark - Private

// Returns the title logo image that is resized to the correct size for the
// bottom sheet. It should return the Google Pay badge
// image corresponding to the current UIUserInterfaceStyle (light/dark mode) if
// `showGooglePayLogo` value is YES otherwise the Chrome logo is shown.
- (UIImage*)titleImage {
  UIImage* image;
#if BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)
  image = MakeSymbolMulticolor(CustomSymbolWithPointSize(
      self.showGooglePayLogo ? kGooglePaySymbol : kMulticolorChromeballSymbol,
      kTitleLogoHeight));
#else
  image = DefaultSymbolTemplateWithPointSize(kDefaultBrowserSymbol,
                                             kTitleLogoHeight);
#endif  // BUILDFLAG(IOS_USE_BRANDED_SYMBOLS)

  return image;
}

// Returns the string to display at a given row in the table view.
- (NSString*)suggestionAtRow:(NSInteger)row {
  return [_creditCardData[row] cardNameAndLastFourDigits];
}

// Returns the display description at a given row in the table view.
- (NSString*)descriptionAtRow:(NSInteger)row {
  return [_creditCardData[row] cardDetails];
}

// Returns the credit card icon at a given row in the table view.
- (UIImage*)iconAtRow:(NSInteger)row {
  return [_creditCardData[row] icon];
}

// Returns an accessible card name at a given row in the table view.
- (NSString*)accessibleCardNameAtRow:(NSInteger)row {
  return l10n_util::GetNSStringF(
      IDS_IOS_AUTOFILL_ACCNAME_SUGGESTION,
      base::SysNSStringToUTF16([_creditCardData[row] accessibleCardName]), u"");
}

// Returns the accessibility value for the card at a given row in the table
// view.
- (NSString*)accessibilityValueForCardAtRow:(NSInteger)row {
  return l10n_util::GetNSStringF(IDS_IOS_AUTOFILL_SUGGESTION_INDEX_VALUE,
                                 base::NumberToString16(row + 1),
                                 base::NumberToString16([self rowCount]));
}

// Creates the UI action used to open the payment methods view.
- (UIAction*)openPaymentMethodsAction {
  __weak __typeof(self) weakSelf = self;
  void (^paymentMethodsButtonTapHandler)(UIAction*) = ^(UIAction* action) {
    // Open Payment Methods.
    weakSelf.disableBottomSheetOnExit = NO;
    [weakSelf.handler displayPaymentMethods];
  };
  UIImage* creditCardIcon =
      DefaultSymbolWithPointSize(kCreditCardSymbol, kSymbolActionPointSize);
  return [UIAction
      actionWithTitle:l10n_util::GetNSString(
                          IDS_IOS_PAYMENT_BOTTOM_SHEET_MANAGE_PAYMENT_METHODS)
                image:creditCardIcon
           identifier:nil
              handler:paymentMethodsButtonTapHandler];
}

// Creates the UI action used to open the payment details for form suggestion at
// index path.
// Test.
- (UIAction*)openPaymentDetailsForIndexPath:(NSIndexPath*)indexPath {
  __weak __typeof(self) weakSelf = self;
  NSString* creditCardIdentifier =
      [_creditCardData[indexPath.row] backendIdentifier];

  void (^showDetailsButtonTapHandler)(UIAction*) = ^(UIAction* action) {
    // Open Payments Details.
    weakSelf.disableBottomSheetOnExit = NO;
    [weakSelf.handler
        displayPaymentDetailsForCreditCardIdentifier:creditCardIdentifier];
  };

  UIImage* infoIcon =
      DefaultSymbolWithPointSize(kInfoCircleSymbol, kSymbolActionPointSize);
  return
      [UIAction actionWithTitle:l10n_util::GetNSString(
                                    IDS_IOS_PAYMENT_BOTTOM_SHEET_SHOW_DETAILS)
                          image:infoIcon
                     identifier:nil
                        handler:showDetailsButtonTapHandler];
}

// Layouts the cell for the table view with the payment info at the specific
// index path.
- (TableViewDetailIconCell*)layoutCell:(TableViewDetailIconCell*)cell
                     forTableViewWidth:(CGFloat)tableViewWidth
                           atIndexPath:(NSIndexPath*)indexPath {
  cell.selectionStyle = UITableViewCellSelectionStyleNone;
  cell.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];
  cell.userInteractionEnabled = YES;
  cell.customAccessibilityLabel = [self accessibleCardNameAtRow:indexPath.row];
  cell.accessibilityValue = [self accessibilityValueForCardAtRow:indexPath.row];

  [cell setDetailText:[self descriptionAtRow:indexPath.row]];
  [cell setIconImage:[self iconAtRow:indexPath.row]
            tintColor:nil
      backgroundColor:cell.backgroundColor
         cornerRadius:kCreditCardIconCornerRadius];
  [cell updateIconBackgroundWidthToFitContent:YES];
  [cell setTextLayoutConstraintAxis:UILayoutConstraintAxisVertical];

  cell.textLabel.text = [self suggestionAtRow:indexPath.row];
  cell.textLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
  cell.textLabel.numberOfLines = 1;

  // If we have the potential presence of a virtual card, the textLabel on its
  // own is no longer a unique identifier, so we include the description.
  if (base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableVirtualCards)) {
    cell.accessibilityIdentifier =
        [NSString stringWithFormat:@"%@ %@", cell.textLabel.text,
                                   [self descriptionAtRow:indexPath.row]];
  } else {
    cell.accessibilityIdentifier = cell.textLabel.text;
  }

  cell.separatorInset = [self separatorInsetForTableViewWidth:tableViewWidth
                                                  atIndexPath:indexPath];
  cell.accessoryType = [self accessoryType:indexPath];
  return cell;
}

#pragma mark - ConfirmationAlertViewController

- (void)customizeSubtitle:(UITextView*)subtitle {
  subtitle.textContainerInset = UIEdgeInsetsZero;
}

@end