// 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/authentication/otp_input_dialog_view_controller.h"
#import "base/check.h"
#import "base/task/sequenced_task_runner.h"
#import "base/task/task_runner.h"
#import "base/time/time.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/net/model/crurl.h"
#import "ios/chrome/browser/shared/ui/list_model/list_model.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_text_edit_item.h"
#import "ios/chrome/browser/shared/ui/table_view/cells/table_view_text_header_footer_item.h"
#import "ios/chrome/browser/shared/ui/table_view/table_view_utils.h"
#import "ios/chrome/browser/autofill/ui_bundled/authentication/otp_input_dialog_content.h"
#import "ios/chrome/browser/autofill/ui_bundled/authentication/otp_input_dialog_mutator.h"
#import "ios/chrome/browser/autofill/ui_bundled/authentication/otp_input_dialog_view_constants.h"
#import "ios/chrome/browser/autofill/ui_bundled/cells/card_unmask_header_item.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ui/base/l10n/l10n_util.h"
namespace {
typedef NS_ENUM(NSInteger, SectionIdentifier) {
SectionIdentifierContent = kSectionIdentifierEnumZero,
SectionIdentifierError,
SectionIdentifierNewCodeLink,
};
typedef NS_ENUM(NSInteger, ItemIdentifier) {
ItemTypeTextField = kItemTypeEnumZero,
};
// Dummy URL used as target of the link in the new code link.
constexpr char kDummyLinkTarget[] = "about:blank";
// The cooldown time for the "Get new code" link after it is clicked.
constexpr base::TimeDelta kNewCodeLinkCooldownTime = base::Seconds(5);
} // namespace
@interface OtpInputDialogViewController () <
TableViewLinkHeaderFooterItemDelegate,
UITableViewDelegate,
UITextFieldDelegate> {
}
@end
@implementation OtpInputDialogViewController {
OtpInputDialogContent* _content;
UITableViewDiffableDataSource<NSNumber*, NSNumber*>* _dataSource;
BOOL _contentSet;
NSString* _inputValue;
NSString* _errorTitle;
BOOL _shouldEnableNewCodeLink;
}
- (instancetype)init {
if ((self = [super initWithStyle:UITableViewStyleInsetGrouped])) {
_shouldEnableNewCodeLink = YES;
}
return self;
}
#pragma mark - ChromeTableViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = l10n_util::GetNSString(
IDS_AUTOFILL_CARD_UNMASK_PROMPT_NAVIGATION_TITLE_VERIFICATION);
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self
action:@selector(didTapCancelButton)];
self.navigationItem.rightBarButtonItem = [self createConfirmButton];
[self loadModel];
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView*)tableView
heightForHeaderInSection:(NSInteger)section {
SectionIdentifier sectionIdentifier = static_cast<SectionIdentifier>(
[_dataSource sectionIdentifierForIndex:section].integerValue);
switch (sectionIdentifier) {
case SectionIdentifierContent:
case SectionIdentifierNewCodeLink:
return UITableViewAutomaticDimension;
case SectionIdentifierError:
return ChromeTableViewHeightForHeaderInSection(sectionIdentifier);
}
}
- (UIView*)tableView:(UITableView*)tableView
viewForHeaderInSection:(NSInteger)section {
SectionIdentifier sectionIdentifier = static_cast<SectionIdentifier>(
[_dataSource sectionIdentifierForIndex:section].integerValue);
switch (sectionIdentifier) {
case SectionIdentifierContent: {
CardUnmaskHeaderView* view =
DequeueTableViewHeaderFooter<CardUnmaskHeaderView>(self.tableView);
view.titleLabel.text = _content.windowTitle;
return view;
}
case SectionIdentifierError: {
if (!_errorTitle) {
return nil;
}
TableViewTextHeaderFooterView* errorMessage =
DequeueTableViewHeaderFooter<TableViewTextHeaderFooterView>(
self.tableView);
[errorMessage setSubtitle:_errorTitle
withColor:[UIColor colorNamed:kRedColor]];
[errorMessage setForceIndents:YES];
errorMessage.accessibilityIdentifier =
kOtpInputErrorMessageAccessibilityIdentifier;
return errorMessage;
}
case SectionIdentifierNewCodeLink: {
return [self createNewCodeLink];
}
}
}
#pragma mark - TableViewLinkHeaderFooterItemDelegate
- (void)view:(TableViewLinkHeaderFooterView*)view didTapLinkURL:(CrURL*)URL {
[_mutator didTapNewCodeLink];
[self setEnableNewCodeLink:NO];
__weak __typeof(self) weakSelf = self;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, base::BindOnce(^{
[weakSelf setEnableNewCodeLink:YES];
}),
kNewCodeLinkCooldownTime);
}
#pragma mark - PaymentsSuggestionBottomSheetConsumer
- (void)setContent:(OtpInputDialogContent*)content {
// Content should not be updated once initialized.
CHECK(!_contentSet);
_content = content;
_contentSet = YES;
}
- (void)setConfirmButtonEnabled:(BOOL)enabled {
self.navigationItem.rightBarButtonItem.enabled = enabled;
}
- (void)showPendingState {
UIActivityIndicatorView* activityIndicator = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium];
UIBarButtonItem* pendingButton =
[[UIBarButtonItem alloc] initWithCustomView:activityIndicator];
pendingButton.accessibilityIdentifier =
kOtpInputNavigationBarPendingButtonAccessibilityIdentifier;
self.navigationItem.rightBarButtonItem = pendingButton;
[activityIndicator startAnimating];
[self.tableView setUserInteractionEnabled:NO];
}
- (void)showInvalidState:(NSString*)invalidLabelText {
self.navigationItem.rightBarButtonItem = [self createConfirmButton];
_errorTitle = invalidLabelText;
[self.tableView setUserInteractionEnabled:YES];
[self reloadModel];
}
#pragma mark - Private
// Helper function to load the model to the data source.
- (void)loadModel {
CHECK(_contentSet);
RegisterTableViewHeaderFooter<CardUnmaskHeaderView>(self.tableView);
RegisterTableViewCell<TableViewTextEditCell>(self.tableView);
RegisterTableViewHeaderFooter<TableViewTextHeaderFooterView>(self.tableView);
RegisterTableViewHeaderFooter<TableViewLinkHeaderFooterView>(self.tableView);
__weak __typeof(self) weakSelf = self;
_dataSource = [[UITableViewDiffableDataSource alloc]
initWithTableView:self.tableView
cellProvider:^UITableViewCell*(UITableView* tableView,
NSIndexPath* indexPath,
NSNumber* itemIdentifier) {
return
[weakSelf cellForTableView:tableView
indexPath:indexPath
itemIdentifier:static_cast<ItemIdentifier>(
itemIdentifier.integerValue)];
}];
NSDiffableDataSourceSnapshot* snapshot =
[[NSDiffableDataSourceSnapshot alloc] init];
[snapshot appendSectionsWithIdentifiers:@[ @(SectionIdentifierContent) ]];
[snapshot appendItemsWithIdentifiers:@[ @(ItemTypeTextField) ]
intoSectionWithIdentifier:@(SectionIdentifierContent)];
[snapshot appendSectionsWithIdentifiers:@[ @(SectionIdentifierError) ]];
[snapshot appendSectionsWithIdentifiers:@[ @(SectionIdentifierNewCodeLink) ]];
[_dataSource applySnapshot:snapshot animatingDifferences:YES];
}
- (void)reloadModel {
NSDiffableDataSourceSnapshot* snapshot = [_dataSource snapshot];
[snapshot reconfigureItemsWithIdentifiers:@[ @(ItemTypeTextField) ]];
[snapshot reloadSectionsWithIdentifiers:@[ @(SectionIdentifierError) ]];
[_dataSource applySnapshot:snapshot animatingDifferences:YES];
}
// Returns the appropriate cell for the table view.
- (UITableViewCell*)cellForTableView:(UITableView*)tableView
indexPath:(NSIndexPath*)indexPath
itemIdentifier:(ItemIdentifier)itemIdentifier {
TableViewTextEditCell* cell =
DequeueTableViewCell<TableViewTextEditCell>(self.tableView);
[cell setIdentifyingIcon:nil];
if (_errorTitle) {
[cell setIcon:TableViewTextEditItemIconTypeError];
cell.textField.text = _inputValue;
cell.textField.textColor = [UIColor colorNamed:kRedColor];
} else {
[cell setIcon:TableViewTextEditItemIconTypeEdit];
cell.textField.textColor = [UIColor colorNamed:kTextPrimaryColor];
}
cell.textField.placeholder = _content.textFieldPlaceholder;
cell.textField.accessibilityIdentifier =
kOtpInputTextfieldAccessibilityIdentifier;
[cell.textField addTarget:self
action:@selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
cell.textField.keyboardType = UIKeyboardTypeNumberPad;
cell.textField.returnKeyType = UIReturnKeyDone;
cell.textField.textAlignment = NSTextAlignmentLeft;
return cell;
}
- (void)textFieldDidChange:(UITextField*)textField {
// Reset error state.
if (_errorTitle) {
_errorTitle = nil;
[self reloadModel];
}
_inputValue = textField.text;
[self didChangeOtpInputText];
}
// Invoked when the confirm button in the navigation bar is tapped by the user.
// This means a valid OTP value is typed in.
- (void)didTapConfirmButton {
[_mutator didTapConfirmButton:_inputValue];
}
// Invoked when the cancel button in the navigation bar is tapped by the user.
- (void)didTapCancelButton {
[_mutator didTapCancelButton];
}
// Notify the model controller when the OTP input value changes.
- (void)didChangeOtpInputText {
[_mutator onOtpInputChanges:_inputValue];
}
- (UIBarButtonItem*)createConfirmButton {
UIBarButtonItem* confirmButton =
[[UIBarButtonItem alloc] initWithTitle:_content.confirmButtonLabel
style:UIBarButtonItemStyleDone
target:self
action:@selector(didTapConfirmButton)];
// Enable the confirm button only after a valid OTP has been entered.
confirmButton.enabled = NO;
confirmButton.accessibilityIdentifier =
kOtpInputNavigationBarConfirmButtonAccessibilityIdentifier;
return confirmButton;
}
// Create a link and when tapped it will request a new OTP code from the server.
// Upon clicking the link will be disabled for a short period of time.
- (TableViewLinkHeaderFooterView*)createNewCodeLink {
TableViewLinkHeaderFooterView* newCodeLink =
DequeueTableViewHeaderFooter<TableViewLinkHeaderFooterView>(
self.tableView);
// Using a dummy target for the link in the footer.
// The link target is ignored and taps on it are handled by `didTapLinkURL`.
newCodeLink.urls = @[ [[CrURL alloc] initWithGURL:GURL(kDummyLinkTarget)] ];
newCodeLink.delegate = self;
newCodeLink.accessibilityIdentifier = kOtpInputFooterAccessibilityIdentifier;
[newCodeLink
setText:l10n_util::GetNSString(
IDS_AUTOFILL_CARD_UNMASK_OTP_INPUT_DIALOG_FOOTER_LINK)
withColor:[UIColor colorNamed:(kTextSecondaryColor)]
textAlignment:NSTextAlignmentCenter];
[newCodeLink setLinkEnabled:_shouldEnableNewCodeLink];
return newCodeLink;
}
- (void)setEnableNewCodeLink:(BOOL)enabled {
_shouldEnableNewCodeLink = enabled;
NSDiffableDataSourceSnapshot* snapshot = [_dataSource snapshot];
[snapshot reloadSectionsWithIdentifiers:@[ @(SectionIdentifierNewCodeLink) ]];
[_dataSource applySnapshot:snapshot animatingDifferences:YES];
}
@end