// Copyright 2018 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/manual_fill/manual_fill_card_cell.h"
#import "base/metrics/histogram_functions.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "build/branding_buildflags.h"
#import "components/autofill/core/browser/data_model/credit_card.h"
#import "components/autofill/core/browser/payments/autofill_payments_feature_availability.h"
#import "components/autofill/core/browser/payments/payments_service_url.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/grit/components_scaled_resources.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/card_list_delegate.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_card_cell+Testing.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_cell_utils.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_constants.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_content_injector.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_credit_card.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_labeled_chip.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/list_model/list_model.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.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/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/text_view_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "url/gurl.h"
using autofill::CreditCard::RecordType::kVirtualCard;
using base::SysNSStringToUTF8;
@interface ManualFillCardItem ()
// The content delegate for this item.
@property(nonatomic, weak, readonly) id<ManualFillContentInjector>
contentInjector;
// The navigation delegate for this item.
@property(nonatomic, weak, readonly) id<CardListDelegate> navigationDelegate;
// The credit card for this item.
@property(nonatomic, readonly) ManualFillCreditCard* card;
// The UIActions that should be available from the cell's overflow menu button.
@property(nonatomic, strong) NSArray<UIAction*>* menuActions;
// The 0-based index at which the payment method is in the list of payment
// methods to show.
@property(nonatomic, assign) NSInteger cellIndex;
// The part of the cell's accessibility label that is used to indicate the
// 1-based index at which the payment method represented by this item is
// positioned in the list of payment methods to show.
@property(nonatomic, strong) NSString* cellIndexAccessibilityLabel;
@end
@implementation ManualFillCardItem {
// If `YES`, autofill button is shown for the item.
BOOL _showAutofillFormButton;
}
- (instancetype)initWithCreditCard:(ManualFillCreditCard*)card
contentInjector:
(id<ManualFillContentInjector>)contentInjector
navigationDelegate:(id<CardListDelegate>)navigationDelegate
menuActions:(NSArray<UIAction*>*)menuActions
cellIndex:(NSInteger)cellIndex
cellIndexAccessibilityLabel:(NSString*)cellIndexAccessibilityLabel
showAutofillFormButton:(BOOL)showAutofillFormButton {
self = [super initWithType:kItemTypeEnumZero];
if (self) {
_contentInjector = contentInjector;
_navigationDelegate = navigationDelegate;
_card = card;
_menuActions = menuActions;
_cellIndex = cellIndex;
_cellIndexAccessibilityLabel = cellIndexAccessibilityLabel;
_showAutofillFormButton = showAutofillFormButton;
self.cellClass = [ManualFillCardCell class];
}
return self;
}
- (void)configureCell:(ManualFillCardCell*)cell
withStyler:(ChromeTableViewStyler*)styler {
[super configureCell:cell withStyler:styler];
[cell setUpWithCreditCard:self.card
contentInjector:self.contentInjector
navigationDelegate:self.navigationDelegate
menuActions:self.menuActions
cellIndex:_cellIndex
cellIndexAccessibilityLabel:self.cellIndexAccessibilityLabel
showAutofillFormButton:_showAutofillFormButton];
}
@end
namespace {
// Width of the card icon.
constexpr CGFloat kCardIconWidth = 40;
// Width of the GPay icon.
constexpr CGFloat kGPayIconWidth = 37;
// Returns the last four digits of the card number to be used in an
// accessibility label. The digits need to be split so that VoiceOver will read
// them individually.
NSString* CardNumberLastFourDigits(NSString* obfuscated_number) {
NSUInteger length = obfuscated_number.length;
if (length >= 4) {
NSString* lastFourDigits =
[obfuscated_number substringFromIndex:length - 4];
NSMutableArray* digits = [[NSMutableArray alloc] init];
for (NSUInteger i = 0; i < lastFourDigits.length; i++) {
[digits addObject:[lastFourDigits substringWithRange:NSMakeRange(i, 1)]];
}
return [digits componentsJoinedByString:@" "];
;
}
return @"";
}
// Helper method to decide whether or not the GPay icon should be shown in the
// cell.
bool ShouldShowGPayIcon(autofill::CreditCard::RecordType card_record_type) {
switch (card_record_type) {
case autofill::CreditCard::RecordType::kLocalCard:
return false;
case autofill::CreditCard::RecordType::kMaskedServerCard:
case autofill::CreditCard::RecordType::kVirtualCard:
return IsKeyboardAccessoryUpgradeEnabled();
case autofill::CreditCard::RecordType::kFullServerCard:
// Full server cards are a temporary cached state and are not given as
// suggestions for manual fill.
NOTREACHED();
}
}
// Returns the offset to apply when setting the top anchor constraint of the
// GPay icon as there's some empty space above and under the icon on official
// builds.
CGFloat GPayIconTopAnchorOffset() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
return -15;
#else
return 0;
#endif
}
} // namespace
@interface ManualFillCardCell () <UITextViewDelegate>
// The dynamic constraints for all the lines (i.e. not set in createView).
@property(nonatomic, strong)
NSMutableArray<NSLayoutConstraint*>* dynamicConstraints;
// The view displayed at the top the cell containing the card icon, the card
// label and an overflow menu button.
@property(nonatomic, strong) UIView* headerView;
// The label with bank name and network.
@property(nonatomic, strong) UILabel* cardLabel;
// The credit card icon.
@property(nonatomic, strong) UIImageView* cardIcon;
// The menu button displayed in the cell's header.
@property(nonatomic, strong) UIButton* overflowMenuButton;
// The text view with instructions for how to use virtual cards.
@property(nonatomic, strong) UITextView* virtualCardInstructionTextView;
// A labeled chip showing the card number.
@property(nonatomic, strong) ManualFillLabeledChip* cardNumberLabeledChip;
// A button showing the card number.
@property(nonatomic, strong) UIButton* cardNumberButton;
// A labeled chip showing the cardholder name.
@property(nonatomic, strong) ManualFillLabeledChip* cardholderLabeledChip;
// A button showing the cardholder name.
@property(nonatomic, strong) UIButton* cardholderButton;
// A labeled chip showing the card's expiration date.
@property(nonatomic, strong) ManualFillLabeledChip* expirationDateLabeledChip;
// A button showing the expiration month.
@property(nonatomic, strong) UIButton* expirationMonthButton;
// A button showing the expiration year.
@property(nonatomic, strong) UIButton* expirationYearButton;
// A labeled chip showing the card's CVC.
@property(nonatomic, strong) ManualFillLabeledChip* CVCLabeledChip;
// The content delegate for this item.
@property(nonatomic, weak) id<ManualFillContentInjector> contentInjector;
// The navigation delegate for this item.
@property(nonatomic, weak) id<CardListDelegate> navigationDelegate;
// The credit card data for this cell.
@property(nonatomic, weak) ManualFillCreditCard* card;
// Layout guide for the cell's content.
@property(nonatomic, strong) UILayoutGuide* layoutGuide;
// Separator line. Used to delimit the header from the rest of the cell.
@property(nonatomic, strong) UIView* headerSeparator;
// Separator line. Used to delimit the virtual card instruction text view from
// the rest of the cell.
@property(nonatomic, strong) UIView* virtualCardInstructionsSeparator;
// Button to autofill the current form with the card's data.
@property(nonatomic, strong) UIButton* autofillFormButton;
// Icon to indicate that the card is a server card.
@property(nonatomic, strong) UIImageView* gPayIcon;
@end
@implementation ManualFillCardCell {
// The 0-based index at which the payment method is in the list of payment
// methods to show.
NSInteger _cellIndex;
// If `YES`, autofill button is shown for the cell.
BOOL _showAutofillFormButton;
}
#pragma mark - Public
- (void)prepareForReuse {
[super prepareForReuse];
[NSLayoutConstraint deactivateConstraints:self.dynamicConstraints];
[self.dynamicConstraints removeAllObjects];
self.cardLabel.text = @"";
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards)) {
self.virtualCardInstructionTextView.text = @"";
self.virtualCardInstructionTextView.hidden = NO;
[self.cardNumberLabeledChip prepareForReuse];
[self.expirationDateLabeledChip prepareForReuse];
[self.cardholderLabeledChip prepareForReuse];
[self.CVCLabeledChip prepareForReuse];
} else {
// TODO(crbug.com/330329960): Deprecate button use once
// kAutofillEnableVirtualCards is enabled.
[self.cardNumberButton setTitle:@"" forState:UIControlStateNormal];
[self.cardholderButton setTitle:@"" forState:UIControlStateNormal];
[self.expirationMonthButton setTitle:@"" forState:UIControlStateNormal];
[self.expirationYearButton setTitle:@"" forState:UIControlStateNormal];
self.cardNumberButton.hidden = NO;
self.cardholderButton.hidden = NO;
}
self.contentInjector = nil;
self.navigationDelegate = nil;
self.cardIcon.image = nil;
self.card = nil;
_showAutofillFormButton = NO;
}
- (void)setUpWithCreditCard:(ManualFillCreditCard*)card
contentInjector:(id<ManualFillContentInjector>)contentInjector
navigationDelegate:(id<CardListDelegate>)navigationDelegate
menuActions:(NSArray<UIAction*>*)menuActions
cellIndex:(NSInteger)cellIndex
cellIndexAccessibilityLabel:(NSString*)cellIndexAccessibilityLabel
showAutofillFormButton:(BOOL)showAutofillFormButton {
if (!self.dynamicConstraints) {
self.dynamicConstraints = [[NSMutableArray alloc] init];
}
_cellIndex = cellIndex;
_showAutofillFormButton = showAutofillFormButton;
if (self.contentView.subviews.count == 0) {
[self createViewHierarchy];
}
self.contentInjector = contentInjector;
self.navigationDelegate = navigationDelegate;
self.card = card;
[self populateViewsWithCardData:card menuActions:menuActions];
[self verticallyArrangeViews:card];
if (IsKeyboardAccessoryUpgradeEnabled()) {
NSString* accessibilityLabel =
[NSString stringWithFormat:@"%@, %@", cellIndexAccessibilityLabel,
self.cardLabel.attributedText.string];
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
if (ShouldShowGPayIcon(self.card.recordType)) {
accessibilityLabel =
[NSString stringWithFormat:@"%@, %@", accessibilityLabel,
l10n_util::GetNSString(
IDS_IOS_AUTOFILL_WALLET_SERVER_NAME)];
}
#endif
GiveAccessibilityContextToCellAndButton(
self.contentView, self.overflowMenuButton, self.autofillFormButton,
accessibilityLabel);
}
}
#pragma mark - Private
// Creates and sets up the view hierarchy.
- (void)createViewHierarchy {
// Holds the views that should be accessible. The ordering in which views are
// added to this array will reflect the order followed by VoiceOver. When the
// Keyboard Accessory Upgrade feature is enabled, subviews that need to be
// read by VoiceOver must be added to this array. Otherwise, they will be
// ignored.
NSMutableArray<UIView*>* accessibilityElements =
[[NSMutableArray alloc] initWithObjects:self.contentView, nil];
self.layoutGuide =
AddLayoutGuideToContentView(self.contentView, /*cell_has_header=*/YES);
self.selectionStyle = UITableViewCellSelectionStyleNone;
// Create the UIViews, add them to the contentView.
self.cardLabel = CreateLabel();
self.cardIcon = [self createCardIcon];
self.overflowMenuButton = CreateOverflowMenuButton();
self.headerView =
CreateHeaderView(self.cardIcon, self.cardLabel, self.overflowMenuButton);
[self.contentView addSubview:self.headerView];
[accessibilityElements addObject:self.overflowMenuButton];
if (IsKeyboardAccessoryUpgradeEnabled()) {
self.headerSeparator = CreateGraySeparatorForContainer(self.contentView);
} else {
// This separator is used to delimit this cell from the others.
CreateGraySeparatorForContainer(self.contentView);
}
UILabel* expirationDateSeparatorLabel;
// If Virtual Cards are enabled, create UIViews with the labeled chips,
// otherwise use the buttons.
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards)) {
// Virtual card instruction textview is always created, but hidden for
// non-virtual cards.
self.virtualCardInstructionTextView =
[self createVirtualCardInstructionTextView];
[self.contentView addSubview:self.virtualCardInstructionTextView];
[accessibilityElements addObject:self.virtualCardInstructionTextView];
self.virtualCardInstructionsSeparator =
CreateGraySeparatorForContainer(self.contentView);
self.cardNumberLabeledChip = [[ManualFillLabeledChip alloc]
initSingleChipWithTarget:self
selector:@selector(userDidTapCardNumber:)];
[self.contentView addSubview:self.cardNumberLabeledChip];
[accessibilityElements addObject:self.cardNumberLabeledChip.singleButton];
self.expirationDateLabeledChip = [[ManualFillLabeledChip alloc]
initExpirationDateChipWithTarget:self
monthSelector:@selector(userDidTapExpirationMonth:)
yearSelector:@selector(userDidTapExpirationYear:)];
[self.contentView addSubview:self.expirationDateLabeledChip];
[accessibilityElements
addObject:self.expirationDateLabeledChip.expirationMonthButton];
[accessibilityElements
addObject:self.expirationDateLabeledChip.expirationYearButton];
self.cardholderLabeledChip = [[ManualFillLabeledChip alloc]
initSingleChipWithTarget:self
selector:@selector(userDidTapCardholderName:)];
[self.contentView addSubview:self.cardholderLabeledChip];
[accessibilityElements addObject:self.cardholderLabeledChip.singleButton];
self.CVCLabeledChip = [[ManualFillLabeledChip alloc]
initSingleChipWithTarget:self
selector:@selector(userDidTapCVC:)];
[self.contentView addSubview:self.CVCLabeledChip];
[accessibilityElements addObject:self.CVCLabeledChip.singleButton];
} else {
// TODO(crbug.com/330329960): Deprecate button use once
// kAutofillEnableVirtualCards is enabled.
self.cardNumberButton =
CreateChipWithSelectorAndTarget(@selector(userDidTapCardNumber:), self);
[self.contentView addSubview:self.cardNumberButton];
[accessibilityElements addObject:self.cardNumberButton];
self.expirationMonthButton =
CreateChipWithSelectorAndTarget(@selector(userDidTapCardInfo:), self);
[self.contentView addSubview:self.expirationMonthButton];
[accessibilityElements addObject:self.expirationMonthButton];
expirationDateSeparatorLabel = [self createExpirationSeparatorLabel];
[self.contentView addSubview:expirationDateSeparatorLabel];
self.expirationYearButton =
CreateChipWithSelectorAndTarget(@selector(userDidTapCardInfo:), self);
[self.contentView addSubview:self.expirationYearButton];
[accessibilityElements addObject:self.expirationYearButton];
self.cardholderButton =
CreateChipWithSelectorAndTarget(@selector(userDidTapCardInfo:), self);
[self.contentView addSubview:self.cardholderButton];
[accessibilityElements addObject:self.cardholderButton];
}
self.autofillFormButton = CreateAutofillFormButton();
[self.contentView addSubview:self.autofillFormButton];
[accessibilityElements addObject:self.autofillFormButton];
[self.autofillFormButton addTarget:self
action:@selector(onAutofillFormButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
[self horizontallyArrangeViews:expirationDateSeparatorLabel];
SetUpCellAccessibilityElements(self, accessibilityElements);
}
// Horizontally positions the UIViews.
- (void)horizontallyArrangeViews:(UILabel*)expirationDateSeparatorLabel {
NSMutableArray<NSLayoutConstraint*>* staticConstraints =
[[NSMutableArray alloc] init];
AppendHorizontalConstraintsForViews(staticConstraints, @[ self.headerView ],
self.layoutGuide);
// If Virtual Cards are enabled, position the labeled chips, else position the
// regular buttons.
self.gPayIcon = [self createGPayIcon];
[self.contentView addSubview:self.gPayIcon];
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards)) {
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.virtualCardInstructionTextView ],
self.layoutGuide);
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.cardNumberLabeledChip ], self.layoutGuide,
kChipsHorizontalMargin,
AppendConstraintsHorizontalEqualOrSmallerThanGuide, self.gPayIcon);
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.expirationDateLabeledChip ],
self.layoutGuide, kChipsHorizontalMargin,
AppendConstraintsHorizontalEqualOrSmallerThanGuide);
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.cardholderLabeledChip ], self.layoutGuide,
kChipsHorizontalMargin,
AppendConstraintsHorizontalEqualOrSmallerThanGuide);
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.CVCLabeledChip ], self.layoutGuide,
kChipsHorizontalMargin,
AppendConstraintsHorizontalEqualOrSmallerThanGuide);
[staticConstraints
addObject:[self.gPayIcon.topAnchor
constraintEqualToAnchor:self.cardNumberLabeledChip
.topAnchor
constant:GPayIconTopAnchorOffset()]];
} else {
// TODO(crbug.com/330329960): Deprecate button use once
// kAutofillEnableVirtualCards is enabled.
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.cardNumberButton ], self.layoutGuide,
kChipsHorizontalMargin,
AppendConstraintsHorizontalEqualOrSmallerThanGuide, self.gPayIcon);
AppendHorizontalConstraintsForViews(
staticConstraints,
@[
self.expirationMonthButton, expirationDateSeparatorLabel,
self.expirationYearButton
],
self.layoutGuide, kChipsHorizontalMargin,
AppendConstraintsHorizontalSyncBaselines |
AppendConstraintsHorizontalEqualOrSmallerThanGuide);
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.cardholderButton ], self.layoutGuide,
kChipsHorizontalMargin,
AppendConstraintsHorizontalEqualOrSmallerThanGuide);
[staticConstraints
addObject:[self.gPayIcon.topAnchor
constraintEqualToAnchor:self.cardNumberButton.topAnchor
constant:GPayIconTopAnchorOffset()]];
}
AppendHorizontalConstraintsForViews(
staticConstraints, @[ self.autofillFormButton ], self.layoutGuide);
if (!IsKeyboardAccessoryUpgradeEnabled()) {
// Without this set, Voice Over will read the content vertically instead of
// horizontally.
self.contentView.shouldGroupAccessibilityChildren = YES;
}
[NSLayoutConstraint activateConstraints:staticConstraints];
}
// Adds the data from the ManualFillCreditCard to the corresponding UIViews.
- (void)populateViewsWithCardData:(ManualFillCreditCard*)card
menuActions:(NSArray<UIAction*>*)menuActions {
self.cardIcon.image = card.icon;
if (menuActions && menuActions.count) {
self.overflowMenuButton.menu = [UIMenu menuWithChildren:menuActions];
self.overflowMenuButton.hidden = NO;
} else {
self.overflowMenuButton.hidden = YES;
}
self.gPayIcon.hidden = !ShouldShowGPayIcon(card.recordType);
self.gPayIcon.accessibilityIdentifier = [NSString
stringWithFormat:@"%@ %@", manual_fill::kPaymentManualFillGPayLogoID,
card.networkAndLastFourDigits];
// If Virtual Cards are enabled set text for labeled chips, else set text for
// buttons.
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards)) {
NSMutableAttributedString* attributedString =
[self createCardLabelAttributedText:card];
self.cardLabel.numberOfLines = 0;
self.cardLabel.attributedText = attributedString;
self.cardLabel.accessibilityIdentifier = attributedString.string;
if (card.recordType == kVirtualCard) {
self.virtualCardInstructionTextView.attributedText =
[self createvirtualCardInstructionTextViewAttributedText];
self.virtualCardInstructionTextView.backgroundColor = UIColor.clearColor;
}
[self.cardNumberLabeledChip
setLabelText:
(card.recordType == kVirtualCard
? l10n_util::GetNSString(
IDS_AUTOFILL_VIRTUAL_CARD_MANUAL_FALLBACK_BUBBLE_CARD_NUMBER_LABEL_IOS)
: l10n_util::GetNSString(
IDS_AUTOFILL_REGULAR_CARD_MANUAL_FALLBACK_BUBBLE_CARD_NUMBER_LABEL_IOS))
buttonTitles:@[ card.obfuscatedNumber ]];
[self.expirationDateLabeledChip
setLabelText:
l10n_util::GetNSString(
IDS_AUTOFILL_VIRTUAL_CARD_MANUAL_FALLBACK_BUBBLE_EXP_DATE_LABEL_IOS)
buttonTitles:@[ card.expirationMonth, card.expirationYear ]];
[self.cardholderLabeledChip
setLabelText:
l10n_util::GetNSString(
IDS_AUTOFILL_VIRTUAL_CARD_MANUAL_FALLBACK_BUBBLE_NAME_ON_CARD_LABEL_IOS)
buttonTitles:@[ card.cardHolder ]];
if (card.recordType == kVirtualCard) {
[self.CVCLabeledChip
setLabelText:
l10n_util::GetNSString(
IDS_AUTOFILL_VIRTUAL_CARD_MANUAL_FALLBACK_BUBBLE_CVC_LABEL_IOS)
buttonTitles:@[ card.CVC ]];
}
if (IsKeyboardAccessoryUpgradeEnabled()) {
self.cardNumberLabeledChip.singleButton.accessibilityLabel =
l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_CARD_NUMBER_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(
CardNumberLastFourDigits(card.obfuscatedNumber)));
self.expirationDateLabeledChip.expirationMonthButton.accessibilityLabel =
l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_EXPIRATION_MONTH_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(card.expirationMonth));
self.expirationDateLabeledChip.expirationYearButton.accessibilityLabel =
l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_EXPIRATION_YEAR_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(card.expirationYear));
self.cardholderLabeledChip.singleButton.accessibilityLabel =
l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_CARDHOLDER_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(card.cardHolder));
self.CVCLabeledChip.singleButton.accessibilityLabel =
l10n_util::GetNSString(
IDS_IOS_MANUAL_FALLBACK_CVC_CHIP_ACCESSIBILITY_LABEL);
}
} else {
// TODO(crbug.com/330329960): Deprecate button use once
// kAutofillEnableVirtualCards is enabled.
NSString* cardName = [self createCardName:card];
self.cardLabel.attributedText = [[NSMutableAttributedString alloc]
initWithString:cardName
attributes:@{
NSForegroundColorAttributeName :
[UIColor colorNamed:kTextPrimaryColor],
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]
}];
[self.cardNumberButton setTitle:card.obfuscatedNumber
forState:UIControlStateNormal];
[self.expirationMonthButton setTitle:card.expirationMonth
forState:UIControlStateNormal];
[self.expirationYearButton setTitle:card.expirationYear
forState:UIControlStateNormal];
[self.cardholderButton setTitle:card.cardHolder
forState:UIControlStateNormal];
if (IsKeyboardAccessoryUpgradeEnabled()) {
self.cardNumberButton.accessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_CARD_NUMBER_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(
CardNumberLastFourDigits(card.obfuscatedNumber)));
self.expirationMonthButton.accessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_EXPIRATION_MONTH_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(card.expirationMonth));
self.expirationYearButton.accessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_EXPIRATION_YEAR_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(card.expirationYear));
self.cardholderButton.accessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_MANUAL_FALLBACK_CARDHOLDER_CHIP_ACCESSIBILITY_LABEL,
base::SysNSStringToUTF16(card.cardHolder));
}
}
}
// Positions the UIViews vertically.
- (void)verticallyArrangeViews:(ManualFillCreditCard*)card {
// Holds the views whose leading anchor is constrained relative to the cell's
// leading anchor.
std::vector<ManualFillCellView> verticalLeadViews;
AddViewToVerticalLeadViews(self.headerView,
ManualFillCellView::ElementType::kOther,
verticalLeadViews);
if (IsKeyboardAccessoryUpgradeEnabled()) {
AddViewToVerticalLeadViews(
self.headerSeparator, ManualFillCellView::ElementType::kHeaderSeparator,
verticalLeadViews);
}
// Holds the chip buttons related to the card that are vertical leads.
NSMutableArray<UIView*>* cardInfoGroupVerticalLeadChips =
[[NSMutableArray alloc] init];
// If Virtual Cards are enabled add labeled chips to be positioned
// else just add the buttons.
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards)) {
// Virtual card instruction.
if (card.recordType == kVirtualCard) {
AddViewToVerticalLeadViews(
self.virtualCardInstructionTextView,
ManualFillCellView::ElementType::kVirtualCardInstructions,
verticalLeadViews);
if (IsKeyboardAccessoryUpgradeEnabled()) {
AddViewToVerticalLeadViews(
self.virtualCardInstructionsSeparator,
ManualFillCellView::ElementType::kVirtualCardInstructionsSeparator,
verticalLeadViews);
self.virtualCardInstructionsSeparator.hidden = NO;
}
self.virtualCardInstructionTextView.hidden = NO;
} else {
self.virtualCardInstructionTextView.hidden = YES;
self.virtualCardInstructionsSeparator.hidden = YES;
}
// Card number labeled chip button.
[self addChipButton:self.cardNumberLabeledChip
toChipGroup:cardInfoGroupVerticalLeadChips
ifTrue:(card.obfuscatedNumber.length > 0)];
// Expiration date labeled chip button.
[cardInfoGroupVerticalLeadChips addObject:self.expirationDateLabeledChip];
// Card holder labeled chip button.
[self addChipButton:self.cardholderLabeledChip
toChipGroup:cardInfoGroupVerticalLeadChips
ifTrue:(card.cardHolder.length > 0)];
// CVC labeled chip button.
[self addChipButton:self.CVCLabeledChip
toChipGroup:cardInfoGroupVerticalLeadChips
ifTrue:(card.CVC.length > 0)];
} else {
// TODO(crbug.com/330329960): Deprecate button use once
// kAutofillEnableVirtualCards is enabled.
// Card number chip button.
[self addChipButton:self.cardNumberButton
toChipGroup:cardInfoGroupVerticalLeadChips
ifTrue:(card.obfuscatedNumber.length > 0)];
// Expiration date chip button.
[cardInfoGroupVerticalLeadChips addObject:self.expirationMonthButton];
// Card holder chip button.
[self addChipButton:self.cardholderButton
toChipGroup:cardInfoGroupVerticalLeadChips
ifTrue:(card.cardHolder.length > 0)];
}
AddChipGroupsToVerticalLeadViews(@[ cardInfoGroupVerticalLeadChips ],
verticalLeadViews);
if (_showAutofillFormButton) {
CHECK(IsKeyboardAccessoryUpgradeEnabled());
AddViewToVerticalLeadViews(self.autofillFormButton,
ManualFillCellView::ElementType::kOther,
verticalLeadViews);
self.autofillFormButton.hidden = NO;
} else {
self.autofillFormButton.hidden = YES;
}
// Set and activate constraints.
AppendVerticalConstraintsSpacingForViews(self.dynamicConstraints,
verticalLeadViews, self.layoutGuide);
[NSLayoutConstraint activateConstraints:self.dynamicConstraints];
}
- (void)userDidTapCardNumber:(UIButton*)sender {
NSString* number = self.card.number;
if (![self.contentInjector canUserInjectInPasswordField:NO
requiresHTTPS:YES]) {
return;
}
if (base::FeatureList::IsEnabled(
autofill::features::kAutofillEnableVirtualCards)) {
base::RecordAction(base::UserMetricsAction(
[self createMetricsAction:@"SelectCardNumber"]));
} else {
base::RecordAction(
base::UserMetricsAction("ManualFallback_CreditCard_SelectCardNumber"));
}
if (self.card.canFillDirectly) {
[self.contentInjector userDidPickContent:number
passwordField:NO
requiresHTTPS:YES];
} else {
[self.navigationDelegate
requestFullCreditCard:self.card
fieldType:manual_fill::PaymentFieldType::kCardNumber];
}
}
// TODO(crbug.com/330329960): Deprecate this method once
// kAutofillEnableVirtualCards is enabled.
- (void)userDidTapCardInfo:(UIButton*)sender {
const char* metricsAction = nullptr;
if (sender == self.cardholderButton) {
metricsAction = "ManualFallback_CreditCard_SelectCardholderName";
} else if (sender == self.expirationMonthButton) {
metricsAction = "ManualFallback_CreditCard_SelectExpirationMonth";
} else if (sender == self.expirationYearButton) {
metricsAction = "ManualFallback_CreditCard_SelectExpirationYear";
}
DCHECK(metricsAction);
base::RecordAction(base::UserMetricsAction(metricsAction));
[self.contentInjector userDidPickContent:sender.titleLabel.text
passwordField:NO
requiresHTTPS:NO];
}
- (void)userDidTapCardholderName:(UIButton*)sender {
base::RecordAction(base::UserMetricsAction(
[self createMetricsAction:@"SelectCardholderName"]));
[self.contentInjector userDidPickContent:sender.titleLabel.text
passwordField:NO
requiresHTTPS:NO];
}
- (void)userDidTapExpirationMonth:(UIButton*)sender {
base::RecordAction(base::UserMetricsAction(
[self createMetricsAction:@"SelectExpirationMonth"]));
if (self.card.recordType == kVirtualCard) {
[self.navigationDelegate
requestFullCreditCard:self.card
fieldType:manual_fill::PaymentFieldType::kExpirationMonth];
} else {
[self.contentInjector userDidPickContent:sender.titleLabel.text
passwordField:NO
requiresHTTPS:NO];
}
}
- (void)userDidTapExpirationYear:(UIButton*)sender {
base::RecordAction(base::UserMetricsAction(
[self createMetricsAction:@"SelectExpirationYear"]));
if (self.card.recordType == kVirtualCard) {
[self.navigationDelegate
requestFullCreditCard:self.card
fieldType:manual_fill::PaymentFieldType::kExpirationYear];
} else {
[self.contentInjector userDidPickContent:sender.titleLabel.text
passwordField:NO
requiresHTTPS:NO];
}
}
- (void)userDidTapCVC:(UIButton*)sender {
CHECK_EQ(self.card.recordType, kVirtualCard);
base::RecordAction(
base::UserMetricsAction([self createMetricsAction:@"SelectCvc"]));
[self.navigationDelegate
requestFullCreditCard:self.card
fieldType:manual_fill::PaymentFieldType::kCVC];
}
// Called when the "Autofill Form" button is tapped. Fills the current form with
// the card's data.
- (void)onAutofillFormButtonTapped {
base::UmaHistogramSparse(
"Autofill.UserAcceptedSuggestionAtIndex.CreditCard.ManualFallback",
_cellIndex);
base::RecordAction(
base::UserMetricsAction("ManualFallback_CreditCard_SuggestionAccepted"));
autofill::SuggestionType type =
autofill::VirtualCardFeatureEnabled() &&
[self.card recordType] == kVirtualCard
? autofill::SuggestionType::kVirtualCreditCardEntry
: autofill::SuggestionType::kCreditCardEntry;
FormSuggestion* suggestion =
[FormSuggestion suggestionWithValue:nil
minorValue:nil
displayDescription:nil
icon:nil
type:type
backendIdentifier:[self.card GUID]
requiresReauth:NO
acceptanceA11yAnnouncement:
base::SysUTF16ToNSString(l10n_util::GetStringUTF16(
IDS_AUTOFILL_A11Y_ANNOUNCE_FILLED_FORM))];
[self.contentInjector autofillFormWithSuggestion:suggestion
atIndex:_cellIndex];
}
- (const char*)createMetricsAction:(NSString*)selectedChip {
return [NSString stringWithFormat:@"ManualFallback_%@_%@",
self.card.recordType == kVirtualCard
? @"VirtualCard"
: @"CreditCard",
selectedChip]
.UTF8String;
}
- (NSString*)createCardName:(ManualFillCreditCard*)card {
NSString* cardName;
// TODO: b/322543459 Take out deprecated bank name, add functionality for card
// product name.
if (card.bankName.length) {
cardName = card.network;
} else {
cardName =
[NSString stringWithFormat:@"%@ %@", card.network, card.bankName];
}
return cardName;
}
// Creates the attributed string containing the card name and potentially a
// virtual card subtitle for the card label.
- (NSMutableAttributedString*)createCardLabelAttributedText:
(ManualFillCreditCard*)card {
NSString* cardName = [self createCardName:card];
NSString* virtualCardSubtitle =
card.recordType == kVirtualCard
? l10n_util::GetNSString(
IDS_AUTOFILL_VIRTUAL_CARD_SUGGESTION_OPTION_VALUE)
: nil;
return CreateHeaderAttributedString(cardName, virtualCardSubtitle);
}
// Creates the attributed string for virtual card instructions.
- (NSMutableAttributedString*)
createvirtualCardInstructionTextViewAttributedText {
NSMutableAttributedString* virtualCardInstructionAttributedString =
[[NSMutableAttributedString alloc]
initWithString:
[NSString
stringWithFormat:
@"%@ ",
l10n_util::GetNSString(
IDS_AUTOFILL_PAYMENTS_MANUAL_FALLBACK_VIRTUAL_CARD_INSTRUCTION_TEXT)]
attributes:@{
NSForegroundColorAttributeName :
[UIColor colorNamed:kTextSecondaryColor],
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]
}];
NSString* learnMoreString = l10n_util::GetNSString(
IDS_AUTOFILL_VIRTUAL_CARD_ENROLLMENT_LEARN_MORE_LINK_LABEL);
NSMutableAttributedString* virtualCardLearnMoreAttributedString =
[[NSMutableAttributedString alloc]
initWithString:learnMoreString
attributes:@{
NSForegroundColorAttributeName :
[UIColor colorNamed:kBlueColor],
NSFontAttributeName :
[UIFont preferredFontForTextStyle:UIFontTextStyleFootnote]
}];
[virtualCardLearnMoreAttributedString
addAttribute:NSLinkAttributeName
value:@"unused"
range:[learnMoreString rangeOfString:learnMoreString]];
[virtualCardInstructionAttributedString
appendAttributedString:virtualCardLearnMoreAttributedString];
return virtualCardInstructionAttributedString;
}
- (UITextView*)createVirtualCardInstructionTextView {
UITextView* virtualCardInstructionTextView =
[[UITextView alloc] initWithFrame:self.contentView.frame];
virtualCardInstructionTextView.scrollEnabled = NO;
virtualCardInstructionTextView.editable = NO;
virtualCardInstructionTextView.delegate = self;
virtualCardInstructionTextView.translatesAutoresizingMaskIntoConstraints = NO;
virtualCardInstructionTextView.textColor =
[UIColor colorNamed:kTextSecondaryColor];
virtualCardInstructionTextView.backgroundColor = UIColor.clearColor;
virtualCardInstructionTextView.textContainerInset =
UIEdgeInsetsMake(0, 0, 0, 0);
virtualCardInstructionTextView.textContainer.lineFragmentPadding = 0;
return virtualCardInstructionTextView;
}
// TODO(crbug.com/330329960): Deprecate this method use once
// kAutofillEnableVirtualCards is enabled.
- (UILabel*)createExpirationSeparatorLabel {
UILabel* expirationSeparatorLabel = CreateLabel();
expirationSeparatorLabel.font =
[UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
[expirationSeparatorLabel setTextColor:[UIColor colorNamed:kSeparatorColor]];
expirationSeparatorLabel.text = @"/";
return expirationSeparatorLabel;
}
// Creates and configures the card icon image view.
- (UIImageView*)createCardIcon {
UIImageView* cardIcon = [[UIImageView alloc] init];
cardIcon.translatesAutoresizingMaskIntoConstraints = NO;
[cardIcon setContentHuggingPriority:UILayoutPriorityDefaultHigh
forAxis:UILayoutConstraintAxisHorizontal];
if (IsKeyboardAccessoryUpgradeEnabled()) {
cardIcon.contentMode = UIViewContentModeScaleAspectFill;
[cardIcon.widthAnchor constraintEqualToConstant:kCardIconWidth].active =
YES;
}
return cardIcon;
}
// Adds or hides ChipButton depending on the 'test' boolean.
- (void)addChipButton:(UIView*)chipButton
toChipGroup:(NSMutableArray<UIView*>*)chipGroup
ifTrue:(BOOL)test {
if (test) {
[chipGroup addObject:chipButton];
chipButton.hidden = NO;
} else {
chipButton.hidden = YES;
}
}
- (BOOL)textView:(UITextView*)textView
shouldInteractWithURL:(NSURL*)URL
inRange:(NSRange)characterRange
interaction:(UITextItemInteraction)interaction {
if (textView == self.virtualCardInstructionTextView) {
// The learn more link was clicked.
[self.navigationDelegate
openURL:[[CrURL alloc]
initWithGURL:autofill::payments::
GetVirtualCardEnrollmentSupportUrl()]
withTitle:[textView.text substringWithRange:characterRange]];
}
return NO;
}
// Creates and configures the GPay icon image view.
- (UIImageView*)createGPayIcon {
UIImage* icon;
// `kGooglePaySymbol` only exists in official builds.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
icon = MakeSymbolMulticolor(
CustomSymbolWithPointSize(kGooglePaySymbol, kGPayIconWidth));
#else
icon = NativeImage(IDR_AUTOFILL_GOOGLE_PAY);
#endif
UIImageView* imageView = [[UIImageView alloc] initWithImage:icon];
imageView.translatesAutoresizingMaskIntoConstraints = NO;
imageView.contentMode = UIViewContentModeScaleAspectFit;
[NSLayoutConstraint
activateConstraints:@[ [imageView.widthAnchor
constraintEqualToConstant:kGPayIconWidth] ]];
return imageView;
}
@end