chromium/ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_coordinator.mm

// Copyright 2014 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/form_input_accessory/form_input_accessory_coordinator.h"

#import <vector>

#import "base/apple/foundation_util.h"
#import "base/feature_list.h"
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/not_fatal_until.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/task/sequenced_task_runner.h"
#import "base/time/time.h"
#import "components/autofill/core/browser/payments/payments_service_url.h"
#import "components/autofill/core/browser/personal_data_manager.h"
#import "components/autofill/core/common/autofill_features.h"
#import "components/autofill/ios/browser/form_suggestion.h"
#import "components/autofill/ios/form_util/form_activity_params.h"
#import "components/feature_engagement/public/feature_constants.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/manage_passwords_referrer.h"
#import "components/password_manager/core/browser/password_ui_utils.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/password_manager/ios/password_generation_provider.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/autofill/model/personal_data_manager_factory.h"
#import "ios/chrome/browser/autofill/ui_bundled/autofill_credit_card_util.h"
#import "ios/chrome/browser/autofill/ui_bundled/branding/branding_coordinator.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_mediator.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_mediator_handler.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_view_controller.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/form_input_accessory_view_controller_delegate.h"
#import "ios/chrome/browser/autofill/ui_bundled/form_input_accessory/scoped_form_input_accessory_reauth_module_override.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/address_coordinator.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/card_coordinator.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/expanded_manual_fill_coordinator.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/fallback_view_controller.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_all_password_coordinator.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_all_password_coordinator_delegate.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_injection_handler.h"
#import "ios/chrome/browser/autofill/ui_bundled/manual_fill/manual_fill_password_coordinator.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_constants.h"
#import "ios/chrome/browser/bubble/ui_bundled/bubble_view_controller_presenter.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_account_password_store_factory.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/passwords/model/password_tab_helper.h"
#import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h"
#import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_util.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h"
#import "ios/chrome/browser/shared/public/commands/security_alert_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/grit/ios_branded_strings.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/web/public/web_state.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util_mac.h"

namespace {
// Delay between the time the view is shown, and the time the suggestion label
// is highlighted.
constexpr base::TimeDelta kAutofillSuggestionHighlightDelay = base::Seconds(1);

// Delay between the time the suggestion label is highlighted, and the time the
// autofill suggestion tip is shown.
constexpr base::TimeDelta kAutofillSuggestionTipDelay = base::Seconds(0.5);

// Additional vertical offset for the IPH, so that it doesn't appear below the
// Autofill strip at the top of the keyboard.
const CGFloat kIPHVerticalOffset = -5;

// Return the feature corresponding to the `feature_for_iph` enum.
const base::Feature* FetchIPHFeatureFromEnum(
    SuggestionFeatureForIPH feature_for_iph) {
  switch (feature_for_iph) {
    case SuggestionFeatureForIPH::kAutofillExternalAccountProfile:
      return &feature_engagement::
          kIPHAutofillExternalAccountProfileSuggestionFeature;
    case SuggestionFeatureForIPH::kPlusAddressCreation:
      return &feature_engagement::kIPHPlusAddressCreateSuggestionFeature;
    case SuggestionFeatureForIPH::kUnknown:
      NOTREACHED();
  }
}

}  // namespace

@interface FormInputAccessoryCoordinator () <
    AddressCoordinatorDelegate,
    CardCoordinatorDelegate,
    FormInputAccessoryMediatorHandler,
    FormInputAccessoryViewControllerDelegate,
    ManualFillAllPasswordCoordinatorDelegate,
    PasswordCoordinatorDelegate,
    ExpandedManualFillCoordinatorDelegate,
    SecurityAlertCommands>

// Coordinator in charge of the presenting password autofill options as a modal.
@property(nonatomic, strong)
    ManualFillAllPasswordCoordinator* allPasswordCoordinator;

// Coordinator in charge of the keyboard autofill branding.
@property(nonatomic, strong) BrandingCoordinator* brandingCoordinator;

// The Mediator for the input accessory view controller.
@property(nonatomic, strong)
    FormInputAccessoryMediator* formInputAccessoryMediator;

// The View Controller for the input accessory view.
@property(nonatomic, strong)
    FormInputAccessoryViewController* formInputAccessoryViewController;

// The object in charge of interacting with the web view. Used to fill the data
// in the forms.
@property(nonatomic, strong) ManualFillInjectionHandler* injectionHandler;

// Reauthentication Module used for re-authentication.
@property(nonatomic, strong) ReauthenticationModule* reauthenticationModule;

// Modal alert.
@property(nonatomic, strong) AlertCoordinator* alertCoordinator;

// Active Form Input View Controller.
@property(nonatomic, strong) UIViewController* formInputViewController;

// The browser state. May return null after the coordinator has been stopped
// (thus the returned value must be checked for null).
@property(nonatomic, readonly) ChromeBrowserState* browserState;

// Bubble view controller presenter for autofill suggestion tip.
@property(nonatomic, strong) BubbleViewControllerPresenter* bubblePresenter;

// UI tap recognizer used to dismiss bubble presenter.
@property(nonatomic, strong)
    UITapGestureRecognizer* formInputAccessoryTapRecognizer;

// The layout guide installed in the base view controller on which to anchor the
// potential IPH bubble.
@property(nonatomic, strong) UILayoutGuide* layoutGuide;

@end

@implementation FormInputAccessoryCoordinator

- (instancetype)initWithBaseViewController:(UIViewController*)viewController
                                   browser:(Browser*)browser {
  self = [super initWithBaseViewController:viewController browser:browser];
  if (self) {
    CommandDispatcher* dispatcher = browser->GetCommandDispatcher();
    [dispatcher startDispatchingToTarget:self
                             forProtocol:@protocol(SecurityAlertCommands)];

    _brandingCoordinator =
        [[BrandingCoordinator alloc] initWithBaseViewController:viewController
                                                        browser:browser];
    _reauthenticationModule = [[ReauthenticationModule alloc] init];
  }
  return self;
}

- (void)start {
  [self.brandingCoordinator start];
  self.formInputAccessoryViewController =
      [[FormInputAccessoryViewController alloc]
          initWithFormInputAccessoryViewControllerDelegate:self];
  self.formInputAccessoryViewController.brandingViewController =
      self.brandingCoordinator.viewController;

  LayoutGuideCenter* layoutGuideCenter =
      LayoutGuideCenterForBrowser(self.browser);
  self.formInputAccessoryViewController.layoutGuideCenter = layoutGuideCenter;

  DCHECK(self.browserState);
  auto profilePasswordStore =
      IOSChromeProfilePasswordStoreFactory::GetForBrowserState(
          self.browserState, ServiceAccessType::EXPLICIT_ACCESS);
  auto accountPasswordStore =
      IOSChromeAccountPasswordStoreFactory::GetForBrowserState(
          self.browserState, ServiceAccessType::EXPLICIT_ACCESS);

  // There is no personal data manager in OTR (incognito). Get the original
  // one for manual fallback.
  autofill::PersonalDataManager* personalDataManager =
      autofill::PersonalDataManagerFactory::GetForBrowserState(
          self.browserState->GetOriginalChromeBrowserState());

  __weak id<SecurityAlertCommands> securityAlertHandler = HandlerForProtocol(
      self.browser->GetCommandDispatcher(), SecurityAlertCommands);
  self.formInputAccessoryMediator = [[FormInputAccessoryMediator alloc]
            initWithConsumer:self.formInputAccessoryViewController
                     handler:self
                webStateList:self.browser->GetWebStateList()
         personalDataManager:personalDataManager
        profilePasswordStore:profilePasswordStore
        accountPasswordStore:accountPasswordStore
        securityAlertHandler:securityAlertHandler
      reauthenticationModule:self.reauthenticationModule
           engagementTracker:feature_engagement::TrackerFactory::
                                 GetForBrowserState(
                                     self.browser->GetBrowserState())];
  self.formInputAccessoryViewController.formSuggestionClient =
      self.formInputAccessoryMediator;

  self.layoutGuide =
      [layoutGuideCenter makeLayoutGuideNamed:kAutofillFirstSuggestionGuide];
  [self.baseViewController.view addLayoutGuide:self.layoutGuide];

  _injectionHandler = [[ManualFillInjectionHandler alloc]
        initWithWebStateList:self.browser->GetWebStateList()
        securityAlertHandler:securityAlertHandler
      reauthenticationModule:_reauthenticationModule
        formSuggestionClient:self.formInputAccessoryMediator];
}

- (void)stop {
  [self clearPresentedState];
  [self.formInputAccessoryTapRecognizer.view
      removeGestureRecognizer:self.formInputAccessoryTapRecognizer];
  self.formInputAccessoryViewController = nil;
  self.formInputViewController = nil;
  [GetFirstResponder() reloadInputViews];

  [self.formInputAccessoryMediator disconnect];
  self.formInputAccessoryMediator = nil;

  [self.brandingCoordinator stop];
  self.brandingCoordinator = nil;
  [self.layoutGuide.owningView removeLayoutGuide:self.layoutGuide];
  self.layoutGuide = nil;
}

- (void)reset {
  [self stopChildren];
  [self resetInputViews];
  [GetFirstResponder() reloadInputViews];
}

#pragma mark - Presenting Children

- (void)clearPresentedState {
  [self stopChildren];

  [self stopManualFillAllPasswordCoordinator];

  [self dismissAlertCoordinator];
}

- (void)stopChildren {
  self.formInputAccessoryMediator.formInputInteractionDelegate = nil;
  for (ChromeCoordinator* coordinator in self.childCoordinators) {
    [coordinator stop];
  }
  [self.childCoordinators removeAllObjects];
}

// Starts the password coordinator and displays its view controller.
- (void)startPasswordsFromButton:(UIButton*)button
        invokedOnObfuscatedField:(BOOL)invokedOnObfuscatedField {
  web::WebState* activeWebState = [self activeWebState];
  if (!activeWebState) {
    return;
  }

  const GURL& URL = activeWebState->GetLastCommittedURL();

  ManualFillPasswordCoordinator* passwordCoordinator =
      [[ManualFillPasswordCoordinator alloc]
             initWithBaseViewController:self.baseViewController
                                browser:self.browser
          manualFillPlusAddressMediator:nil
                                    URL:URL
                       injectionHandler:self.injectionHandler
               invokedOnObfuscatedField:invokedOnObfuscatedField
                 showAutofillFormButton:NO];

  passwordCoordinator.delegate = self;
  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    [passwordCoordinator presentFromButton:button];
  } else {
    self.formInputViewController = passwordCoordinator.viewController;
    [GetFirstResponder() reloadInputViews];
  }

  [self.childCoordinators addObject:passwordCoordinator];
}

// Starts the card coordinator and displays its view controller.
- (void)startCardsFromButton:(UIButton*)button {
  CardCoordinator* cardCoordinator = [[CardCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.browser
                injectionHandler:self.injectionHandler
          reauthenticationModule:self.reauthenticationModule
          showAutofillFormButton:NO];
  cardCoordinator.delegate = self;
  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    [cardCoordinator presentFromButton:button];
  } else {
    self.formInputViewController = cardCoordinator.viewController;
    [GetFirstResponder() reloadInputViews];
  }

  [self.childCoordinators addObject:cardCoordinator];
}

// Starts the address coordinator and displays its view controller.
- (void)startAddressFromButton:(UIButton*)button {
  AddressCoordinator* addressCoordinator = [[AddressCoordinator alloc]
         initWithBaseViewController:self.baseViewController
                            browser:self.browser
      manualFillPlusAddressMediator:nil
                   injectionHandler:self.injectionHandler
             showAutofillFormButton:NO];
  addressCoordinator.delegate = self;
  if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
    [addressCoordinator presentFromButton:button];
  } else {
    self.formInputViewController = addressCoordinator.viewController;
    [GetFirstResponder() reloadInputViews];
  }

  [self.childCoordinators addObject:addressCoordinator];
}

// Starts the expanded manual fill coordinator and displays its view controller.
- (void)startManualFillForDataType:(manual_fill::ManualFillDataType)dataType
          invokedOnObfuscatedField:(BOOL)invokedOnObfuscatedField {
  ExpandedManualFillCoordinator* expandedManualFillCoordinator =
      [[ExpandedManualFillCoordinator alloc]
          initWithBaseViewController:self.baseViewController
                             browser:self.browser
                         forDataType:dataType
              reauthenticationModule:self.reauthenticationModule];

  expandedManualFillCoordinator.injectionHandler = self.injectionHandler;
  expandedManualFillCoordinator.invokedOnObfuscatedField =
      invokedOnObfuscatedField;
  expandedManualFillCoordinator.delegate = self;
  self.formInputAccessoryMediator.formInputInteractionDelegate =
      expandedManualFillCoordinator;
  [expandedManualFillCoordinator start];

  self.formInputViewController = expandedManualFillCoordinator.viewController;
  [GetFirstResponder() reloadInputViews];

  [self.childCoordinators addObject:expandedManualFillCoordinator];
}

#pragma mark - FormInputAccessoryMediatorHandler

- (void)resetFormInputView {
  [self reset];
}

- (void)notifyAutofillSuggestionWithIPHSelectedFor:
    (SuggestionFeatureForIPH)featureForIPH {
  // The engagement tracker can change during testing (in feature engagement app
  // interface), therefore we retrive it here instead of storing it in the
  // mediator.
  feature_engagement::Tracker* tracker = self.featureEngagementTracker;
  if (tracker) {
    switch (featureForIPH) {
      case SuggestionFeatureForIPH::kAutofillExternalAccountProfile:
        tracker->NotifyEvent(
            "autofill_external_account_profile_suggestion_accepted");
        break;
      case SuggestionFeatureForIPH::kPlusAddressCreation:
        tracker->NotifyEvent("plus_address_create_suggestion_feature_used");
        break;
      case SuggestionFeatureForIPH::kUnknown:
        NOTREACHED();
    }
  }
}

- (void)showAutofillSuggestionIPHIfNeededFor:
    (SuggestionFeatureForIPH)featureForIPH {
  if (self.bubblePresenter) {
    // Already showing a bubble.
    return;
  }

  __weak __typeof(self) weakSelf = self;
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, base::BindOnce(^{
        [weakSelf tryPresentingBubbleFor:featureForIPH];
      }),
      kAutofillSuggestionHighlightDelay);
}

- (void)startManualFillForDataType:(manual_fill::ManualFillDataType)dataType {
  // Currently only payment methods form input accessory may start manual fill
  // directly.
  CHECK_EQ(dataType, manual_fill::ManualFillDataType::kPaymentMethod);
  [self startManualFillForDataType:dataType invokedOnObfuscatedField:NO];
}

#pragma mark - FormInputAccessoryViewControllerDelegate

- (void)formInputAccessoryViewController:
            (FormInputAccessoryViewController*)formInputAccessoryViewController
                  didPressKeyboardButton:(UIButton*)keyboardButton {
  [self reset];
}

- (void)formInputAccessoryViewController:
            (FormInputAccessoryViewController*)formInputAccessoryViewController
                   didPressAccountButton:(UIButton*)accountButton {
  [self stopChildren];
  [self startAddressFromButton:accountButton];
  [self updateKeyboardAccessoryForManualFilling];
}

- (void)formInputAccessoryViewController:
            (FormInputAccessoryViewController*)formInputAccessoryViewController
                didPressCreditCardButton:(UIButton*)creditCardButton {
  [self stopChildren];
  [self startCardsFromButton:creditCardButton];
  [self updateKeyboardAccessoryForManualFilling];
}

- (void)formInputAccessoryViewController:
            (FormInputAccessoryViewController*)formInputAccessoryViewController
                  didPressPasswordButton:(UIButton*)passwordButton {
  [self stopChildren];
  BOOL invokedOnObfuscatedField =
      [self.formInputAccessoryMediator lastFocusedFieldWasObfuscated];
  [self startPasswordsFromButton:passwordButton
        invokedOnObfuscatedField:invokedOnObfuscatedField];
  [self updateKeyboardAccessoryForManualFilling];
}

- (void)formInputAccessoryViewController:
            (FormInputAccessoryViewController*)formInputAccessoryViewController
                didPressManualFillButton:(UIButton*)manualFillButton
                             forDataType:
                                 (manual_fill::ManualFillDataType)dataType {
  CHECK(IsKeyboardAccessoryUpgradeEnabled());

  [self stopChildren];
  BOOL invokedOnObfuscatedField =
      [self.formInputAccessoryMediator lastFocusedFieldWasObfuscated];
  [self startManualFillForDataType:dataType
          invokedOnObfuscatedField:invokedOnObfuscatedField];

  // TODO(crbug.com/326265397): Hide the keyboard accessory and remove line
  // below.
  [self updateKeyboardAccessoryForManualFilling];
}

- (void)formInputAccessoryViewController:
            (FormInputAccessoryViewController*)formInputAccessoryViewController
            didTapFormInputAccessoryView:(UIView*)formInputAccessoryView {
  [self dismissBubble];
}

- (void)formInputAccessoryViewControllerReset:
    (FormInputAccessoryViewController*)formInputAccessoryViewController {
  CHECK_EQ(self.formInputAccessoryViewController,
           formInputAccessoryViewController);
  [self resetInputViews];
}

#pragma mark - FallbackCoordinatorDelegate

- (void)fallbackCoordinatorDidDismissPopover:
    (FallbackCoordinator*)fallbackCoordinator {
  [self reset];
}

#pragma mark - PasswordCoordinatorDelegate

- (void)openPasswordManager {
  [self reset];
  [self.navigator openPasswordManager];

  UMA_HISTOGRAM_ENUMERATION(
      "PasswordManager.ManagePasswordsReferrer",
      password_manager::ManagePasswordsReferrer::kPasswordsAccessorySheet);
  base::RecordAction(
      base::UserMetricsAction("MobileKeyboardAccessoryOpenPasswordManager"));
}

- (void)openPasswordSettings {
  [self reset];
  [self.navigator openPasswordSettings];
}

- (void)openAllPasswordsPicker {
  [self reset];
  [self showConfirmationDialogToUseOtherPassword];
}

- (void)openPasswordSuggestion {
  [self reset];
  if (![self.injectionHandler canUserInjectInPasswordField:YES
                                             requiresHTTPS:NO]) {
    return;
  }

  web::WebState* activeWebState = [self activeWebState];
  if (!activeWebState) {
    return;
  }

  id<PasswordGenerationProvider> generationProvider =
      PasswordTabHelper::FromWebState(activeWebState)
          ->GetPasswordGenerationProvider();
  [generationProvider triggerPasswordGeneration];
}

- (void)openPasswordDetailsInEditModeForCredential:
    (password_manager::CredentialUIEntry)credential {
  [self reset];
  [self dispatchCommandToEditPassword:credential];
}

#pragma mark - ManualFillAllPasswordCoordinatorDelegate

- (void)manualFillAllPasswordCoordinatorWantsToBeDismissed:
    (ManualFillAllPasswordCoordinator*)coordinator {
  [self stopManualFillAllPasswordCoordinator];
}

- (void)manualFillAllPasswordCoordinator:
            (ManualFillAllPasswordCoordinator*)coordinator
    didTriggerOpenPasswordDetailsInEditMode:
        (password_manager::CredentialUIEntry)credential {
  [self stopManualFillAllPasswordCoordinator];
  [self dispatchCommandToEditPassword:credential];
}

#pragma mark - CardCoordinatorDelegate

- (void)cardCoordinatorDidTriggerOpenCardSettings:
    (CardCoordinator*)cardCoordinator {
  [self reset];
  [self.navigator openCreditCardSettings];
}

- (void)cardCoordinatorDidTriggerOpenAddCreditCard:
    (CardCoordinator*)cardCoordinator {
  [self reset];
  CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
  id<BrowserCoordinatorCommands> handler =
      HandlerForProtocol(dispatcher, BrowserCoordinatorCommands);
  [handler showAddCreditCard];
}

- (void)cardCoordinator:(CardCoordinator*)cardCoordinator
    didTriggerOpenCardDetails:(const autofill::CreditCard*)card
                   inEditMode:(BOOL)editMode {
  [self reset];

  // Check if the card should be edited from the Payments web page.
  if (editMode &&
      [AutofillCreditCardUtil shouldEditCardFromPaymentsWebPage:card]) {
    GURL paymentsURL =
        autofill::payments::GetManageInstrumentUrl(card->instrument_id());
    OpenNewTabCommand* command =
        [OpenNewTabCommand commandWithURLFromChrome:paymentsURL];
    id<ApplicationCommands> applicationHandler = HandlerForProtocol(
        self.browser->GetCommandDispatcher(), ApplicationCommands);
    [applicationHandler openURLInNewTab:command];

    return;
  }

  CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
  id<SettingsCommands> settingsHandler =
      HandlerForProtocol(dispatcher, SettingsCommands);
  [settingsHandler showCreditCardDetails:card inEditMode:editMode];
}

#pragma mark - AddressCoordinatorDelegate

- (void)openAddressDetailsInEditMode:(const autofill::AutofillProfile*)address
               offerMigrateToAccount:(BOOL)offerMigrateToAccount {
  [self reset];
  CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
  id<SettingsCommands> settingsHandler =
      HandlerForProtocol(dispatcher, SettingsCommands);
  [settingsHandler showAddressDetails:address
                           inEditMode:YES
                offerMigrateToAccount:offerMigrateToAccount];
}

- (void)openAddressSettings {
  [self reset];
  [self.navigator openAddressSettings];
}

#pragma mark - ExpandedManualFillCoordinatorDelegate

- (void)stopExpandedManualFillCoordinator:
    (ExpandedManualFillCoordinator*)coordinator {
  [self reset];
}

#pragma mark - SecurityAlertCommands

- (void)presentSecurityWarningAlertWithText:(NSString*)body {
  NSString* alertTitle =
      l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_NOT_SECURE_TITLE);
  NSString* defaultActionTitle =
      l10n_util::GetNSString(IDS_IOS_MANUAL_FALLBACK_NOT_SECURE_OK_BUTTON);

  UIAlertController* alert =
      [UIAlertController alertControllerWithTitle:alertTitle
                                          message:body
                                   preferredStyle:UIAlertControllerStyleAlert];
  UIAlertAction* defaultAction =
      [UIAlertAction actionWithTitle:defaultActionTitle
                               style:UIAlertActionStyleDefault
                             handler:nil];
  [alert addAction:defaultAction];
  UIViewController* presenter = self.baseViewController;
  while (presenter.presentedViewController) {
    presenter = presenter.presentedViewController;
  }
  [presenter presentViewController:alert animated:YES completion:nil];
}

#pragma mark - CRWResponderInputView

- (UIView*)inputView {
  BOOL isIPad = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET;
  return isIPad ? nil : self.formInputViewController.view;
}

- (UIView*)inputAccessoryView {
  if (self.formInputAccessoryMediator.inputAccessoryViewActive) {
    return self.formInputAccessoryViewController.view;
  }
  return nil;
}

#pragma mark - Actions

- (void)tapInsideRecognized:(id)sender {
  [self dismissBubble];
}

#pragma mark - Private

// Returns the reauthentication module, which can be an override for testing
// purposes.
- (ReauthenticationModule*)reauthenticationModule {
  id<ReauthenticationProtocol> overrideModule =
      ScopedFormInputAccessoryReauthModuleOverride::Get();
  return overrideModule ? overrideModule : _reauthenticationModule;
}

// Returns the active web state. May return nil.
- (web::WebState*)activeWebState {
  web::WebState* webState =
      self.browser->GetWebStateList()->GetActiveWebState();
  if (!webState) {
    // TODO: b/40940511 - The web state should not be nil, but we have seen
    // cases of it being nil in the wild, so, for now, we handle the nil case
    // gracefully, but still dump the information we need to find the root
    // cause. This can be removed once the root cause has been fixed.
    base::debug::DumpWithoutCrashing();
  }
  return webState;
}

- (void)stopManualFillAllPasswordCoordinator {
  [self.allPasswordCoordinator stop];
  self.allPasswordCoordinator.manualFillAllPasswordCoordinatorDelegate = nil;
  self.allPasswordCoordinator = nil;
}

- (void)dismissAlertCoordinator {
  [self.alertCoordinator stop];
  self.alertCoordinator = nil;
}

- (ChromeBrowserState*)browserState {
  return self.browser ? self.browser->GetBrowserState() : nullptr;
}

- (feature_engagement::Tracker*)featureEngagementTracker {
  ChromeBrowserState* browserState = self.browserState;
  if (!browserState) {
    return nullptr;
  }
  feature_engagement::Tracker* tracker =
      feature_engagement::TrackerFactory::GetForBrowserState(browserState);
  CHECK(tracker);
  return tracker;
}

// Shows confirmation dialog before opening Other passwords.
- (void)showConfirmationDialogToUseOtherPassword {
  web::WebState* activeWebState = [self activeWebState];
  if (!activeWebState) {
    return;
  }

  const GURL& URL = activeWebState->GetLastCommittedURL();
  std::u16string origin = base::ASCIIToUTF16(
      password_manager::GetShownOrigin(url::Origin::Create(URL)));

  NSString* title = l10n_util::GetNSString(
      IDS_IOS_MANUAL_FALLBACK_SELECT_PASSWORD_DIALOG_TITLE);
  NSString* message = l10n_util::GetNSStringF(
      IDS_IOS_MANUAL_FALLBACK_SELECT_PASSWORD_DIALOG_MESSAGE, origin);

  self.alertCoordinator = [[AlertCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.browser
                           title:title
                         message:message];
  [self.childCoordinators addObject:self.alertCoordinator];

  __weak __typeof__(self) weakSelf = self;

  [self.alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_CANCEL)
                                   action:^{
                                     [weakSelf dismissAlertCoordinator];
                                   }
                                    style:UIAlertActionStyleCancel];

  NSString* actionTitle =
      l10n_util::GetNSString(IDS_IOS_CONFIRM_USING_OTHER_PASSWORD_CONTINUE);
  [self.alertCoordinator addItemWithTitle:actionTitle
                                   action:^{
                                     [weakSelf showAllPasswords];
                                     [weakSelf dismissAlertCoordinator];
                                   }
                                    style:UIAlertActionStyleDefault];

  [self.alertCoordinator start];
}

// Opens other passwords.
- (void)showAllPasswords {
  [self reset];
  // The old coordinator could still be alive at this point. Stop it and release
  // it before starting a new one. See crbug.com/40063966.
  [self stopManualFillAllPasswordCoordinator];

  self.allPasswordCoordinator = [[ManualFillAllPasswordCoordinator alloc]
      initWithBaseViewController:self.baseViewController
                         browser:self.browser
                injectionHandler:self.injectionHandler];
  self.allPasswordCoordinator.manualFillAllPasswordCoordinatorDelegate = self;
  [self.allPasswordCoordinator start];
}

// Returns a new bubble view controller presenter for password suggestion tip.
- (BubbleViewControllerPresenter*)newBubbleViewControllerPresenterFor:
    (SuggestionFeatureForIPH)featureForIPH {
  NSString* text = nil;
  NSString* voiceOverText = nil;
  switch (featureForIPH) {
    case SuggestionFeatureForIPH::kAutofillExternalAccountProfile:
      text = l10n_util::GetNSString(
          IDS_AUTOFILL_IPH_EXTERNAL_ACCOUNT_PROFILE_SUGGESTION);
      voiceOverText = l10n_util::GetNSString(
          IDS_AUTOFILL_IPH_EXTERNAL_ACCOUNT_PROFILE_SUGGESTION);
      break;
    case SuggestionFeatureForIPH::kPlusAddressCreation:
      text = l10n_util::GetNSString(IDS_PLUS_ADDRESS_CREATE_SUGGESTION_IPH_IOS);
      voiceOverText = l10n_util::GetNSString(
          IDS_PLUS_ADDRESS_CREATE_SUGGESTION_IPH_SCREENREADER_IOS);
      break;
    case SuggestionFeatureForIPH::kUnknown:
      NOTREACHED();
  }

  CHECK(text != nil);
  CHECK(voiceOverText != nil);

  // Prepare the main arguments for the BubbleViewControllerPresenter
  // initializer.
  // Prepare the dismissal callback.
  __weak __typeof(self) weakSelf = self;
  CallbackWithIPHDismissalReasonType dismissalCallback =
      ^(IPHDismissalReasonType IPHDismissalReasonType,
        feature_engagement::Tracker::SnoozeAction snoozeAction) {
        [weakSelf IPHDidDismissWithSnoozeAction:snoozeAction
                                     forFeature:featureForIPH];
      };

  // Create the BubbleViewControllerPresenter.
  BubbleViewControllerPresenter* bubbleViewControllerPresenter =
      [[BubbleViewControllerPresenter alloc]
               initWithText:text
                      title:nil
                      image:nil
             arrowDirection:BubbleArrowDirectionDown
                  alignment:BubbleAlignmentTopOrLeading
                 bubbleType:BubbleViewTypeWithClose
          dismissalCallback:dismissalCallback];
  bubbleViewControllerPresenter.voiceOverAnnouncement = voiceOverText;
  return bubbleViewControllerPresenter;
}

// Checks if the bubble should be presented and acts on it.
- (void)tryPresentingBubbleFor:(SuggestionFeatureForIPH)featureForIPH {
  BubbleViewControllerPresenter* bubblePresenter =
      [self newBubbleViewControllerPresenterFor:featureForIPH];

  // Get the anchor point for the bubble.
  CGRect anchorFrame = self.layoutGuide.layoutFrame;
  CGPoint anchorPoint =
      CGPointMake(CGRectGetMidX(anchorFrame),
                  CGRectGetMinY(anchorFrame) + kIPHVerticalOffset);

  // Discard if it doesn't fit in the view as it is currently shown.
  if (![bubblePresenter canPresentInView:self.baseViewController.view
                             anchorPoint:anchorPoint]) {
    return;
  }

  // Early return if the engagement tracker won't display the IPH.
  feature_engagement::Tracker* tracker = self.featureEngagementTracker;
  const base::Feature* feature = FetchIPHFeatureFromEnum(featureForIPH);
  if (!tracker || !tracker->ShouldTriggerHelpUI(*feature)) {
    return;
  }

  // Present the bubble after the delay.
  self.bubblePresenter = bubblePresenter;
  __weak __typeof(self) weakSelf = self;
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, base::BindOnce(^{
        [weakSelf presentBubbleAtAnchorPoint:anchorPoint];
      }),
      kAutofillSuggestionTipDelay);
}

- (void)IPHDidDismissWithSnoozeAction:
            (feature_engagement::Tracker::SnoozeAction)snoozeAction
                           forFeature:(SuggestionFeatureForIPH)featureForIPH {
  feature_engagement::Tracker* tracker = self.featureEngagementTracker;
  if (tracker) {
    const base::Feature* feature = FetchIPHFeatureFromEnum(featureForIPH);
    tracker->DismissedWithSnooze(*feature, snoozeAction);
  }
  self.bubblePresenter = nil;
}

// Actually presents the bubble.
- (void)presentBubbleAtAnchorPoint:(CGPoint)anchorPoint {
  [self.bubblePresenter presentInViewController:self.baseViewController
                                    anchorPoint:anchorPoint];
}

- (void)dismissBubble {
  [self.bubblePresenter dismissAnimated:YES];
  self.bubblePresenter = nil;
}

// Resets `formInputAccessoryViewController` and `formInputViewController` to
// their initial state.
- (void)resetInputViews {
  self.formInputAccessoryMediator.suggestionsEnabled = YES;
  [self.formInputAccessoryViewController reset];

  self.formInputViewController = nil;
}

// Updates the keyboard accessory to the state it should be in when a manual
// fill view is displayed.
- (void)updateKeyboardAccessoryForManualFilling {
  [self.formInputAccessoryViewController lockManualFallbackView];
  self.formInputAccessoryMediator.suggestionsEnabled = NO;
}

// Creates a SettingsCommend handler and uses it to dispatch a command to show
// the details of a password in edit mode.
- (void)dispatchCommandToEditPassword:
    (password_manager::CredentialUIEntry)credential {
  id<SettingsCommands> settingsHandler = HandlerForProtocol(
      self.browser->GetCommandDispatcher(), SettingsCommands);
  [settingsHandler showPasswordDetailsForCredential:credential
                                         inEditMode:YES
                                   showCancelButton:YES];
}

@end