chromium/ios/chrome/browser/ui/settings/password/passwords_coordinator.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/ui/settings/password/passwords_coordinator.h"

#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "components/feature_engagement/public/tracker.h"
#import "components/keyed_service/core/service_access_type.h"
#import "components/password_manager/core/browser/ui/credential_ui_entry.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.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_password_check_manager.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_password_check_manager_factory.h"
#import "ios/chrome/browser/passwords/model/metrics/ios_password_manager_metrics.h"
#import "ios/chrome/browser/passwords/model/metrics/ios_password_manager_visits_recorder.h"
#import "ios/chrome/browser/passwords/model/password_checkup_metrics.h"
#import "ios/chrome/browser/passwords/model/password_checkup_utils.h"
#import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/shared/public/commands/snackbar_commands.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/sync/model/sync_service_factory.h"
#import "ios/chrome/browser/ui/settings/password/password_checkup/password_checkup_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_details/add_password_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_details/add_password_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller.h"
#import "ios/chrome/browser/ui/settings/password/password_manager_view_controller_presentation_delegate.h"
#import "ios/chrome/browser/ui/settings/password/password_settings/password_settings_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/password_settings/password_settings_coordinator_delegate.h"
#import "ios/chrome/browser/ui/settings/password/passwords_consumer.h"
#import "ios/chrome/browser/ui/settings/password/passwords_mediator.h"
#import "ios/chrome/browser/ui/settings/password/passwords_settings_commands.h"
#import "ios/chrome/browser/ui/settings/password/reauthentication/reauthentication_coordinator.h"
#import "ios/chrome/browser/ui/settings/password/widget_promo_instructions/widget_promo_instructions_coordinator.h"
#import "ios/chrome/browser/ui/settings/utils/password_utils.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_module.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"

using password_manager::WarningType;

@interface PasswordsCoordinator () <
    AddPasswordCoordinatorDelegate,
    PasswordDetailsCoordinatorDelegate,
    PasswordCheckupCoordinatorDelegate,
    PasswordSettingsCoordinatorDelegate,
    PasswordsSettingsCommands,
    PasswordManagerViewControllerPresentationDelegate,
    ReauthenticationCoordinatorDelegate,
    WidgetPromoInstructionsCoordinatorDelegate>

// Main view controller for this coordinator.
@property(nonatomic, strong)
    PasswordManagerViewController* passwordsViewController;

// Main mediator for this coordinator.
@property(nonatomic, strong) PasswordsMediator* mediator;

// Reauthentication module used by passwords export and password details.
@property(nonatomic, strong) ReauthenticationModule* reauthModule;

// Coordinator for Password Checkup.
@property(nonatomic, strong)
    PasswordCheckupCoordinator* passwordCheckupCoordinator;

// Coordinator for editing existing password details.
@property(nonatomic, strong)
    PasswordDetailsCoordinator* passwordDetailsCoordinator;

// The action sheet coordinator, if one is currently being shown.
@property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator;

// Coordinator for add password details.
@property(nonatomic, strong) AddPasswordCoordinator* addPasswordCoordinator;

@property(nonatomic, strong)
    PasswordSettingsCoordinator* passwordSettingsCoordinator;

// Coordinator for blocking password manager until successful Local
// Authentication.
@property(nonatomic, strong) ReauthenticationCoordinator* reauthCoordinator;

// Coordinator that presents the instructions on how to install the Password
// Manager widget.
@property(nonatomic, strong)
    WidgetPromoInstructionsCoordinator* widgetPromoInstructionsCoordinator;

@end

@implementation PasswordsCoordinator {
  // For recording visits after successful authentication.
  IOSPasswordManagerVisitsRecorder* _visitsRecorder;

  // Whether local authentication failed for a child coordinator and thus the
  // whole Password Manager UI is being dismissed.
  BOOL _authDidFailForChildCoordinator;
}

@synthesize baseNavigationController = _baseNavigationController;

- (instancetype)initWithBaseNavigationController:
                    (UINavigationController*)navigationController
                                         browser:(Browser*)browser {
  self = [super initWithBaseViewController:navigationController
                                   browser:browser];
  if (self) {
    _baseNavigationController = navigationController;
  }
  return self;
}

- (void)checkSavedPasswords {
  [self.mediator startPasswordCheck];

  password_manager::LogStartPasswordCheckAutomatically();
}

- (UIViewController*)viewController {
  return self.passwordsViewController;
}

#pragma mark - ChromeCoordinator

- (void)start {
  ChromeBrowserState* browserState = self.browser->GetBrowserState();
  FaviconLoader* faviconLoader =
      IOSChromeFaviconLoaderFactory::GetForBrowserState(browserState);

  self.mediator = [[PasswordsMediator alloc]
      initWithPasswordCheckManager:IOSChromePasswordCheckManagerFactory::
                                       GetForBrowserState(browserState)
                     faviconLoader:faviconLoader
                       syncService:SyncServiceFactory::GetForBrowserState(
                                       browserState)
                       prefService:browserState->GetPrefs()];
  self.mediator.tracker =
      feature_engagement::TrackerFactory::GetForBrowserState(browserState);

  self.reauthModule = password_manager::BuildReauthenticationModule(
      /*successfulReauthTimeAccessor=*/self.mediator);
  ChromeAccountManagerService* accountManagerService =
      ChromeAccountManagerServiceFactory::GetForBrowserState(browserState);

  PasswordManagerViewController* passwordsViewController =
      [[PasswordManagerViewController alloc]
          initWithChromeAccountManagerService:accountManagerService
                                  prefService:browserState->GetPrefs()
                       shouldOpenInSearchMode:
                           self.openViewControllerForPasswordSearch];
  self.passwordsViewController = passwordsViewController;

  CommandDispatcher* dispatcher = self.browser->GetCommandDispatcher();
  passwordsViewController.applicationHandler =
      HandlerForProtocol(dispatcher, ApplicationCommands);
  passwordsViewController.browserHandler =
      HandlerForProtocol(dispatcher, BrowserCommands);
  passwordsViewController.settingsHandler =
      HandlerForProtocol(dispatcher, SettingsCommands);
  passwordsViewController.snackbarHandler =
      HandlerForProtocol(dispatcher, SnackbarCommands);

  passwordsViewController.handler = self;
  passwordsViewController.delegate = self.mediator;
  passwordsViewController.presentationDelegate = self;
  passwordsViewController.reauthenticationModule = self.reauthModule;
  passwordsViewController.imageDataSource = self.mediator;

  self.mediator.consumer = self.passwordsViewController;

  // Disable animation when content will be blocked for reauth to prevent
  // flickering in navigation bar.
  [self.baseNavigationController pushViewController:self.passwordsViewController
                                           animated:NO];

  _visitsRecorder = [[IOSPasswordManagerVisitsRecorder alloc]
      initWithPasswordManagerSurface:password_manager::PasswordManagerSurface::
                                         kPasswordList];

  [self startReauthCoordinatorWithAuthOnStart:YES];

  // Start a password check.
  [self checkSavedPasswords];
}

- (void)stop {
  self.passwordsViewController.delegate = nil;
  self.passwordsViewController = nil;

  [self.passwordCheckupCoordinator stop];
  self.passwordCheckupCoordinator.delegate = nil;
  self.passwordCheckupCoordinator = nil;

  [self.passwordDetailsCoordinator stop];
  self.passwordDetailsCoordinator.delegate = nil;
  self.passwordDetailsCoordinator = nil;

  // When the coordinator is stopped due to failed authentication, the whole
  // Password Manager UI is dismissed via command. Not dismissing the top
  // presented coordinator UI before everything else prevents the Password
  // Manager UI from being visible without local authentication.
  [self.passwordSettingsCoordinator
      stopWithUIDismissal:!_authDidFailForChildCoordinator];
  self.passwordSettingsCoordinator.delegate = nil;
  self.passwordSettingsCoordinator = nil;

  [self.addPasswordCoordinator
      stopWithUIDismissal:!_authDidFailForChildCoordinator];
  self.addPasswordCoordinator.delegate = nil;
  self.addPasswordCoordinator = nil;

  [self.reauthCoordinator stop];
  self.reauthCoordinator.delegate = nil;
  self.reauthCoordinator = nil;
  [self dismissActionSheetCoordinator];

  [self.mediator disconnect];
}

#pragma mark - PasswordsSettingsCommands

- (void)showPasswordCheckup {
  DCHECK(!self.passwordCheckupCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];

  self.passwordCheckupCoordinator = [[PasswordCheckupCoordinator alloc]
      initWithBaseNavigationController:self.baseNavigationController
                               browser:self.browser
                          reauthModule:self.reauthModule
                              referrer:password_manager::PasswordCheckReferrer::
                                           kPasswordSettings];
  self.passwordCheckupCoordinator.delegate = self;
  [self.passwordCheckupCoordinator start];
}

- (void)showDetailedViewForCredential:
    (const password_manager::CredentialUIEntry&)credential {
  DCHECK(!self.passwordDetailsCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];

  self.passwordDetailsCoordinator = [[PasswordDetailsCoordinator alloc]
      initWithBaseNavigationController:self.baseNavigationController
                               browser:self.browser
                            credential:credential
                          reauthModule:self.reauthModule
                               context:DetailsContext::kPasswordSettings];
  self.passwordDetailsCoordinator.delegate = self;
  [self.passwordDetailsCoordinator start];
}

- (void)showDetailedViewForAffiliatedGroup:
    (const password_manager::AffiliatedGroup&)affiliatedGroup {
  // Not an invariant due to possible race conditions. DCHECKing for debugging
  // purposes. See crbug.com/40067451.
  DCHECK(!self.passwordDetailsCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];
  self.passwordDetailsCoordinator = [[PasswordDetailsCoordinator alloc]
      initWithBaseNavigationController:self.baseNavigationController
                               browser:self.browser
                       affiliatedGroup:affiliatedGroup
                          reauthModule:self.reauthModule
                               context:DetailsContext::kPasswordSettings];
  self.passwordDetailsCoordinator.delegate = self;
  [self.passwordDetailsCoordinator start];
}

- (void)showAddPasswordSheet {
  // Not an invariant. DCHECKing for debugging purposes. See crbug.com/40067451.
  DCHECK(!self.addPasswordCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];
  self.addPasswordCoordinator = [[AddPasswordCoordinator alloc]
      initWithBaseViewController:self.viewController
                         browser:self.browser];
  self.addPasswordCoordinator.delegate = self;
  [self.addPasswordCoordinator start];
}

- (void)showPasswordDeleteDialogWithOrigins:(NSArray<NSString*>*)origins
                                 completion:(void (^)(void))completion {
  std::pair<NSString*, NSString*> titleAndMessage =
      password_manager::GetPasswordAlertTitleAndMessageForOrigins(origins);
  NSString* title = titleAndMessage.first;
  NSString* message = titleAndMessage.second;

  self.actionSheetCoordinator = [[ActionSheetCoordinator alloc]
      initWithBaseViewController:self.viewController
                         browser:self.browser
                           title:title
                         message:message
                   barButtonItem:self.passwordsViewController.deleteButton];

  NSString* deleteButtonString =
      l10n_util::GetNSString(IDS_IOS_DELETE_ACTION_TITLE);
  __weak PasswordsCoordinator* weakSelf = self;

  [self.actionSheetCoordinator addItemWithTitle:deleteButtonString
                                         action:^{
                                           completion();
                                           [weakSelf
                                               dismissActionSheetCoordinator];
                                         }
                                          style:UIAlertActionStyleDestructive];

  [self.actionSheetCoordinator
      addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_DELETION)
                action:^{
                  [weakSelf dismissActionSheetCoordinator];
                }
                 style:UIAlertActionStyleCancel];

  [self.actionSheetCoordinator start];
}

#pragma mark - PasswordManagerViewControllerPresentationDelegate

- (void)PasswordManagerViewControllerDismissed {
  [self.delegate passwordsCoordinatorDidRemove:self];
}

- (void)showPasswordSettingsSubmenu {
  DCHECK(!self.passwordSettingsCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];

  self.passwordSettingsCoordinator = [[PasswordSettingsCoordinator alloc]
      initWithBaseViewController:self.viewController
                         browser:self.browser];
  self.passwordSettingsCoordinator.delegate = self;
  // No auth required as Passwords Coordinator already is auth protected.
  self.passwordSettingsCoordinator.skipAuthenticationOnStart = YES;

  base::RecordAction(base::UserMetricsAction("PasswordManager_OpenSettings"));
  [self.passwordSettingsCoordinator start];
}

- (void)showPasswordManagerWidgetPromoInstructions {
  // Not an invariant due to possible race conditions. DCHECKing for debugging
  // purposes. See crbug.com/40067451.
  DCHECK(!self.widgetPromoInstructionsCoordinator);

  [self stopReauthCoordinatorBeforeStartingChildCoordinator];

  self.widgetPromoInstructionsCoordinator =
      [[WidgetPromoInstructionsCoordinator alloc]
          initWithBaseViewController:self.viewController
                             browser:self.browser];
  self.widgetPromoInstructionsCoordinator.delegate = self;
  [self.widgetPromoInstructionsCoordinator start];
}

#pragma mark - PasswordCheckupCoordinatorDelegate

- (void)passwordCheckupCoordinatorDidRemove:
    (PasswordCheckupCoordinator*)coordinator {
  DCHECK_EQ(self.passwordCheckupCoordinator, coordinator);
  [self.passwordCheckupCoordinator stop];
  self.passwordCheckupCoordinator.delegate = nil;
  self.passwordCheckupCoordinator = nil;
  [self restartReauthCoordinator];
}

#pragma mark - PasswordManagerReauthenticationDelegate

- (void)dismissPasswordManagerAfterFailedReauthentication {
  _authDidFailForChildCoordinator = YES;
  [_delegate dismissPasswordManagerAfterFailedReauthentication];
}

#pragma mark PasswordDetailsCoordinatorDelegate

- (void)passwordDetailsCoordinatorDidRemove:
    (PasswordDetailsCoordinator*)coordinator {
  DCHECK_EQ(self.passwordDetailsCoordinator, coordinator);
  [self.passwordDetailsCoordinator stop];
  self.passwordDetailsCoordinator.delegate = nil;
  self.passwordDetailsCoordinator = nil;
  [self restartReauthCoordinator];
}

#pragma mark AddPasswordDetailsCoordinatorDelegate

- (void)passwordDetailsTableViewControllerDidFinish:
    (AddPasswordCoordinator*)coordinator {
  DCHECK_EQ(self.addPasswordCoordinator, coordinator);
  [self.addPasswordCoordinator stop];
  self.addPasswordCoordinator.delegate = nil;
  self.addPasswordCoordinator = nil;
  [self restartReauthCoordinator];
}

- (void)setMostRecentlyUpdatedPasswordDetails:
    (const password_manager::CredentialUIEntry&)credential {
  [self.passwordsViewController
      setMostRecentlyUpdatedPasswordDetails:credential];
}

- (void)dismissAddViewControllerAndShowPasswordDetails:
            (const password_manager::CredentialUIEntry&)credential
                                           coordinator:(AddPasswordCoordinator*)
                                                           coordinator {
  DCHECK(self.addPasswordCoordinator &&
         self.addPasswordCoordinator == coordinator);
  [self passwordDetailsTableViewControllerDidFinish:coordinator];
  [self showDetailedViewForCredential:credential];
  [self.passwordDetailsCoordinator
          showPasswordDetailsInEditModeWithoutAuthentication];
}

#pragma mark - PasswordSettingsCoordinatorDelegate

- (void)passwordSettingsCoordinatorDidRemove:
    (PasswordSettingsCoordinator*)coordinator {
  DCHECK_EQ(self.passwordSettingsCoordinator, coordinator);
  [self.passwordSettingsCoordinator stop];
  self.passwordSettingsCoordinator.delegate = nil;
  self.passwordSettingsCoordinator = nil;

  [self restartReauthCoordinator];
}

#pragma mark - ReauthenticationCoordinatorDelegate

- (void)successfulReauthenticationWithCoordinator:
    (ReauthenticationCoordinator*)coordinator {
  DCHECK_EQ(_reauthCoordinator, coordinator);

  [_visitsRecorder maybeRecordVisitMetric];

  [self.mediator askFETToShowPasswordManagerWidgetPromo];

  // Make sure that the Password Manager's toolbar is in the correct state once
  // the reauthentication view controller is dismissed. This is a fix for
  // crbug.com/1503081 that works well in pratice, but isn't perfect due to
  // possible race conditions.
  if (_baseNavigationController.topViewController ==
      self.passwordsViewController) {
    [self.passwordsViewController updateUIForEditState];
  }
}

- (void)dismissUIAfterFailedReauthenticationWithCoordinator:
    (ReauthenticationCoordinator*)coordinator {
  CHECK_EQ(_reauthCoordinator, coordinator);

  [_delegate dismissPasswordManagerAfterFailedReauthentication];
}

- (void)willPushReauthenticationViewController {
  [self dismissActionSheetCoordinator];
}

#pragma mark - WidgetPromoInstructionsCoordinatorDelegate

- (void)removeWidgetPromoInstructionsCoordinator:
    (WidgetPromoInstructionsCoordinator*)coordinator {
  DCHECK_EQ(self.widgetPromoInstructionsCoordinator, coordinator);
  [self.widgetPromoInstructionsCoordinator stop];
  self.widgetPromoInstructionsCoordinator.delegate = nil;
  self.widgetPromoInstructionsCoordinator = nil;
  [self restartReauthCoordinator];
}

#pragma mark - Private

// Starts reauthCoordinator.
// - authOnStart: Pass `YES` to cover password manager with an empty view
// controller until successful Local Authentication when reauthCoordinator
// starts.
//
// Local authentication is required everytime the current
// scene is backgrounded and foregrounded until reauthCoordinator is stopped.
- (void)startReauthCoordinatorWithAuthOnStart:(BOOL)authOnStart {
  // At this point we are either starting the PasswordsCoordinator or we have
  // just dismissed a child coordinator. If the previous reauth coordinator was
  // not stopped and deallocated when the child coordinator was started, we
  // would have multiple reauth coordinators listening for scene states and
  // triggering reauth at the same time with undefined behavior.
  DCHECK(!_reauthCoordinator);

  _reauthCoordinator = [[ReauthenticationCoordinator alloc]
      initWithBaseNavigationController:_baseNavigationController
                               browser:self.browser
                reauthenticationModule:_reauthModule
                           authOnStart:authOnStart];

  _reauthCoordinator.delegate = self;

  [_reauthCoordinator start];
}

// Stop reauth coordinator when a child coordinator will be started.
//
// Needed so reauth coordinator doesn't block for reauth if the scene state
// changes while the child coordinator is presenting its content. The child
// coordinator will add its own reauth coordinator to block its content for
// reauth.
- (void)stopReauthCoordinatorBeforeStartingChildCoordinator {
  // Popping the view controller in case Local Authentication was triggered
  // outside reauthCoordinator before starting the child coordinator. Local
  // Authentication changes the scene state which triggers the presentation of
  // the ReauthenticationViewController by reauthCoordinator. Ideally
  // reauthCoordinator would be stopped when Local Authentication is triggered
  // outside of it but still defending against that scenario to avoid leaving an
  // unintended view controller in the navigation stack.
  [_reauthCoordinator stopAndPopViewController];
  _reauthCoordinator.delegate = nil;
  _reauthCoordinator = nil;
}

// Starts reauthCoordinator after a child coordinator content was dismissed.
- (void)restartReauthCoordinator {
  // Restart reauth coordinator so it monitors scene state changes and requests
  // local authentication after the scene goes to the background.
  [self startReauthCoordinatorWithAuthOnStart:NO];
}

- (void)dismissActionSheetCoordinator {
  [self.actionSheetCoordinator stop];
  self.actionSheetCoordinator = nil;
}

@end