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

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

#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/virtual_card_enrollment_bottom_sheet_view_controller.h"

#import "build/branding_buildflags.h"
#import "components/autofill/core/browser/payments/payments_service_url.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "components/grit/components_scaled_resources.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/net/model/crurl.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/common/ui/util/text_view_util.h"
#import "ui/base/l10n/l10n_util.h"
#import "url/gurl.h"

static NSString* kDetailIconCellIdentifier = @"DetailIconCell";

namespace {

// The padding above and below the logo image.
CGFloat const kLogoPadding = 16;

// The padding above and below the illustration image.
CGFloat const kIllustrationPadding = 20;

// The spacing between vertically stacked elements.
CGFloat const kVerticalSpacingMedium = 16;

// The credit card corner radius.
CGFloat const kCreditCardCellCornerRadius = 10;

// The credit card cell height.
CGFloat const kCreditCardCellHeight = 64;

}  // namespace

@interface VirtualCardEnrollmentBottomSheetViewController () <
    UITableViewDataSource,
    UITableViewDelegate,
    UITextViewDelegate,
    ConfirmationAlertActionHandler>
@end

@implementation VirtualCardEnrollmentBottomSheetViewController {
  VirtualCardEnrollmentBottomSheetData* _bottomSheetData;

  UITextView* _explanatoryMessageView;
  UIStackView* _customUnderTitleView;
}

#pragma mark - UIViewController

- (void)viewDidLoad {
  self.actionHandler = self;

  // Prevent extra spacing at the top of the bottom sheet content.
  self.topAlignedLayout = YES;

  // Set the spacing between the two stack views.
  self.customSpacing = kVerticalSpacingMedium;

  // Remove extra space between the scroll view bottom and last legal message.
  self.customScrollViewBottomInsets = 0;

  // Hide the "Done" button in the navigation bar.
  self.showDismissBarButton = NO;

  self.aboveTitleView = [self createAboveTitleStackView];

  _customUnderTitleView = [self createUnderTitleView];
  [self addLegalMessages:_bottomSheetData.paymentServerLegalMessageLines];
  [self addLegalMessages:_bottomSheetData.issuerLegalMessageLines];

  self.underTitleView = _customUnderTitleView;

  [super viewDidLoad];
}

#pragma mark - VirtualCardEnrollmentBottomSheetConsumer

- (void)setCardData:(VirtualCardEnrollmentBottomSheetData*)data {
  _bottomSheetData = data;
  self.primaryActionString = data.acceptActionText;
  self.secondaryActionString = data.cancelActionText;
}

- (void)showLoadingState {
  self.primaryActionButton.accessibilityLabel = l10n_util::GetNSString(
      IDS_AUTOFILL_VIRTUAL_CARD_ENROLL_LOADING_THROBBER_ACCESSIBLE_NAME);
  self.isLoading = YES;
  self.isConfirmed = NO;
}

- (void)showConfirmationState {
  self.isLoading = NO;
  self.isConfirmed = YES;
  UIAccessibilityPostNotification(
      UIAccessibilityAnnouncementNotification,
      l10n_util::GetNSString(
          IDS_AUTOFILL_VIRTUAL_CARD_ENROLLED_ACCESSIBILITY_ANNOUNCEMENT));
}

#pragma mark - ConfirmationAlertActionHandler

- (void)confirmationAlertPrimaryAction {
  // Accept button was clicked.
  [self.mutator didAccept];
}

- (void)confirmationAlertSecondaryAction {
  // Dismiss button was clicked.
  [self.mutator didCancel];
}

#pragma mark - UITableViewDataSource

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

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

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

  cell.selectionStyle = UITableViewCellSelectionStyleNone;
  cell.backgroundColor = [UIColor colorNamed:kSecondaryBackgroundColor];
  cell.userInteractionEnabled = NO;
  cell.accessibilityIdentifier =
      _bottomSheetData.creditCard.cardNameAndLastFourDigits;
  [cell.textLabel
      setText:_bottomSheetData.creditCard.cardNameAndLastFourDigits];
  [cell setDetailText:_bottomSheetData.creditCard.cardDetails];
  [cell setIconImage:_bottomSheetData.creditCard.icon
            tintColor:nil
      backgroundColor:cell.backgroundColor
         cornerRadius:kCreditCardCellCornerRadius];
  [cell setTextLayoutConstraintAxis:UILayoutConstraintAxisVertical];

  return cell;
}

#pragma mark - Private

// Create the view containing the logo, illustration, title and message.
- (UIStackView*)createAboveTitleStackView {
  UIStackView* aboveTitleStackView =
      [[UIStackView alloc] initWithFrame:CGRectZero];
  aboveTitleStackView.layoutMarginsRelativeArrangement = YES;
  aboveTitleStackView.axis = UILayoutConstraintAxisVertical;
  aboveTitleStackView.spacing = kVerticalSpacingMedium;

  [aboveTitleStackView
      addArrangedSubview:[[UIView alloc]
                             initWithFrame:CGRectMake(0, 0, 0, kLogoPadding)]];

  [aboveTitleStackView addArrangedSubview:[self createGooglePayLogoView]];
  CGFloat logoIllustrationSpacerHeight =
      kLogoPadding + kIllustrationPadding - aboveTitleStackView.spacing;
  [aboveTitleStackView
      addArrangedSubview:
          [self createVerticalSpacerView:logoIllustrationSpacerHeight]];
  [aboveTitleStackView addArrangedSubview:[self createIllustrationView]];
  [aboveTitleStackView
      addArrangedSubview:[self createVerticalSpacerView:kIllustrationPadding]];
  [aboveTitleStackView addArrangedSubview:[self createTitleLabel]];
  _explanatoryMessageView = [self createExplanatoryMessageTextView];
  [aboveTitleStackView addArrangedSubview:_explanatoryMessageView];
  return aboveTitleStackView;
}

- (UIImageView*)createGooglePayLogoView {
  UIImageView* logoImageTitleView =
      [[UIImageView alloc] initWithImage:[self googlePayBadgeImage]];
  logoImageTitleView.contentMode = UIViewContentModeCenter;
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  if (base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableVcnEnrollLoadingAndConfirmation)) {
    logoImageTitleView.isAccessibilityElement = YES;
    logoImageTitleView.accessibilityLabel =
        l10n_util::GetNSString(IDS_AUTOFILL_GOOGLE_PAY_LOGO_ACCESSIBLE_NAME);
  }
#endif
  return logoImageTitleView;
}

- (UIView*)createVerticalSpacerView:(CGFloat)height {
  return [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, height)];
}

// Returns the google pay badge image corresponding to the current
// UIUserInterfaceStyle (light/dark mode).
- (UIImage*)googlePayBadgeImage {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  return MakeSymbolMulticolor(CustomSymbolWithPointSize(
      kGooglePaySymbol, kCreditCardCellHeight - 2 * kLogoPadding));
#else
  return NativeImage(IDR_AUTOFILL_GOOGLE_PAY);
#endif
}

- (UIImageView*)createIllustrationView {
  UIImageView* illustrationImageView = [[UIImageView alloc]
      initWithImage:[UIImage
                        imageNamed:@"virtual_card_enrollment_illustration"]];
  illustrationImageView.contentMode = UIViewContentModeCenter;
  return illustrationImageView;
}

- (UILabel*)createTitleLabel {
  UILabel* titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
  titleLabel.accessibilityTraits |= UIAccessibilityTraitHeader;
  titleLabel.text = _bottomSheetData.title;
  titleLabel.numberOfLines = 0;  // Allow multiple lines.
  UIFontDescriptor* title2FontDescriptor =
      [UIFont preferredFontForTextStyle:UIFontTextStyleTitle2].fontDescriptor;
  titleLabel.font = [UIFont
      fontWithDescriptor:[title2FontDescriptor fontDescriptorWithSymbolicTraits:
                                                   UIFontDescriptorTraitBold]
                    size:0];
  titleLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
  titleLabel.textAlignment = NSTextAlignmentCenter;
  return titleLabel;
}

- (UITextView*)createExplanatoryMessageTextView {
  UITextView* explanatoryMessageView = CreateUITextViewWithTextKit1();
  explanatoryMessageView.scrollEnabled = NO;
  explanatoryMessageView.editable = NO;
  explanatoryMessageView.delegate = self;
  explanatoryMessageView.translatesAutoresizingMaskIntoConstraints = NO;
  explanatoryMessageView.textContainerInset = UIEdgeInsetsZero;
  explanatoryMessageView.linkTextAttributes =
      @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]};
  explanatoryMessageView.attributedText =
      [self attributedTextForExplanatoryMessage];
  return explanatoryMessageView;
}

- (NSAttributedString*)attributedTextForExplanatoryMessage {
  NSRange rangeOfLearnMore = [_bottomSheetData.explanatoryMessage
      rangeOfString:_bottomSheetData.learnMoreLinkText];
  NSMutableParagraphStyle* centeredTextStyle =
      [[NSMutableParagraphStyle alloc] init];
  centeredTextStyle.alignment = NSTextAlignmentCenter;
  NSMutableAttributedString* attributedText = [[NSMutableAttributedString alloc]
      initWithString:_bottomSheetData.explanatoryMessage
          attributes:@{
            NSForegroundColorAttributeName :
                [UIColor colorNamed:kTextPrimaryColor],
            NSFontAttributeName :
                [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote],
            NSParagraphStyleAttributeName : centeredTextStyle,
          }];
  [attributedText addAttribute:NSLinkAttributeName
                         value:@"unused"
                         range:rangeOfLearnMore];
  return attributedText;
}

- (UIStackView*)createUnderTitleView {
  UIStackView* underTitleView = [[UIStackView alloc] initWithFrame:CGRectZero];
  underTitleView.axis = UILayoutConstraintAxisVertical;
  underTitleView.spacing = kVerticalSpacingMedium;

  [underTitleView addArrangedSubview:[self createCardContainerTableView]];
  return underTitleView;
}

- (UITableView*)createCardContainerTableView {
  UITableView* cardContainerTable =
      [[UITableView alloc] initWithFrame:CGRectZero];
  cardContainerTable.rowHeight = kCreditCardCellHeight;
  cardContainerTable.separatorStyle = UITableViewCellSeparatorStyleNone;
  cardContainerTable.layer.cornerRadius = kCreditCardCellCornerRadius;
  [cardContainerTable registerClass:[TableViewDetailIconCell class]
             forCellReuseIdentifier:kDetailIconCellIdentifier];
  cardContainerTable.dataSource = self;
  cardContainerTable.delegate = self;
  [cardContainerTable.heightAnchor
      constraintEqualToConstant:kCreditCardCellHeight]
      .active = YES;
  return cardContainerTable;
}

// Adds a text view for the given legal message to the under title view.
- (void)addLegalMessages:(NSArray<SaveCardMessageWithLinks*>*)messages {
  for (SaveCardMessageWithLinks* message in messages) {
    UITextView* textView = CreateUITextViewWithTextKit1();
    textView.scrollEnabled = NO;
    textView.editable = NO;
    textView.delegate = self;
    textView.translatesAutoresizingMaskIntoConstraints = NO;
    textView.textContainerInset = UIEdgeInsetsZero;
    textView.linkTextAttributes =
        @{NSForegroundColorAttributeName : [UIColor colorNamed:kBlueColor]};
    textView.backgroundColor = UIColor.clearColor;
    textView.attributedText = [VirtualCardEnrollmentBottomSheetViewController
        attributedTextForText:message.messageText
                     linkUrls:message.linkURLs
                   linkRanges:message.linkRanges];
    [_customUnderTitleView addArrangedSubview:textView];
  }
}

+ (NSAttributedString*)attributedTextForText:(NSString*)text
                                    linkUrls:(std::vector<GURL>)linkURLs
                                  linkRanges:(NSArray*)linkRanges {
  NSMutableParagraphStyle* centeredTextStyle =
      [[NSMutableParagraphStyle alloc] init];
  centeredTextStyle.alignment = NSTextAlignmentCenter;
  NSDictionary* textAttributes = @{
    NSFontAttributeName :
        [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2],
    NSForegroundColorAttributeName : [UIColor colorNamed:kTextSecondaryColor],
    NSParagraphStyleAttributeName : centeredTextStyle,
  };

  NSMutableAttributedString* attributedText =
      [[NSMutableAttributedString alloc] initWithString:text
                                             attributes:textAttributes];
  if (linkRanges) {
    [linkRanges enumerateObjectsUsingBlock:^(NSValue* rangeValue, NSUInteger i,
                                             BOOL* stop) {
      CrURL* crurl = [[CrURL alloc] initWithGURL:linkURLs[i]];
      if (!crurl || !crurl.gurl.is_valid()) {
        return;
      }
      [attributedText addAttribute:NSLinkAttributeName
                             value:crurl.nsurl
                             range:rangeValue.rangeValue];
    }];
  }
  return attributedText;
}

#pragma mark - UITextViewDelegate

- (BOOL)textView:(UITextView*)textView
    shouldInteractWithURL:(NSURL*)URL
                  inRange:(NSRange)characterRange
              interaction:(UITextItemInteraction)interaction {
  if (textView == _explanatoryMessageView) {
    // The learn more link was clicked.
    [self.delegate
        didTapLinkURL:[[CrURL alloc]
                          initWithGURL:autofill::payments::
                                           GetVirtualCardEnrollmentSupportUrl()]
                 text:[textView.text substringWithRange:characterRange]];
    return NO;
  } else {
    // A link in a legal message was clicked.
    [self.delegate
        didTapLinkURL:[[CrURL alloc] initWithNSURL:URL]
                 text:[textView.text substringWithRange:characterRange]];
    return NO;
  }
}

- (UIAction*)textView:(UITextView*)textView
    primaryActionForTextItem:(UITextItem*)textItem
               defaultAction:(UIAction*)defaultAction API_AVAILABLE(ios(17.0)) {
  CrURL* URL = nil;
  if (textView == _explanatoryMessageView) {
    URL = [[CrURL alloc]
        initWithGURL:autofill::payments::GetVirtualCardEnrollmentSupportUrl()];
  } else {
    URL = [[CrURL alloc] initWithNSURL:textItem.link];
  }
  __weak __typeof__(self) weakSelf = self;
  return [UIAction actionWithHandler:^(UIAction* action) {
    [weakSelf.delegate
        didTapLinkURL:URL
                 text:[textView.text substringWithRange:textItem.range]];
  }];
}

@end