| // 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/password_details/password_details_coordinator.h" |
| |
| #import <utility> |
| #import <vector> |
| |
| #import "base/apple/foundation_util.h" |
| #import "base/memory/scoped_refptr.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "components/password_manager/core/browser/password_manager_client.h" |
| #import "components/password_manager/core/browser/ui/affiliated_group.h" |
| #import "components/password_manager/core/browser/ui/credential_ui_entry.h" |
| #import "components/password_manager/core/common/password_manager_features.h" |
| #import "components/strings/grit/components_strings.h" |
| #import "ios/chrome/browser/credential_provider_promo/features.h" |
| #import "ios/chrome/browser/passwords/ios_chrome_password_check_manager.h" |
| #import "ios/chrome/browser/passwords/ios_chrome_password_check_manager_factory.h" |
| #import "ios/chrome/browser/passwords/password_tab_helper.h" |
| #import "ios/chrome/browser/shared/coordinator/alert/action_sheet_coordinator.h" |
| #import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h" |
| #import "ios/chrome/browser/shared/model/browser/browser.h" |
| #import "ios/chrome/browser/shared/model/browser_state/chrome_browser_state.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/command_dispatcher.h" |
| #import "ios/chrome/browser/shared/public/commands/credential_provider_promo_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/open_new_tab_command.h" |
| #import "ios/chrome/browser/shared/public/commands/snackbar_commands.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/browser/sync/sync_service_factory.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details_consumer.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details_coordinator_delegate.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details_handler.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details_mediator.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details_mediator_delegate.h" |
| #import "ios/chrome/browser/ui/settings/password/password_details/password_details_table_view_controller.h" |
| #import "ios/chrome/browser/ui/settings/password/password_manager_ui_features.h" |
| #import "ios/chrome/browser/ui/settings/password/password_sharing/password_sharing_coordinator.h" |
| #import "ios/chrome/browser/ui/settings/password/reauthentication/reauthentication_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 "ios/web/public/web_state.h" |
| #import "ui/base/l10n/l10n_util.h" |
| |
| using password_manager::features::IsAuthOnEntryV2Enabled; |
| |
| @interface PasswordDetailsCoordinator () <PasswordDetailsHandler, |
| PasswordDetailsMediatorDelegate, |
| ReauthenticationCoordinatorDelegate> { |
| password_manager::AffiliatedGroup _affiliatedGroup; |
| password_manager::CredentialUIEntry _credential; |
| |
| // The context in which the password details are accessed. |
| DetailsContext _context; |
| } |
| |
| // Main view controller for this coordinator. |
| @property(nonatomic, strong) PasswordDetailsTableViewController* viewController; |
| |
| // Main mediator for this coordinator. |
| @property(nonatomic, strong) PasswordDetailsMediator* mediator; |
| |
| // Module containing the reauthentication mechanism for viewing and copying |
| // passwords. |
| // Has to be strong for password bottom sheet feature or else it becomes nil. |
| @property(nonatomic, strong) ReauthenticationModule* reauthenticationModule; |
| |
| // Modal alert for interactions with password. |
| @property(nonatomic, strong) AlertCoordinator* alertCoordinator; |
| |
| // The action sheet coordinator, if one is currently being shown. |
| @property(nonatomic, strong) ActionSheetCoordinator* actionSheetCoordinator; |
| |
| // Coordinator for the password sharing flow. |
| @property(nonatomic, strong) |
| PasswordSharingCoordinator* passwordSharingCoordinator; |
| |
| // Coordinator for blocking password details until Local Authentication is |
| // successful. |
| @property(nonatomic, strong) ReauthenticationCoordinator* reauthCoordinator; |
| |
| @end |
| |
| @implementation PasswordDetailsCoordinator |
| |
| @synthesize baseNavigationController = _baseNavigationController; |
| |
| - (instancetype) |
| initWithBaseNavigationController: |
| (UINavigationController*)navigationController |
| browser:(Browser*)browser |
| credential: |
| (const password_manager::CredentialUIEntry&) |
| credential |
| reauthModule:(ReauthenticationModule*)reauthModule |
| context:(DetailsContext)context { |
| self = [super initWithBaseViewController:navigationController |
| browser:browser]; |
| if (self) { |
| DCHECK(navigationController); |
| |
| _baseNavigationController = navigationController; |
| _credential = credential; |
| _reauthenticationModule = reauthModule; |
| _context = context; |
| } |
| return self; |
| } |
| |
| - (instancetype) |
| initWithBaseNavigationController: |
| (UINavigationController*)navigationController |
| browser:(Browser*)browser |
| affiliatedGroup:(const password_manager::AffiliatedGroup&) |
| affiliatedGroup |
| reauthModule:(ReauthenticationModule*)reauthModule |
| context:(DetailsContext)context { |
| self = [super initWithBaseViewController:navigationController |
| browser:browser]; |
| if (self) { |
| DCHECK(navigationController); |
| |
| _baseNavigationController = navigationController; |
| _affiliatedGroup = affiliatedGroup; |
| _reauthenticationModule = reauthModule; |
| _context = context; |
| } |
| return self; |
| } |
| |
| - (void)start { |
| self.viewController = [[PasswordDetailsTableViewController alloc] init]; |
| |
| std::vector<password_manager::CredentialUIEntry> credentials; |
| NSString* displayName; |
| if (_affiliatedGroup.GetCredentials().size() > 0) { |
| displayName = [NSString |
| stringWithUTF8String:_affiliatedGroup.GetDisplayName().c_str()]; |
| for (const auto& credentialGroup : _affiliatedGroup.GetCredentials()) { |
| credentials.push_back(credentialGroup); |
| } |
| } else { |
| credentials.push_back(_credential); |
| } |
| |
| ChromeBrowserState* browserState = self.browser->GetBrowserState(); |
| self.mediator = [[PasswordDetailsMediator alloc] |
| initWithPasswords:credentials |
| displayName:displayName |
| passwordCheckManager:IOSChromePasswordCheckManagerFactory:: |
| GetForBrowserState(browserState) |
| .get() |
| prefService:browserState->GetPrefs() |
| syncService:SyncServiceFactory::GetForBrowserState(browserState) |
| context:_context |
| delegate:self]; |
| self.mediator.consumer = self.viewController; |
| self.viewController.handler = self; |
| self.viewController.delegate = self.mediator; |
| self.viewController.applicationCommandsHandler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), ApplicationCommands); |
| self.viewController.snackbarCommandsHandler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), SnackbarCommands); |
| self.viewController.reauthModule = self.reauthenticationModule; |
| if (self.showCancelButton) { |
| [self.viewController setupLeftCancelButton]; |
| } |
| |
| // Disable animation when content will be blocked for reauth to prevent |
| // flickering in navigation bar. |
| [self.baseNavigationController |
| pushViewController:self.viewController |
| animated:![self shouldRequireAuthOnStart]]; |
| if (IsAuthOnEntryV2Enabled()) { |
| [self startReauthCoordinator]; |
| } |
| } |
| |
| - (void)stop { |
| [_reauthCoordinator stop]; |
| _reauthCoordinator.delegate = nil; |
| _reauthCoordinator = nil; |
| [self dismissActionSheetCoordinator]; |
| [self.mediator disconnect]; |
| self.mediator = nil; |
| self.viewController = nil; |
| [self dismissAlertCoordinator]; |
| } |
| |
| #pragma mark - PasswordDetailsHandler |
| |
| - (void)passwordDetailsTableViewControllerWasDismissed { |
| [self.delegate passwordDetailsCoordinatorDidRemove:self]; |
| } |
| |
| - (void)dismissPasswordDetailsTableViewController { |
| [self.delegate passwordDetailsCancelButtonWasTapped]; |
| [self.delegate passwordDetailsCoordinatorDidRemove:self]; |
| } |
| |
| - (void)showPasscodeDialog { |
| NSString* title = |
| l10n_util::GetNSString(IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_TITLE); |
| NSString* message = |
| l10n_util::GetNSString(IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_CONTENT); |
| self.alertCoordinator = |
| [[AlertCoordinator alloc] initWithBaseViewController:self.viewController |
| browser:self.browser |
| title:title |
| message:message]; |
| |
| __weak __typeof(self) weakSelf = self; |
| OpenNewTabCommand* command = |
| [OpenNewTabCommand commandWithURLFromChrome:GURL(kPasscodeArticleURL)]; |
| |
| [self.alertCoordinator addItemWithTitle:l10n_util::GetNSString(IDS_OK) |
| action:^{ |
| [weakSelf dismissAlertCoordinator]; |
| } |
| style:UIAlertActionStyleCancel]; |
| |
| [self.alertCoordinator |
| addItemWithTitle:l10n_util::GetNSString( |
| IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_LEARN_HOW) |
| action:^{ |
| id<ApplicationCommands> applicationCommandsHandler = |
| HandlerForProtocol( |
| weakSelf.browser->GetCommandDispatcher(), |
| ApplicationCommands); |
| [applicationCommandsHandler |
| closeSettingsUIAndOpenURL:command]; |
| [weakSelf dismissAlertCoordinator]; |
| } |
| style:UIAlertActionStyleDefault]; |
| |
| [self.alertCoordinator start]; |
| } |
| |
| - (void)showPasswordEditDialogWithOrigin:(NSString*)origin { |
| NSString* message = l10n_util::GetNSStringF(IDS_IOS_EDIT_PASSWORD_DESCRIPTION, |
| base::SysNSStringToUTF16(origin)); |
| self.actionSheetCoordinator = [[ActionSheetCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| browser:self.browser |
| title:nil |
| message:message |
| barButtonItem:self.viewController.navigationItem |
| .rightBarButtonItem]; |
| |
| __weak __typeof(self) weakSelf = self; |
| |
| [self.actionSheetCoordinator |
| addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CONFIRM_PASSWORD_EDIT) |
| action:^{ |
| [weakSelf.viewController passwordEditingConfirmed]; |
| [weakSelf dismissActionSheetCoordinator]; |
| } |
| style:UIAlertActionStyleDefault]; |
| |
| [self.actionSheetCoordinator |
| addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_EDIT) |
| action:^{ |
| [weakSelf dismissActionSheetCoordinator]; |
| } |
| style:UIAlertActionStyleCancel]; |
| |
| [self.actionSheetCoordinator start]; |
| } |
| |
| - (void)showPasswordDeleteDialogWithPasswordDetails:(PasswordDetails*)password |
| anchorView:(UIView*)anchorView { |
| NSString* title; |
| NSString* message; |
| // Blocked websites have empty `password` and no title or message. |
| if ([password.password length]) { |
| std::tie(title, message) = |
| password_manager::GetPasswordAlertTitleAndMessageForOrigins( |
| password.origins); |
| } |
| NSString* buttonText = l10n_util::GetNSString(IDS_IOS_DELETE_ACTION_TITLE); |
| |
| self.actionSheetCoordinator = |
| anchorView |
| ? [[ActionSheetCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| browser:self.browser |
| title:title |
| message:message |
| rect:anchorView.bounds |
| view:anchorView] |
| : [[ActionSheetCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| browser:self.browser |
| title:title |
| message:message |
| barButtonItem:self.viewController.deleteButton]; |
| __weak __typeof(self.mediator) weakMediator = self.mediator; |
| __weak __typeof(self) weakSelf = self; |
| [self.actionSheetCoordinator |
| addItemWithTitle:buttonText |
| action:^{ |
| [weakMediator removeCredential:password]; |
| [weakSelf dismissActionSheetCoordinator]; |
| } |
| style:UIAlertActionStyleDestructive]; |
| [self.actionSheetCoordinator |
| addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_DELETION) |
| action:^{ |
| [weakSelf dismissActionSheetCoordinator]; |
| } |
| style:UIAlertActionStyleCancel]; |
| [self.actionSheetCoordinator start]; |
| } |
| |
| - (void)moveCredentialToAccountStore:(PasswordDetails*)password |
| anchorView:(UIView*)anchorView |
| movedCompletion:(void (^)())movedCompletion { |
| if (![self.mediator hasPasswordConflictInAccount:password]) { |
| [self.mediator moveCredentialToAccountStore:password]; |
| movedCompletion(); |
| return; |
| } |
| NSString* actionSheetTitle = |
| l10n_util::GetNSString(IDS_IOS_PASSWORD_MOVE_CONFLICT_ACTION_SHEET_TITLE); |
| NSString* actionSheetMessage = l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_MOVE_CONFLICT_ACTION_SHEET_MESSAGE); |
| self.actionSheetCoordinator = [[ActionSheetCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| browser:self.browser |
| title:actionSheetTitle |
| message:actionSheetMessage |
| rect:anchorView.bounds |
| view:anchorView]; |
| |
| __weak __typeof(self) weakSelf = self; |
| [self.actionSheetCoordinator |
| addItemWithTitle:l10n_util::GetNSString(IDS_IOS_KEEP_RECENT_PASSWORD) |
| action:^{ |
| [weakSelf.mediator |
| moveCredentialToAccountStoreWithConflict:password]; |
| movedCompletion(); |
| [weakSelf dismissActionSheetCoordinator]; |
| } |
| style:UIAlertActionStyleDefault]; |
| |
| [self.actionSheetCoordinator |
| addItemWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_MOVE) |
| action:^{ |
| [weakSelf dismissActionSheetCoordinator]; |
| } |
| style:UIAlertActionStyleCancel]; |
| [self.actionSheetCoordinator start]; |
| } |
| |
| - (void)showPasswordDetailsInEditModeWithoutAuthentication { |
| [self.viewController showEditViewWithoutAuthentication]; |
| } |
| |
| - (void)onPasswordCopiedByUser { |
| if (IsCredentialProviderExtensionPromoEnabledOnPasswordCopied()) { |
| id<CredentialProviderPromoCommands> credentialProviderPromoHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), |
| CredentialProviderPromoCommands); |
| [credentialProviderPromoHandler |
| showCredentialProviderPromoWithTrigger:CredentialProviderPromoTrigger:: |
| PasswordCopied]; |
| } |
| } |
| |
| - (void)onAllPasswordsDeleted { |
| DCHECK_EQ(self.baseNavigationController.topViewController, |
| self.viewController); |
| // For password details opened outside of the settings context. |
| if (_context == DetailsContext::kOutsideSettings) { |
| [self dismissPasswordDetailsTableViewController]; |
| } else { |
| // For password details opened from the Password Manager in the settings. |
| [self.baseNavigationController popViewControllerAnimated:YES]; |
| } |
| } |
| |
| - (void)onShareButtonPressed { |
| [self.passwordSharingCoordinator stop]; |
| self.passwordSharingCoordinator = [[PasswordSharingCoordinator alloc] |
| initWithBaseViewController:self.viewController |
| browser:self.browser]; |
| [self.passwordSharingCoordinator start]; |
| } |
| |
| #pragma mark - PasswordDetailsMediatorDelegate |
| |
| - (void)showDismissWarningDialogWithPasswordDetails:(PasswordDetails*)password { |
| NSString* title = |
| l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING_DIALOG_TITLE); |
| NSString* message = |
| l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING_DIALOG_MESSAGE); |
| self.alertCoordinator = |
| [[AlertCoordinator alloc] initWithBaseViewController:self.viewController |
| browser:self.browser |
| title:title |
| message:message]; |
| |
| NSString* cancelButtonText = l10n_util::GetNSString(IDS_CANCEL); |
| __weak PasswordDetailsCoordinator* weakSelf = self; |
| [self.alertCoordinator addItemWithTitle:cancelButtonText |
| action:^{ |
| [weakSelf dismissAlertCoordinator]; |
| } |
| style:UIAlertActionStyleDefault]; |
| |
| NSString* dismissButtonText = |
| l10n_util::GetNSString(IDS_IOS_DISMISS_WARNING_DIALOG_DISMISS_BUTTON); |
| __weak __typeof(self.mediator) weakMediator = self.mediator; |
| [self.alertCoordinator |
| addItemWithTitle:dismissButtonText |
| action:^{ |
| [weakMediator didConfirmWarningDismissalForPassword:password]; |
| [weakSelf dismissAlertCoordinator]; |
| } |
| style:UIAlertActionStyleDefault |
| preferred:YES |
| enabled:YES]; |
| [self.alertCoordinator start]; |
| } |
| |
| - (void)updateFormManagers { |
| web::WebState* activeWebState = |
| self.browser->GetWebStateList()->GetActiveWebState(); |
| if (!activeWebState) { |
| // PasswordDetailsCoordinator and other settings coordinators always receive |
| // a normal Browser, even if they are started from incognito. So if only |
| // incognito tabs are open, `activeWebState` is null, causing a crash |
| // (crbug.com/1468506). |
| return; |
| } |
| password_manager::PasswordManagerClient* passwordManagerClient = |
| PasswordTabHelper::FromWebState(activeWebState) |
| ->GetPasswordManagerClient(); |
| passwordManagerClient->UpdateFormManagers(); |
| } |
| |
| #pragma mark - ReauthenticationCoordinatorDelegate |
| |
| - (void)successfulReauthenticationWithCoordinator: |
| (ReauthenticationCoordinator*)coordinator { |
| // No-op. |
| } |
| |
| - (void)willPushReauthenticationViewController { |
| // Dismiss modal ui before reauth view controller is pushed in front of |
| // password details. |
| [self dismissAlertCoordinator]; |
| [self dismissActionSheetCoordinator]; |
| [self dismissPasswordSharingCoordinator]; |
| } |
| |
| #pragma mark - Private |
| |
| - (void)dismissActionSheetCoordinator { |
| [self.actionSheetCoordinator stop]; |
| self.actionSheetCoordinator = nil; |
| } |
| |
| - (void)dismissAlertCoordinator { |
| [self.alertCoordinator stop]; |
| self.alertCoordinator = nil; |
| } |
| |
| - (void)dismissPasswordSharingCoordinator { |
| [_passwordSharingCoordinator stop]; |
| _passwordSharingCoordinator = nil; |
| } |
| |
| // Starts reauthCoordinator. If Password Details was opened from outside the |
| // Password Manager, Local Authentication is required. Once started |
| // reauthCoordinator observes scene state changes and requires authentication |
| // when the scene is backgrounded and then foregrounded while Password Details |
| // is opened. |
| - (void)startReauthCoordinator { |
| _reauthCoordinator = [[ReauthenticationCoordinator alloc] |
| initWithBaseNavigationController:_baseNavigationController |
| browser:self.browser |
| reauthenticationModule:_reauthenticationModule |
| authOnStart:[self shouldRequireAuthOnStart]]; |
| _reauthCoordinator.delegate = self; |
| [_reauthCoordinator start]; |
| } |
| |
| // Whether Local Authentication should be required before displaying the |
| // contents of Password Details. |
| - (BOOL)shouldRequireAuthOnStart { |
| if (!IsAuthOnEntryV2Enabled()) { |
| return NO; |
| } |
| |
| // Authentication required only if opening Password Details from outside the |
| // Password Manager. |
| switch (_context) { |
| case DetailsContext::kWeakIssues: |
| case DetailsContext::kReusedIssues: |
| case DetailsContext::kPasswordSettings: |
| case DetailsContext::kCompromisedIssues: |
| case DetailsContext::kDismissedWarnings: |
| return NO; |
| case DetailsContext::kOutsideSettings: |
| return YES; |
| } |
| } |
| |
| @end |