chromium/ios/chrome/browser/overlays/ui_bundled/infobar_modal/save_card/save_card_infobar_modal_overlay_mediator.mm

// Copyright 2020 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/overlays/ui_bundled/infobar_modal/save_card/save_card_infobar_modal_overlay_mediator.h"

#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/strings/sys_string_conversions.h"
#import "base/timer/timer.h"
#import "components/autofill/core/browser/metrics/payments/credit_card_save_metrics.h"
#import "components/autofill/core/browser/payments/autofill_save_card_infobar_delegate_mobile.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "ios/chrome/browser/autofill/model/credit_card/autofill_save_card_infobar_delegate_ios.h"
#import "ios/chrome/browser/autofill/model/message/save_card_message_with_links.h"
#import "ios/chrome/browser/infobars/model/overlays/infobar_overlay_util.h"
#import "ios/chrome/browser/overlays/model/public/default/default_infobar_overlay_request_config.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/infobars/modals/infobar_modal_constants.h"
#import "ios/chrome/browser/ui/infobars/modals/infobar_save_card_modal_consumer.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_modal/infobar_modal_overlay_coordinator+modal_configuration.h"
#import "ios/chrome/browser/overlays/ui_bundled/infobar_modal/save_card/save_card_infobar_modal_overlay_mediator_delegate.h"
#import "ios/chrome/browser/overlays/ui_bundled/overlay_request_mediator+subclassing.h"
#import "ui/gfx/image/image.h"

namespace {
// Time duration to wait before auto-closing modal in save card success
// confirmation state.
static constexpr base::TimeDelta kConfirmationStateDuration =
    base::Seconds(1.5);

// Time duration to wait before auto-closing modal in save card success
// confirmation state when VoiceOver is running. This is slightly greater than
// `kConfirmationStateDuration` to give VoiceOver enough time to read the
// required content.
// TODO(crbug.com/339887700): When VO is running do not use this and listen for
// VO announcement to finish before auto-closing the modal in confirmation
// state.
static constexpr base::TimeDelta kConfirmationStateDurationIfVoiceOverRunning =
    base::Seconds(5);

}  // namespace

@interface SaveCardInfobarModalOverlayMediator ()
// The save card modal config from the request.
@property(nonatomic, assign, readonly)
    DefaultInfobarOverlayRequestConfig* config;
@end

@implementation SaveCardInfobarModalOverlayMediator {
  // Timer that controls auto closure of modal in save card success confirmation
  // state.
  base::OneShotTimer _autoCloseConfirmationTimer;

  // Holds a value when loading and confirmation is enabled. `NO` indicates
  // modal is in loading state. `YES` indicates modal is in confirmation state.
  std::optional<BOOL> _creditCardUploadCompleted;

  BOOL _loadingDismissedByUser;
}

- (instancetype)initWithRequest:(OverlayRequest*)request {
  self = [super initWithRequest:request];
  if (self) {
    DefaultInfobarOverlayRequestConfig* config =
        request ? request->GetConfig<DefaultInfobarOverlayRequestConfig>()
                : nullptr;

    if (config) {
      autofill::AutofillSaveCardInfoBarDelegateIOS* delegate =
          static_cast<autofill::AutofillSaveCardInfoBarDelegateIOS*>(
              config->delegate());
      __weak __typeof__(self) weakSelf = self;
      delegate->SetCreditCardUploadCompletionCallback(
          base::BindOnce(^(BOOL card_saved) {
            [weakSelf creditCardUploadCompleted:card_saved];
          }));
    }
  }
  return self;
}

#pragma mark - Accessors

- (DefaultInfobarOverlayRequestConfig*)config {
  return self.request
             ? self.request->GetConfig<DefaultInfobarOverlayRequestConfig>()
             : nullptr;
}

// Returns the delegate attached to the config or `nullptr` if there is no
// config.
- (autofill::AutofillSaveCardInfoBarDelegateIOS*)saveCardDelegate {
  return self.config
             ? static_cast<autofill::AutofillSaveCardInfoBarDelegateIOS*>(
                   self.config->delegate())
             : nullptr;
}

- (void)setConsumer:(id<InfobarSaveCardModalConsumer>)consumer {
  if (_consumer == consumer)
    return;
  _consumer = consumer;

  DefaultInfobarOverlayRequestConfig* config = self.config;
  if (!_consumer || !config)
    return;

  autofill::AutofillSaveCardInfoBarDelegateIOS* delegate =
      self.saveCardDelegate;
  if (!delegate) {
    return;
  }

  delegate->SetInfobarIsPresenting(YES);

  InfoBarIOS* infobar = GetOverlayRequestInfobar(self.request);
  NSString* cardNumber = [NSString
      stringWithFormat:@"•••• %@", base::SysUTF16ToNSString(
                                       delegate->card_last_four_digits())];

  // Only allow editing if the card will be uploaded and it hasn't been
  // previously saved.
  BOOL supportsEditing = delegate->is_for_upload() && !infobar->accepted();

  // Convert gfx::Image to UIImage. The NSDictionary below doesn't support nil,
  // so NSNull must be used.
  const gfx::Image& avatarGfx = delegate->displayed_target_account_avatar();
  NSObject* avatar =
      avatarGfx.IsEmpty() ? [NSNull null] : avatarGfx.ToUIImage();

  NSDictionary* prefs = @{
    kCardholderNamePrefKey :
        base::SysUTF16ToNSString(delegate->cardholder_name()),
    kCardIssuerIconNamePrefKey : NativeImage(delegate->issuer_icon_id()),
    kCardNumberPrefKey : cardNumber,
    kExpirationMonthPrefKey :
        base::SysUTF16ToNSString(delegate->expiration_date_month()),
    kExpirationYearPrefKey :
        base::SysUTF16ToNSString(delegate->expiration_date_year()),
    kLegalMessagesPrefKey : [self legalMessages],
    kCurrentCardSaveAcceptedPrefKey : @(infobar->accepted()),
    kSupportsEditingPrefKey : @(supportsEditing),
    kDisplayedTargetAccountEmailPrefKey :
        base::SysUTF16ToNSString(delegate->displayed_target_account_email()),
    kDisplayedTargetAccountAvatarPrefKey : avatar,
  };
  [_consumer setupModalViewControllerWithPrefs:prefs];

  if (delegate->is_for_upload() && infobar->accepted() &&
      base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableSaveCardLoadingAndConfirmation)) {
    // If the infobar has been accepted and the card upload is in progress or
    // complete, display the appropriate progress state (loading or
    // confirmation).
    [self.consumer
        showProgressWithUploadCompleted:delegate->IsCreditCardUploadComplete()];
  }
}

#pragma mark - Public

- (void)creditCardUploadCompleted:(BOOL)card_saved {
  if (base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableSaveCardLoadingAndConfirmation)) {
    if (!_loadingDismissedByUser) {
      autofill::autofill_metrics::LogCreditCardUploadLoadingViewResultMetric(
          autofill::autofill_metrics::SaveCardPromptResult::kNotInteracted);
    }
    if (card_saved) {
      autofill::autofill_metrics::
          LogCreditCardUploadConfirmationViewShownMetric(
              /*is_shown=*/true, /*is_card_uploaded=*/true);

      _creditCardUploadCompleted = YES;
      [self.consumer showProgressWithUploadCompleted:YES];

      // Auto close modal after showing successful card save confirmation.
      __weak __typeof(self) weakSelf = self;
      _autoCloseConfirmationTimer.Start(
          FROM_HERE,
          UIAccessibilityIsVoiceOverRunning()
              ? kConfirmationStateDurationIfVoiceOverRunning
              : kConfirmationStateDuration,
          base::BindOnce(^{
            [weakSelf dimissConfirmationStateOnTimeout];
          }));
    } else {
      // On card save failure, this modal is dimissed and user is shown an error
      // dialog triggered from IOSChromePaymentsAutofillClient.
      [self dismissOverlay];
    }
  }
}

#pragma mark - OverlayRequestMediator

+ (const OverlayRequestSupport*)requestSupport {
  return DefaultInfobarOverlayRequestConfig::RequestSupport();
}

#pragma mark - InfobarSaveCardModalDelegate

- (void)saveCardWithCardholderName:(NSString*)cardholderName
                   expirationMonth:(NSString*)month
                    expirationYear:(NSString*)year {
  autofill::AutofillSaveCardInfoBarDelegateIOS* delegate =
      self.saveCardDelegate;
  InfoBarIOS* infobar = GetOverlayRequestInfobar(self.request);

  infobar->set_accepted(delegate->UpdateAndAccept(
      base::SysNSStringToUTF16(cardholderName), base::SysNSStringToUTF16(month),
      base::SysNSStringToUTF16(year)));

  if (base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableSaveCardLoadingAndConfirmation)) {
    autofill::autofill_metrics::LogCreditCardUploadLoadingViewShownMetric(
        /*is_shown=*/true);
    _creditCardUploadCompleted = NO;
    [self.consumer showProgressWithUploadCompleted:NO];
  } else {
    autofill::autofill_metrics::LogCreditCardUploadLoadingViewShownMetric(
        /*is_shown=*/false);
    [self dismissOverlay];
  }
}

- (void)dismissModalAndOpenURL:(const GURL&)linkURL {
  [self.save_card_delegate pendingURLToLoad:linkURL];
  [self dismissOverlay];
}

- (void)dimissConfirmationStateOnTimeout {
  [self dismissOverlay];
  [self onConfirmationClosedWithAutoClose:YES];
}

- (void)dismissInfobarModal:(id)infobarModal {
  base::RecordAction(base::UserMetricsAction(kInfobarModalCancelButtonTapped));
  [self dismissOverlay];

  // When loading and confirmation feature is enabled and credit card upload is
  // completed, modal would be showing a success confirmation and value of
  // `_creditCardUploadCompleted` would be `YES`. Modal getting closed from here
  // means user dismissed it using the close button.
  if (_creditCardUploadCompleted.has_value() &&
      base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableSaveCardLoadingAndConfirmation)) {
    if (_creditCardUploadCompleted.value()) {
      [self onConfirmationClosedWithAutoClose:NO];
    } else {
      _loadingDismissedByUser = YES;
      autofill::autofill_metrics::LogCreditCardUploadLoadingViewResultMetric(
          autofill::autofill_metrics::SaveCardPromptResult::kClosed);
    }
  }
}

#pragma mark - Private

// Returns an array of UI SaveCardMessageWithLinks model objects.
- (NSMutableArray<SaveCardMessageWithLinks*>*)legalMessages {
  autofill::AutofillSaveCardInfoBarDelegateIOS* delegate =
      self.saveCardDelegate;
  // Only display legal Messages if the card is being uploaded and there are
  // any.
  if (delegate->is_for_upload() && !delegate->legal_message_lines().empty()) {
    return
        [SaveCardMessageWithLinks convertFrom:delegate->legal_message_lines()];
  }
  return [[NSMutableArray alloc] init];
}

// Called when modal gets closed in confirmation state. Logs how the modal got
// closed and calls `AutofillSaveCardInfoBarDelegateIOS::OnConfirmationClosed`.
- (void)onConfirmationClosedWithAutoClose:(BOOL)autoClosed {
  autofill::autofill_metrics::LogCreditCardUploadConfirmationViewResultMetric(
      autoClosed
          ? autofill::autofill_metrics::SaveCardPromptResult::kNotInteracted
          : autofill::autofill_metrics::SaveCardPromptResult::kClosed,
      /*is_card_uploaded=*/true);
  _autoCloseConfirmationTimer.Stop();
  if (!self.saveCardDelegate) {
    return;
  }
  self.saveCardDelegate->OnConfirmationClosed();
}

- (void)dismissOverlay {
  if (self.saveCardDelegate) {
    self.saveCardDelegate->SetCreditCardUploadCompletionCallback(
        base::NullCallback());
    self.saveCardDelegate->SetInfobarIsPresenting(NO);
  }
  [super dismissOverlay];
}

@end