chromium/ios/chrome/browser/passwords/ui_bundled/bottom_sheet/password_suggestion_bottom_sheet_coordinator.mm

// Copyright 2023 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/passwords/ui_bundled/bottom_sheet/password_suggestion_bottom_sheet_coordinator.h"

#import <optional>

#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "components/password_manager/core/browser/ui/saved_passwords_presenter.h"
#import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.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_controller_delegate.h"
#import "ios/chrome/browser/passwords/ui_bundled/bottom_sheet/password_suggestion_bottom_sheet_mediator.h"
#import "ios/chrome/browser/passwords/ui_bundled/bottom_sheet/password_suggestion_bottom_sheet_view_controller.h"
#import "ios/chrome/browser/passwords/ui_bundled/bottom_sheet/scoped_password_suggestion_bottom_sheet_reauth_module_override.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/browser_coordinator_commands.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/web/public/web_state.h"
#import "services/network/public/cpp/shared_url_loader_factory.h"

using PasswordSuggestionBottomSheetExitReason::kCouldNotPresent;
using PasswordSuggestionBottomSheetExitReason::kDismissal;
using PasswordSuggestionBottomSheetExitReason::kShowPasswordDetails;
using PasswordSuggestionBottomSheetExitReason::kShowPasswordManager;
using PasswordSuggestionBottomSheetExitReason::kUsePasswordSuggestion;

@interface PasswordSuggestionBottomSheetCoordinator () {
  // The password controller delegate used to open the password manager.
  id<PasswordControllerDelegate> _passwordControllerDelegate;

  // Currently in the process of dismissing the bottom sheet.
  bool _dismissing;
}

// This mediator is used to fetch data related to the bottom sheet.
@property(nonatomic, strong) PasswordSuggestionBottomSheetMediator* mediator;

// This view controller is used to display the bottom sheet.
@property(nonatomic, strong)
    PasswordSuggestionBottomSheetViewController* viewController;

// Module handling reauthentication before accessing sensitive data.
@property(nonatomic, strong) id<ReauthenticationProtocol> reauthModule;

@end

@implementation PasswordSuggestionBottomSheetCoordinator

- (instancetype)
    initWithBaseViewController:(UIViewController*)viewController
                       browser:(Browser*)browser
                        params:(const autofill::FormActivityParams&)params
                      delegate:(id<PasswordControllerDelegate>)delegate {
  self = [super initWithBaseViewController:viewController browser:browser];
  if (self) {
    _passwordControllerDelegate = delegate;
    _dismissing = NO;

    WebStateList* webStateList = browser->GetWebStateList();
    const GURL& URL = webStateList->GetActiveWebState()->GetLastCommittedURL();
    self.viewController = [[PasswordSuggestionBottomSheetViewController alloc]
        initWithHandler:self
                    URL:URL];

    ChromeBrowserState* browserState =
        browser->GetBrowserState()->GetOriginalChromeBrowserState();

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

    self.reauthModule =
        ScopedPasswordSuggestionBottomSheetReauthModuleOverride::Get();
    if (!self.reauthModule) {
      self.reauthModule = [[ReauthenticationModule alloc] init];
    }
    self.mediator = [[PasswordSuggestionBottomSheetMediator alloc]
          initWithWebStateList:webStateList
                 faviconLoader:IOSChromeFaviconLoaderFactory::
                                   GetForBrowserState(browserState)
                   prefService:browserState->GetPrefs()
                        params:params
                  reauthModule:_reauthModule
                           URL:URL
          profilePasswordStore:profilePasswordStore
          accountPasswordStore:accountPasswordStore
        sharedURLLoaderFactory:browserState->GetSharedURLLoaderFactory()
             engagementTracker:feature_engagement::TrackerFactory::
                                   GetForBrowserState(
                                       self.browser->GetBrowserState())];
    self.viewController.delegate = self.mediator;
    self.mediator.consumer = self.viewController;
  }
  return self;
}

#pragma mark - ChromeCoordinator

- (void)start {
  // If the bottom sheet has no suggestion to show, do not show the bottom
  // sheet. Instead, re-focus the field which triggered the bottom sheet and
  // disable it.
  if (![self.mediator hasSuggestions]) {
    [self.mediator dismiss];
    return;
  }

  self.viewController.parentViewControllerHeight =
      self.baseViewController.view.frame.size.height;
  __weak __typeof(self) weakSelf = self;
  [self.baseViewController presentViewController:self.viewController
                                        animated:YES
                                      completion:^{
                                        [weakSelf setInitialVoiceOverFocus];
                                      }];

  // Dismiss right away if the presentation failed to avoid having a zombie
  // coordinator. This is the best proxy we have to know whether the view
  // controller for the bottom sheet could really be presented as the completion
  // block is only called when presentation really happens, and we can't get any
  // error message or signal. Based on what we could test, we know that
  // presentingViewController is only set if the view controller can be
  // presented, where it is left to nil if the presentation is rejected for
  // various reasons (having another view controller already presented is one of
  // them). One should not think they can know all the reasons why the
  // presentation fails.
  //
  // Keep this line at the end of -start because the
  // delegate will likely -stop the coordinator when closing suggestions, so the
  // coordinator should be in the most up to date state where it can be safely
  // stopped.
  if (!self.viewController.presentingViewController) {
    [self.mediator logExitReason:kCouldNotPresent];
    [self.browserCoordinatorCommandsHandler dismissPasswordSuggestions];
  }
}

- (void)stop {
  [super stop];
  [_mediator disconnect];
  _mediator.consumer = nil;
  _mediator = nil;
  _viewController.delegate = nil;
  _viewController = nil;
}

#pragma mark - PasswordSuggestionBottomSheetHandler

- (void)displayPasswordManager {
  _dismissing = YES;
  [self.mediator logExitReason:kShowPasswordManager];

  __weak __typeof(self) weakSelf = self;
  [self.baseViewController.presentedViewController
      dismissViewControllerAnimated:NO
                         completion:^{
                           [weakSelf displaySavedPasswordList];
                           [weakSelf.browserCoordinatorCommandsHandler
                                   dismissPasswordSuggestions];
                         }];
}

- (void)displayPasswordDetailsForFormSuggestion:
    (FormSuggestion*)formSuggestion {
  _dismissing = YES;
  [self.mediator logExitReason:kShowPasswordDetails];
  std::optional<password_manager::CredentialUIEntry> credential =
      [self.mediator getCredentialForFormSuggestion:formSuggestion];

  __weak __typeof(self) weakSelf = self;
  [self.baseViewController.presentedViewController
      dismissViewControllerAnimated:NO
                         completion:^{
                           // TODO(crbug.com/40896839): Add metric for when the
                           // credential is nil.
                           if (credential.has_value()) {
                             [weakSelf
                                 showPasswordDetailsForCredential:credential
                                                                      .value()];
                           }
                           [weakSelf.browserCoordinatorCommandsHandler
                                   dismissPasswordSuggestions];
                         }];
}

- (void)primaryButtonTappedForSuggestion:(FormSuggestion*)formSuggestion
                                 atIndex:(NSInteger)index {
  _dismissing = YES;
  [self.mediator logExitReason:kUsePasswordSuggestion];
  __weak __typeof(self) weakSelf = self;
  ProceduralBlock completion = ^{
    [weakSelf.browserCoordinatorCommandsHandler dismissPasswordSuggestions];
  };
  [self.viewController
      dismissViewControllerAnimated:NO
                         completion:^{
                           [weakSelf.mediator didSelectSuggestion:formSuggestion
                                                          atIndex:index
                                                       completion:completion];
                         }];
}

- (void)secondaryButtonTapped {
  // "Use Keyboard" button, which dismisses the bottom sheet.
  [self.viewController dismissViewControllerAnimated:YES completion:NULL];
}

- (void)viewDidDisappear {
  if (_dismissing) {
    return;
  }

  [self.mediator logExitReason:kDismissal];
  [self.mediator dismiss];
  [self.mediator disconnect];
  [_browserCoordinatorCommandsHandler dismissPasswordSuggestions];
}

#pragma mark - Private

- (void)setInitialVoiceOverFocus {
  UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                  self.viewController.aboveTitleView);
}

- (void)displaySavedPasswordList {
  [_passwordControllerDelegate displaySavedPasswordList];
}

- (void)showPasswordDetailsForCredential:
    (password_manager::CredentialUIEntry)credential {
  [_passwordControllerDelegate showPasswordDetailsForCredential:credential];
}

@end