chromium/ios/chrome/browser/autofill/ui_bundled/bottom_sheet/virtual_card_enrollment_bottom_sheet_mediator.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_mediator.h"

#import <optional>

#import "base/feature_list.h"
#import "base/scoped_observation.h"
#import "base/strings/sys_string_conversions.h"
#import "components/autofill/core/browser/metrics/payments/virtual_card_enrollment_metrics.h"
#import "components/autofill/core/browser/payments/virtual_card_enroll_metrics_logger.h"
#import "components/autofill/core/browser/ui/payments/virtual_card_enroll_ui_model.h"
#import "components/autofill/core/common/autofill_payments_features.h"
#import "ios/chrome/browser/autofill/model/credit_card/credit_card_data.h"
#import "ios/chrome/browser/autofill/ui_bundled/bottom_sheet/virtual_card_enrollment_bottom_sheet_data.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ui/base/l10n/l10n_util.h"
#import "ui/gfx/image/image_skia_util_ios.h"

namespace {

// Bridges the C++ Observer interface to the Objective-C mediator. Uses scoped
// observation to add and remove itself as the observer of the
// VirtualCardEnrollUiModel.
class UiModelObserverBridge
    : public autofill::VirtualCardEnrollUiModel::Observer {
 public:
  UiModelObserverBridge(
      autofill::VirtualCardEnrollUiModel* model,
      __weak VirtualCardEnrollmentBottomSheetMediator* mediator)
      : mediator_(mediator) {
    scoped_model_observation_.Observe(model);
  }
  ~UiModelObserverBridge() override = default;
  void OnEnrollmentProgressChanged(
      autofill::VirtualCardEnrollUiModel::EnrollmentProgress
          enrollment_progress) override {
    [mediator_ modelDidChangeEnrollmentProgress:enrollment_progress];
  }

 private:
  __weak VirtualCardEnrollmentBottomSheetMediator* mediator_;
  base::ScopedObservation<autofill::VirtualCardEnrollUiModel,
                          UiModelObserverBridge>
      scoped_model_observation_{this};
};

// The delay between showing the confirmation and dismissing the virtual card
// enrollment prompt.
const base::TimeDelta kConfirmationDismissDelay = base::Seconds(1.5);

}  // namespace

@interface VirtualCardEnrollmentBottomSheetMediator () {
  VirtualCardEnrollmentBottomSheetData* _bottomSheetData;

  // The following members will be destroyed in reverse order. So that
  // the C++ observer object is destroyed, before the model is destroyed.
  std::unique_ptr<autofill::VirtualCardEnrollUiModel> _model;
  std::unique_ptr<UiModelObserverBridge> _uiModelObserverBridge;

  std::optional<autofill::VirtualCardEnrollmentCallbacks> _callbacks;
  __weak id<BrowserCoordinatorCommands> _browserCoordinatorHandler;
}

@end

@implementation VirtualCardEnrollmentBottomSheetMediator

- (instancetype)initWithUIModel:
                    (std::unique_ptr<autofill::VirtualCardEnrollUiModel>)model
                      callbacks:
                          (autofill::VirtualCardEnrollmentCallbacks)callbacks
      browserCoordinatorHandler:
          (id<BrowserCoordinatorCommands>)browserCoordinatorHandler {
  self = [super init];
  if (self) {
    UIImage* icon = nil;
    bool cardArtAvailable = model->enrollment_fields().card_art_image;
    if (cardArtAvailable) {
      icon = UIImageFromImageSkia(*model->enrollment_fields().card_art_image);
    }
    autofill::VirtualCardEnrollMetricsLogger::OnCardArtAvailable(
        cardArtAvailable,
        model->enrollment_fields().virtual_card_enrollment_source);
    CreditCardData* creditCard = [[CreditCardData alloc]
        initWithCreditCard:model->enrollment_fields().credit_card
                      icon:icon];
    _bottomSheetData = [[VirtualCardEnrollmentBottomSheetData alloc]
             initWithCreditCard:creditCard
                          title:base::SysUTF16ToNSString(model->window_title())
             explanatoryMessage:base::SysUTF16ToNSString(
                                    model->explanatory_message())
               acceptActionText:base::SysUTF16ToNSString(
                                    model->accept_action_text())
               cancelActionText:base::SysUTF16ToNSString(
                                    model->cancel_action_text())
              learnMoreLinkText:base::SysUTF16ToNSString(
                                    model->learn_more_link_text())
        googleLegalMessageLines:[SaveCardMessageWithLinks
                                    convertFrom:model->enrollment_fields()
                                                    .google_legal_message]
        issuerLegalMessageLines:[SaveCardMessageWithLinks
                                    convertFrom:model->enrollment_fields()
                                                    .issuer_legal_message]];
    _model = std::move(model);
    _callbacks = std::move(callbacks);
    _browserCoordinatorHandler = browserCoordinatorHandler;
    if (base::FeatureList::IsEnabled(
            autofill::features::
                kAutofillEnableVcnEnrollLoadingAndConfirmation)) {
      _uiModelObserverBridge =
          std::make_unique<UiModelObserverBridge>(_model.get(), self);
    }
  }
  return self;
}

- (void)setConsumer:(id<VirtualCardEnrollmentBottomSheetConsumer>)consumer {
  _consumer = consumer;
  [self.consumer setCardData:_bottomSheetData];
  autofill::VirtualCardEnrollMetricsLogger::OnShown(
      _model->enrollment_fields().virtual_card_enrollment_source,
      /*is_reshow=*/false);
}

#pragma mark VirtualCardEnrollmentBottomSheetMutator

- (void)didAccept {
  CHECK(_callbacks) << "Callbacks are not set. Callbacks should have been set "
                       "and called only once.";
  _callbacks->OnAccepted();
  _callbacks.reset();
  [self logResultMetric:autofill::VirtualCardEnrollmentBubbleResult::
                            VIRTUAL_CARD_ENROLLMENT_BUBBLE_ACCEPTED];
  if (base::FeatureList::IsEnabled(
          autofill::features::kAutofillEnableVcnEnrollLoadingAndConfirmation)) {
    [_consumer showLoadingState];
    autofill::LogVirtualCardEnrollmentLoadingViewShown(/*is_shown=*/true);
    return;
  }
  autofill::LogVirtualCardEnrollmentLoadingViewShown(/*is_shown=*/false);
  [_browserCoordinatorHandler dismissVirtualCardEnrollmentBottomSheet];
}

- (void)didCancel {
  CHECK(_callbacks) << "Callbacks are not set. Callbacks should have been set "
                       "and called only once.";
  _callbacks->OnDeclined();
  _callbacks.reset();
  [self logResultMetric:autofill::VirtualCardEnrollmentBubbleResult::
                            VIRTUAL_CARD_ENROLLMENT_BUBBLE_CANCELLED];
  [_browserCoordinatorHandler dismissVirtualCardEnrollmentBottomSheet];
}

#pragma mark - VirtualCardEnrollUiModel Observer

- (void)modelDidChangeEnrollmentProgress:
    (autofill::VirtualCardEnrollUiModel::EnrollmentProgress)enrollmentProgress {
  switch (enrollmentProgress) {
    case autofill::VirtualCardEnrollUiModel::EnrollmentProgress::kEnrolled: {
      [self.consumer showConfirmationState];
      autofill::LogVirtualCardEnrollmentConfirmationViewShown(
          /*is_shown=*/true,
          /*is_card_enrolled=*/true);
      __weak __typeof__(self) weakSelf = self;
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE, base::BindOnce(^{
            [weakSelf onFinishedConfirmationDelay];
          }),
          kConfirmationDismissDelay);
      break;
    }
    case autofill::VirtualCardEnrollUiModel::EnrollmentProgress::kFailed:
      // Dismiss the virtual card enrollment bottom sheet. Failure messages are
      // expected to be initiated by the IOSChromePaymentsAutofillClient.
      [_browserCoordinatorHandler dismissVirtualCardEnrollmentBottomSheet];
      autofill::LogVirtualCardEnrollmentConfirmationViewShown(
          /*is_shown=*/true,
          /*is_card_enrolled=*/false);
      break;
    case autofill::VirtualCardEnrollUiModel::EnrollmentProgress::kOffered:
      // The enrollment progress is set by IOSChromePaymentsAutofillClient to
      // either kEnrolled or kFailed and cannot transition back to kOffered.
      NOTREACHED();
  }
}

#pragma mark - Private

// Logs the result metric attaching additional parameters from the model.
- (void)logResultMetric:(autofill::VirtualCardEnrollmentBubbleResult)result {
  autofill::VirtualCardEnrollMetricsLogger::OnDismissed(
      result, _model->enrollment_fields().virtual_card_enrollment_source,
      /*is_reshow=*/false, _model->enrollment_fields().previously_declined);
}

// Handles dismissal after confirmation was shown with a delay.
- (void)onFinishedConfirmationDelay {
  [_browserCoordinatorHandler dismissVirtualCardEnrollmentBottomSheet];
}

@end