| // Copyright 2022 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/settings/ui_bundled/password/password_settings/password_settings_coordinator.h" |
| |
| #import <UIKit/UIKit.h> |
| |
| #import "base/debug/dump_without_crashing.h" |
| #import "base/i18n/message_formatter.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "components/google/core/common/google_util.h" |
| #import "components/keyed_service/core/service_access_type.h" |
| #import "components/password_manager/core/browser/ui/saved_passwords_presenter.h" |
| #import "components/strings/grit/components_strings.h" |
| #import "ios/chrome/browser/affiliations/model/ios_chrome_affiliation_service_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/metrics/ios_password_manager_metrics.h" |
| #import "ios/chrome/browser/passwords/model/metrics/ios_password_manager_visits_recorder.h" |
| #import "ios/chrome/browser/settings/ui_bundled/elements/enterprise_info_popover_view_controller.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/create_password_manager_title_view.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_bulk_move_handler.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_export_handler.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_constants.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_coordinator_delegate.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_mediator.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_metrics_utils.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/password_settings_view_controller.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/password_settings/scoped_password_settings_reauth_module_override.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/passwords_in_other_apps/passwords_in_other_apps_coordinator.h" |
| #import "ios/chrome/browser/settings/ui_bundled/password/reauthentication/reauthentication_coordinator.h" |
| #import "ios/chrome/browser/settings/ui_bundled/settings_navigation_controller.h" |
| #import "ios/chrome/browser/settings/ui_bundled/utils/password_utils.h" |
| #import "ios/chrome/browser/shared/coordinator/alert/alert_coordinator.h" |
| #import "ios/chrome/browser/shared/model/application_context/application_context.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/url/chrome_url_constants.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/open_new_tab_command.h" |
| #import "ios/chrome/browser/shared/public/commands/snackbar_commands.h" |
| #import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/browser/signin/model/authentication_service.h" |
| #import "ios/chrome/browser/signin/model/authentication_service_factory.h" |
| #import "ios/chrome/browser/signin/model/identity_manager_factory.h" |
| #import "ios/chrome/browser/signin/model/system_identity.h" |
| #import "ios/chrome/browser/signin/model/trusted_vault_client_backend.h" |
| #import "ios/chrome/browser/signin/model/trusted_vault_client_backend_factory.h" |
| #import "ios/chrome/browser/sync/model/sync_service_factory.h" |
| #import "ios/chrome/browser/webauthn/model/ios_passkey_model_factory.h" |
| #import "ios/chrome/common/ui/elements/branded_navigation_item_title_view.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 "net/base/apple/url_conversions.h" |
| #import "ui/base/l10n/l10n_util.h" |
| #import "ui/base/l10n/l10n_util_mac.h" |
| |
| namespace { |
| |
| // The user action for when the bulk move passwords to account confirmation |
| // dialog's cancel button is clicked. |
| constexpr const char* kBulkMovePasswordsToAccountConfirmationDialogCancelled = |
| "Mobile.PasswordsSettings.BulkSavePasswordsToAccountDialog.Cancelled"; |
| |
| // The user action for when the bulk move passwords to account confirmation |
| // dialog's accept button is clicked. |
| constexpr const char* kBulkMovePasswordsToAccountConfirmationDialogAccepted = |
| "Mobile.PasswordsSettings.BulkSavePasswordsToAccountDialog.Accepted"; |
| |
| // The user action for when the delete all saved data button is clicked. |
| constexpr const char* kDeleteAllSavedDataButtonClicked = |
| "IOS.PasswordManager.Settings.DeleteAllSavedData.Clicked"; |
| |
| // Represents the code of an error returned when the user dismisses the update |
| // GPM Pin flow by clicking the "Cancel" button. This should not be treated as |
| // an actual error. |
| const NSInteger kErrorUserDismissedUpdateGPMPinFlow = -105; |
| |
| } // namespace |
| |
| // Methods to update state in response to actions taken in the Export |
| // ActivityViewController. |
| @protocol ExportActivityViewControllerDelegate <NSObject> |
| |
| // Used to reset the export state when the activity view disappears. |
| - (void)resetExport; |
| |
| @end |
| |
| // Convenience wrapper around ActivityViewController for presenting share sheet |
| // at the end of the export flow. We do not use completionWithItemsHandler |
| // because it fails sometimes; see crbug.com/820053. |
| @interface ExportActivityViewController : UIActivityViewController |
| |
| - (instancetype)initWithActivityItems:(NSArray*)activityItems |
| delegate:(id<ExportActivityViewControllerDelegate>) |
| delegate; |
| |
| @end |
| |
| @implementation ExportActivityViewController { |
| __weak id<ExportActivityViewControllerDelegate> _weakDelegate; |
| } |
| |
| - (instancetype)initWithActivityItems:(NSArray*)activityItems |
| delegate:(id<ExportActivityViewControllerDelegate>) |
| delegate { |
| self = [super initWithActivityItems:activityItems applicationActivities:nil]; |
| if (self) { |
| _weakDelegate = delegate; |
| } |
| |
| return self; |
| } |
| |
| - (void)viewDidDisappear:(BOOL)animated { |
| [_weakDelegate resetExport]; |
| [super viewDidDisappear:animated]; |
| } |
| |
| @end |
| |
| @interface PasswordSettingsCoordinator () < |
| ExportActivityViewControllerDelegate, |
| BulkMoveLocalPasswordsToAccountHandler, |
| PasswordExportHandler, |
| PasswordsInOtherAppsCoordinatorDelegate, |
| PopoverLabelViewControllerDelegate, |
| ReauthenticationCoordinatorDelegate, |
| SettingsNavigationControllerDelegate> |
| |
| @end |
| |
| @implementation PasswordSettingsCoordinator { |
| // Main view controller for this coordinator. |
| PasswordSettingsViewController* _passwordSettingsViewController; |
| |
| // The presented SettingsNavigationController containing |
| // `passwordSettingsViewController`. |
| SettingsNavigationController* _settingsNavigationController; |
| |
| // The coupled mediator. |
| PasswordSettingsMediator* _mediator; |
| |
| // Command dispatcher. |
| __weak id<ApplicationCommands> _dispatcher; |
| |
| // Module handling reauthentication before accessing sensitive data. |
| ReauthenticationModule* _reauthModule; |
| |
| // Coordinator for the "Passwords in Other Apps" screen. |
| PasswordsInOtherAppsCoordinator* _passwordsInOtherAppsCoordinator; |
| |
| // Coordinator for blocking Password Settings until Local Authentication is |
| // passed. Used for requiring authentication when opening Password Settings |
| // from outside the Password Manager and when the app is |
| // backgrounded/foregrounded with Password Settings opened. |
| ReauthenticationCoordinator* _reauthCoordinator; |
| |
| // Service which gives us a view on users' saved passwords. |
| std::unique_ptr<password_manager::SavedPasswordsPresenter> |
| _savedPasswordsPresenter; |
| |
| // Alert informing the user that passwords are being prepared for |
| // export. |
| UIAlertController* _preparingPasswordsAlert; |
| |
| // For recording visits after successful authentication. |
| IOSPasswordManagerVisitsRecorder* _visitsRecorder; |
| |
| // Identity of the user. Can be nil if there is no primary account. |
| id<SystemIdentity> _identity; |
| |
| // Coordinator for displaying errors in update GPM PIN flow. |
| AlertCoordinator* _updateGPMPinErrorCoordinator; |
| } |
| |
| #pragma mark - ChromeCoordinator |
| |
| - (void)start { |
| ProfileIOS* profile = self.browser->GetProfile(); |
| |
| _reauthModule = password_manager::BuildReauthenticationModule(); |
| |
| _savedPasswordsPresenter = |
| std::make_unique<password_manager::SavedPasswordsPresenter>( |
| IOSChromeAffiliationServiceFactory::GetForProfile(profile), |
| IOSChromeProfilePasswordStoreFactory::GetForProfile( |
| profile, ServiceAccessType::EXPLICIT_ACCESS), |
| IOSChromeAccountPasswordStoreFactory::GetForProfile( |
| profile, ServiceAccessType::EXPLICIT_ACCESS), |
| IOSPasskeyModelFactory::GetForProfile(profile)); |
| |
| _identity = |
| AuthenticationServiceFactory::GetForProfile(profile)->GetPrimaryIdentity( |
| signin::ConsentLevel::kSignin); |
| _mediator = [[PasswordSettingsMediator alloc] |
| initWithReauthenticationModule:_reauthModule |
| savedPasswordsPresenter:_savedPasswordsPresenter.get() |
| bulkMovePasswordsToAccountHandler:self |
| exportHandler:self |
| prefService:profile->GetPrefs() |
| identityManager:IdentityManagerFactory::GetForProfile( |
| profile) |
| syncService:SyncServiceFactory::GetForProfile( |
| profile) |
| trustedVaultClientBackend:TrustedVaultClientBackendFactory:: |
| GetForProfile(profile) |
| identity:_identity]; |
| |
| _dispatcher = static_cast<id<ApplicationCommands>>( |
| self.browser->GetCommandDispatcher()); |
| |
| _passwordSettingsViewController = |
| [[PasswordSettingsViewController alloc] init]; |
| |
| _passwordSettingsViewController.presentationDelegate = self; |
| |
| _settingsNavigationController = [[SettingsNavigationController alloc] |
| initWithRootViewController:_passwordSettingsViewController |
| browser:self.browser |
| delegate:self]; |
| |
| _mediator.consumer = _passwordSettingsViewController; |
| _passwordSettingsViewController.delegate = _mediator; |
| |
| _visitsRecorder = [[IOSPasswordManagerVisitsRecorder alloc] |
| initWithPasswordManagerSurface:password_manager::PasswordManagerSurface:: |
| kPasswordSettings]; |
| |
| // Only record visit if no auth is required, otherwise wait for successful |
| // auth. |
| if (_skipAuthenticationOnStart) { |
| [_visitsRecorder maybeRecordVisitMetric]; |
| } |
| |
| [self startReauthCoordinatorWithAuthOnStart:!_skipAuthenticationOnStart]; |
| |
| [self.baseViewController presentViewController:_settingsNavigationController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)stop { |
| [self stopWithUIDismissal:YES]; |
| } |
| |
| #pragma mark - PasswordSettingsCoordinator |
| |
| - (void)stopWithUIDismissal:(BOOL)shouldDismissUI { |
| if (shouldDismissUI) { |
| [_settingsNavigationController.presentingViewController |
| dismissViewControllerAnimated:NO |
| completion:nil]; |
| } |
| |
| [_passwordsInOtherAppsCoordinator stop]; |
| _passwordsInOtherAppsCoordinator.delegate = nil; |
| _passwordsInOtherAppsCoordinator = nil; |
| |
| _passwordSettingsViewController.presentationDelegate = nil; |
| _passwordSettingsViewController.delegate = nil; |
| _passwordSettingsViewController = nil; |
| [_settingsNavigationController cleanUpSettings]; |
| _settingsNavigationController = nil; |
| _preparingPasswordsAlert = nil; |
| |
| _dispatcher = nil; |
| _reauthModule = nil; |
| |
| [_mediator disconnect]; |
| _mediator.consumer = nil; |
| _mediator = nil; |
| _savedPasswordsPresenter.reset(); |
| |
| [self stopReauthenticationCoordinator]; |
| } |
| |
| #pragma mark - PasswordSettingsPresentationDelegate |
| |
| - (void)startDeletionFlow { |
| base::RecordAction(base::UserMetricsAction(kDeleteAllSavedDataButtonClicked)); |
| CredentialCounts counts = [_mediator passwordAndPasskeyCounts]; |
| |
| NSString* alertDescription; |
| if (counts.passwordCounts == 0 && counts.passkeyCounts == 0) { |
| alertDescription = l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_SETTINGS_DELETE_ALL_CREDENTIALS_DESCRIPTION_BLOCK_SITES_ONLY); |
| } else if (counts.passwordCounts == 0) { |
| alertDescription = base::SysUTF16ToNSString( |
| base::i18n::MessageFormatter::FormatWithNamedArgs( |
| l10n_util::GetStringUTF16( |
| IDS_IOS_PASSWORD_SETTINGS_DELETE_ALL_CREDENTIALS_DESCRIPTION_NO_PASSWORDS), |
| "passkey_count", counts.passkeyCounts)); |
| } else if (counts.passkeyCounts == 0) { |
| alertDescription = base::SysUTF16ToNSString( |
| base::i18n::MessageFormatter::FormatWithNamedArgs( |
| l10n_util::GetStringUTF16( |
| IDS_IOS_PASSWORD_SETTINGS_DELETE_ALL_CREDENTIALS_DESCRIPTION_NO_PASSKEYS), |
| "password_count", counts.passwordCounts)); |
| } else { |
| alertDescription = base::SysUTF16ToNSString( |
| base::i18n::MessageFormatter::FormatWithNamedArgs( |
| l10n_util::GetStringUTF16( |
| IDS_IOS_PASSWORD_SETTINGS_DELETE_ALL_CREDENTIALS_DESCRIPTION), |
| "password_count", counts.passwordCounts, "passkey_count", |
| counts.passkeyCounts)); |
| } |
| |
| UIAlertController* deletionConfirmation = [UIAlertController |
| alertControllerWithTitle: |
| l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_SETTINGS_DELETE_ALL_CREDENTIALS_TITLE) |
| message:alertDescription |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| |
| UIAlertAction* cancelAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_CANCEL_PASSWORD_DELETION) |
| style:UIAlertActionStyleCancel |
| handler:^(UIAlertAction* action) { |
| base::UmaHistogramEnumeration( |
| "IOS.PasswordManager.Settings." |
| "DeleteAllSavedCredentialsActions", |
| password_manager::IOSDeleteAllSavedCredentialsActions:: |
| kDeleteAllSavedCredentialsCancelled); |
| }]; |
| [deletionConfirmation addAction:cancelAction]; |
| |
| __weak __typeof(self) weakSelf = self; |
| UIAlertAction* deleteAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_DELETE_ACTION_TITLE) |
| style:UIAlertActionStyleDestructive |
| handler:^(UIAlertAction* action) { |
| base::UmaHistogramEnumeration( |
| "IOS.PasswordManager.Settings." |
| "DeleteAllSavedCredentialsActions", |
| password_manager::IOSDeleteAllSavedCredentialsActions:: |
| kDeleteAllSavedCredentialsConfirmed); |
| [weakSelf showReauthDialogForDeletion]; |
| }]; |
| |
| [deletionConfirmation addAction:deleteAction]; |
| |
| deletionConfirmation.popoverPresentationController.sourceView = |
| [_passwordSettingsViewController sourceViewForAlerts]; |
| deletionConfirmation.popoverPresentationController.sourceRect = |
| [_passwordSettingsViewController sourceRectForCredentialDeletionAlerts]; |
| |
| [_passwordSettingsViewController presentViewController:deletionConfirmation |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)startExportFlow { |
| UIAlertController* exportConfirmation = [UIAlertController |
| alertControllerWithTitle:nil |
| message:l10n_util::GetNSString( |
| IDS_IOS_EXPORT_PASSWORDS_ALERT_MESSAGE) |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| exportConfirmation.view.accessibilityIdentifier = |
| kPasswordSettingsExportConfirmViewId; |
| |
| UIAlertAction* cancelAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_EXPORT_PASSWORDS_CANCEL_BUTTON) |
| style:UIAlertActionStyleCancel |
| handler:^(UIAlertAction* action){ |
| }]; |
| [exportConfirmation addAction:cancelAction]; |
| |
| __weak __typeof(self) weakSelf = self; |
| UIAlertAction* exportAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| [weakSelf onStartExportFlowConfirmed]; |
| }]; |
| |
| [exportConfirmation addAction:exportAction]; |
| |
| exportConfirmation.popoverPresentationController.sourceView = |
| [_passwordSettingsViewController sourceViewForAlerts]; |
| exportConfirmation.popoverPresentationController.sourceRect = |
| [_passwordSettingsViewController sourceRectForPasswordExportAlerts]; |
| |
| [_passwordSettingsViewController presentViewController:exportConfirmation |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showManagedPrefInfoForSourceView:(UIButton*)sourceView { |
| // EnterpriseInfoPopoverViewController automatically handles reenabling the |
| // `sourceView`, so we don't need to add any dismiss handlers or delegation, |
| // just present the bubble. |
| EnterpriseInfoPopoverViewController* bubbleViewController = |
| [[EnterpriseInfoPopoverViewController alloc] initWithEnterpriseName:nil]; |
| bubbleViewController.delegate = self; |
| |
| // Set the anchor and arrow direction of the bubble. |
| bubbleViewController.popoverPresentationController.sourceView = sourceView; |
| bubbleViewController.popoverPresentationController.sourceRect = |
| sourceView.bounds; |
| bubbleViewController.popoverPresentationController.permittedArrowDirections = |
| UIPopoverArrowDirectionAny; |
| |
| [_passwordSettingsViewController presentViewController:bubbleViewController |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showPasswordsInOtherAppsScreen { |
| DCHECK(!_passwordsInOtherAppsCoordinator); |
| [self stopReauthCoordinatorBeforeStartingChildCoordinator]; |
| _passwordsInOtherAppsCoordinator = [[PasswordsInOtherAppsCoordinator alloc] |
| initWithBaseNavigationController:_settingsNavigationController |
| browser:self.browser]; |
| _passwordsInOtherAppsCoordinator.delegate = self; |
| [_passwordsInOtherAppsCoordinator start]; |
| } |
| |
| - (void)showOnDeviceEncryptionSetUp { |
| GURL URL = google_util::AppendGoogleLocaleParam( |
| GURL(kOnDeviceEncryptionOptInURL), |
| GetApplicationContext()->GetApplicationLocale()); |
| OpenNewTabCommand* command = [OpenNewTabCommand commandWithURLFromChrome:URL]; |
| [_dispatcher closePresentedViewsAndOpenURL:command]; |
| } |
| |
| - (void)showOnDeviceEncryptionHelp { |
| GURL URL = GURL(kOnDeviceEncryptionLearnMoreURL); |
| OpenNewTabCommand* command = [OpenNewTabCommand commandWithURLFromChrome:URL]; |
| [_dispatcher closePresentedViewsAndOpenURL:command]; |
| } |
| |
| - (void)showChangeGPMPinDialog { |
| if (![_reauthModule canAttemptReauth]) { |
| [self |
| showSetPasscodeDialogWithContent: |
| l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_SETTINGS_CHANGE_PIN_SET_UP_PASSCODE_CONTENT)]; |
| return; |
| } |
| |
| __weak __typeof(self) weakSelf = self; |
| void (^onReauthFinished)(ReauthenticationResult) = |
| ^(ReauthenticationResult result) { |
| // Reauth can't be skipped for this flow. |
| CHECK(result != ReauthenticationResult::kSkipped); |
| |
| if (result == ReauthenticationResult::kSuccess) { |
| [weakSelf updateGPMPinForAccount]; |
| } |
| }; |
| |
| [_reauthModule |
| attemptReauthWithLocalizedReason:l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_SETTINGS_CHANGE_PIN) |
| canReusePreviousAuth:NO |
| handler:onReauthFinished]; |
| } |
| |
| #pragma mark - PopoverLabelViewControllerDelegate |
| |
| - (void)didTapLinkURL:(NSURL*)URL { |
| [_dispatcher |
| openURLInNewTab:[OpenNewTabCommand |
| commandWithURLFromChrome:net::GURLWithNSURL(URL) |
| inIncognito:NO]]; |
| } |
| |
| #pragma mark - BulkMoveLocalPasswordsToAccountHandler |
| |
| - (void)showAuthenticationForMovePasswordsToAccountWithMessage: |
| (NSString*)message { |
| [_mediator userDidStartBulkMoveLocalPasswordsToAccountFlow]; |
| } |
| |
| - (void)showConfirmationDialogWithAlertTitle:(NSString*)alertTitle |
| alertDescription:(NSString*)alertDescription { |
| // Create the confirmation alert. |
| UIAlertController* movePasswordsConfirmation = [UIAlertController |
| alertControllerWithTitle:alertTitle |
| message:alertDescription |
| preferredStyle:UIAlertControllerStyleActionSheet]; |
| movePasswordsConfirmation.view.accessibilityIdentifier = |
| kPasswordSettingsBulkMovePasswordsToAccountAlertViewId; |
| |
| // Create the cancel action. |
| UIAlertAction* cancelAction = [UIAlertAction |
| actionWithTitle: |
| l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_SETTINGS_BULK_UPLOAD_PASSWORDS_ALERT_CANCEL) |
| style:UIAlertActionStyleCancel |
| handler:^(UIAlertAction* action) { |
| base::RecordAction(base::UserMetricsAction( |
| kBulkMovePasswordsToAccountConfirmationDialogCancelled)); |
| }]; |
| [movePasswordsConfirmation addAction:cancelAction]; |
| |
| // Create the accept action (i.e. move passwords to account). |
| __weak __typeof(self) weakSelf = self; |
| UIAlertAction* movePasswordsAction = [UIAlertAction |
| actionWithTitle: |
| l10n_util::GetNSString( |
| IDS_IOS_PASSWORD_SETTINGS_BULK_UPLOAD_PASSWORDS_ALERT_BUTTON) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction* action) { |
| base::RecordAction(base::UserMetricsAction( |
| kBulkMovePasswordsToAccountConfirmationDialogAccepted)); |
| [weakSelf |
| showAuthenticationForMovePasswordsToAccountWithMessage: |
| alertTitle]; |
| }]; |
| |
| [movePasswordsConfirmation addAction:movePasswordsAction]; |
| |
| movePasswordsConfirmation.popoverPresentationController.sourceView = |
| [_passwordSettingsViewController sourceViewForAlerts]; |
| movePasswordsConfirmation.popoverPresentationController.sourceRect = |
| [_passwordSettingsViewController sourceRectForBulkMovePasswordsToAccount]; |
| |
| // Show the alert. |
| [_passwordSettingsViewController |
| presentViewController:movePasswordsConfirmation |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showMovedToAccountSnackbarWithPasswordCount:(int)count |
| userEmail:(std::string)email { |
| std::u16string pattern = l10n_util::GetStringUTF16( |
| IDS_IOS_PASSWORD_SETTINGS_BULK_UPLOAD_PASSWORDS_SNACKBAR_MESSAGE); |
| std::u16string result = base::i18n::MessageFormatter::FormatWithNamedArgs( |
| pattern, "COUNT", count, "EMAIL", base::UTF8ToUTF16(email)); |
| |
| TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess); |
| id<SnackbarCommands> handler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), SnackbarCommands); |
| [handler showSnackbarWithMessage:base::SysUTF16ToNSString(result) |
| buttonText:nil |
| messageAction:nil |
| completionAction:nil]; |
| } |
| |
| #pragma mark - PasswordExportHandler |
| |
| - (void)showActivityViewWithActivityItems:(NSArray*)activityItems |
| completionHandler:(void (^)(NSString* activityType, |
| BOOL completed, |
| NSArray* returnedItems, |
| NSError* activityError)) |
| completionHandler { |
| ExportActivityViewController* activityViewController = |
| [[ExportActivityViewController alloc] initWithActivityItems:activityItems |
| delegate:self]; |
| NSArray* excludedActivityTypes = @[ |
| UIActivityTypeAddToReadingList, UIActivityTypeAirDrop, |
| UIActivityTypeCopyToPasteboard, UIActivityTypeOpenInIBooks, |
| UIActivityTypePostToFacebook, UIActivityTypePostToFlickr, |
| UIActivityTypePostToTencentWeibo, UIActivityTypePostToTwitter, |
| UIActivityTypePostToVimeo, UIActivityTypePostToWeibo, UIActivityTypePrint |
| ]; |
| [activityViewController setExcludedActivityTypes:excludedActivityTypes]; |
| |
| [activityViewController setCompletionWithItemsHandler:completionHandler]; |
| |
| UIView* sourceView = [_passwordSettingsViewController sourceViewForAlerts]; |
| CGRect sourceRect = |
| [_passwordSettingsViewController sourceRectForPasswordExportAlerts]; |
| |
| activityViewController.modalPresentationStyle = UIModalPresentationPopover; |
| activityViewController.popoverPresentationController.sourceView = sourceView; |
| activityViewController.popoverPresentationController.sourceRect = sourceRect; |
| activityViewController.popoverPresentationController |
| .permittedArrowDirections = |
| UIPopoverArrowDirectionUp | UIPopoverArrowDirectionDown; |
| |
| [self presentViewControllerForExportFlow:activityViewController]; |
| } |
| |
| - (void)showExportErrorAlertWithLocalizedReason:(NSString*)localizedReason { |
| UIAlertController* alertController = [UIAlertController |
| alertControllerWithTitle:l10n_util::GetNSString( |
| IDS_IOS_EXPORT_PASSWORDS_FAILED_ALERT_TITLE) |
| message:localizedReason |
| preferredStyle:UIAlertControllerStyleAlert]; |
| UIAlertAction* okAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_OK) |
| style:UIAlertActionStyleDefault |
| handler:nil]; |
| [alertController addAction:okAction]; |
| [self presentViewControllerForExportFlow:alertController]; |
| } |
| |
| - (void)showPreparingPasswordsAlert { |
| _preparingPasswordsAlert = [UIAlertController |
| alertControllerWithTitle: |
| l10n_util::GetNSString(IDS_IOS_EXPORT_PASSWORDS_PREPARING_ALERT_TITLE) |
| message:nil |
| preferredStyle:UIAlertControllerStyleAlert]; |
| __weak __typeof(self) weakSelf = self; |
| UIAlertAction* cancelAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_EXPORT_PASSWORDS_CANCEL_BUTTON) |
| style:UIAlertActionStyleCancel |
| handler:^(UIAlertAction*) { |
| [weakSelf onExportFlowCancelled]; |
| }]; |
| [_preparingPasswordsAlert addAction:cancelAction]; |
| [_passwordSettingsViewController |
| presentViewController:_preparingPasswordsAlert |
| animated:YES |
| completion:nil]; |
| } |
| |
| - (void)showSetPasscodeForPasswordExportDialog { |
| [self showSetPasscodeDialogWithContent: |
| l10n_util::GetNSString( |
| IDS_IOS_SETTINGS_EXPORT_PASSWORDS_SET_UP_SCREENLOCK_CONTENT)]; |
| } |
| |
| #pragma mark - ExportActivityViewControllerDelegate |
| |
| - (void)resetExport { |
| [_mediator userDidCompleteExportFlow]; |
| } |
| |
| #pragma mark - PasswordsInOtherAppsCoordinatorDelegate |
| |
| - (void)passwordsInOtherAppsCoordinatorDidRemove: |
| (PasswordsInOtherAppsCoordinator*)coordinator { |
| DCHECK_EQ(_passwordsInOtherAppsCoordinator, coordinator); |
| [_passwordsInOtherAppsCoordinator stop]; |
| _passwordsInOtherAppsCoordinator.delegate = nil; |
| _passwordsInOtherAppsCoordinator = nil; |
| [self restartReauthCoordinator]; |
| } |
| |
| #pragma mark - PasswordManagerReauthenticationDelegate |
| |
| - (void)dismissPasswordManagerAfterFailedReauthentication { |
| [_delegate dismissPasswordManagerAfterFailedReauthentication]; |
| } |
| |
| #pragma mark - SettingsNavigationControllerDelegate |
| |
| - (void)closeSettings { |
| // Dismiss UI and notify parent coordinator. |
| __weak __typeof(self) weakSelf = self; |
| [self.baseViewController dismissViewControllerAnimated:YES |
| completion:^{ |
| [weakSelf settingsWasDismissed]; |
| }]; |
| } |
| |
| - (void)settingsWasDismissed { |
| [self.delegate passwordSettingsCoordinatorDidRemove:self]; |
| } |
| |
| #pragma mark - ReauthenticationCoordinatorDelegate |
| |
| - (void)successfulReauthenticationWithCoordinator: |
| (ReauthenticationCoordinator*)coordinator { |
| [_visitsRecorder maybeRecordVisitMetric]; |
| } |
| |
| - (void)dismissUIAfterFailedReauthenticationWithCoordinator: |
| (ReauthenticationCoordinator*)coordinator { |
| CHECK_EQ(_reauthCoordinator, coordinator); |
| [_delegate dismissPasswordManagerAfterFailedReauthentication]; |
| } |
| |
| - (void)willPushReauthenticationViewController { |
| // Cancel password export flow before authentication UI is presented. |
| if (_preparingPasswordsAlert.beingPresented) { |
| [_preparingPasswordsAlert dismissViewControllerAnimated:NO completion:nil]; |
| [_mediator exportFlowCanceled]; |
| _preparingPasswordsAlert = nil; |
| } |
| } |
| |
| #pragma mark - Private |
| |
| // Closes the settings and load the passcode help article in a new tab. |
| - (void)showPasscodeHelp { |
| GURL URL = GURL(kPasscodeArticleURL); |
| OpenNewTabCommand* command = [OpenNewTabCommand commandWithURLFromChrome:URL]; |
| [_dispatcher closePresentedViewsAndOpenURL:command]; |
| } |
| |
| // Helper to show the "set passcode" dialog with customizable content. |
| - (void)showSetPasscodeDialogWithContent:(NSString*)content { |
| UIAlertController* alertController = [UIAlertController |
| alertControllerWithTitle:l10n_util::GetNSString( |
| IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_TITLE) |
| message:content |
| preferredStyle:UIAlertControllerStyleAlert]; |
| |
| __weak __typeof(self) weakSelf = self; |
| UIAlertAction* learnAction = [UIAlertAction |
| actionWithTitle:l10n_util::GetNSString( |
| IDS_IOS_SETTINGS_SET_UP_SCREENLOCK_LEARN_HOW) |
| style:UIAlertActionStyleDefault |
| handler:^(UIAlertAction*) { |
| [weakSelf showPasscodeHelp]; |
| }]; |
| [alertController addAction:learnAction]; |
| UIAlertAction* okAction = |
| [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_OK) |
| style:UIAlertActionStyleDefault |
| handler:nil]; |
| [alertController addAction:okAction]; |
| alertController.preferredAction = okAction; |
| [_passwordSettingsViewController presentViewController:alertController |
| animated:YES |
| completion:nil]; |
| } |
| |
| // Helper method for presenting several ViewControllers used in the export flow. |
| // Ensures that the "Preparing passwords" alert is dismissed when something is |
| // ready to replace it. |
| - (void)presentViewControllerForExportFlow:(UIViewController*)viewController { |
| if (_preparingPasswordsAlert.beingPresented) { |
| __weak __typeof(self) weakSelf = self; |
| [_preparingPasswordsAlert |
| dismissViewControllerAnimated:YES |
| completion:^{ |
| [weakSelf presentViewControllerForExportFlow: |
| viewController]; |
| }]; |
| } else { |
| [_passwordSettingsViewController presentViewController:viewController |
| animated:YES |
| completion:nil]; |
| } |
| } |
| |
| // Starts reauthCoordinator. |
| // - authOnStart: Pass `YES` to cover Password Settings with an empty view |
| // controller until successful Local Authentication when reauthCoordinator |
| // starts. |
| // |
| // Local authentication is required every time the current |
| // scene is backgrounded and foregrounded until reauthCoordinator is stopped. |
| - (void)startReauthCoordinatorWithAuthOnStart:(BOOL)authOnStart { |
| if (_reauthCoordinator) { |
| // The previous reauth coordinator should have been stopped and deallocated |
| // by now. Create a crash report without crashing and gracefully handle the |
| // error by cleaning up the old coordinator. |
| base::debug::DumpWithoutCrashing(); |
| [_reauthCoordinator stopAndPopViewController]; |
| } |
| |
| _reauthCoordinator = [[ReauthenticationCoordinator alloc] |
| initWithBaseNavigationController:_settingsNavigationController |
| browser:self.browser |
| reauthenticationModule:_reauthModule |
| authOnStart:authOnStart]; |
| |
| _reauthCoordinator.delegate = self; |
| |
| [_reauthCoordinator start]; |
| } |
| |
| // Stops reauthCoordinator. |
| - (void)stopReauthenticationCoordinator { |
| [_reauthCoordinator stop]; |
| _reauthCoordinator.delegate = nil; |
| _reauthCoordinator = nil; |
| } |
| |
| // 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 { |
| // See PasswordsCoordinator |
| // stopReauthCoordinatorBeforeStartingChildCoordinator. |
| [_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]; |
| } |
| |
| // Starts `_updateGPMPinErrorCoordinator` from the currently visible view |
| // controller (which should be the update GPM Pin VC). The cancel completion |
| // should close both of them. |
| - (void)startUpdateGPMPinErrorCoordinator { |
| NSString* title = |
| l10n_util::GetNSString(IDS_IOS_PASSWORD_SETTINGS_UPDATE_PIN_ERROR_TITLE); |
| NSString* message = |
| l10n_util::GetNSString(IDS_IOS_PASSWORD_SETTINGS_UPDATE_PIN_ERROR); |
| NSString* buttonTitle = |
| l10n_util::GetNSString(IDS_IOS_PASSWORD_SETTINGS_UPDATE_PIN_ERROR_BUTTON); |
| |
| [_updateGPMPinErrorCoordinator stop]; |
| _updateGPMPinErrorCoordinator = [[AlertCoordinator alloc] |
| initWithBaseViewController:_settingsNavigationController |
| .visibleViewController |
| browser:self.browser |
| title:title |
| message:message]; |
| __weak __typeof(self) weakSelf = self; |
| [_updateGPMPinErrorCoordinator |
| addItemWithTitle:buttonTitle |
| action:^() { |
| [weakSelf dismissUpdateGPMPinViewController]; |
| [weakSelf stopUpdateGPMPinErrorCoordinator]; |
| } |
| style:UIAlertActionStyleCancel]; |
| [_updateGPMPinErrorCoordinator start]; |
| } |
| |
| // Stops `_updateGPMPinErrorCoordinator`. |
| - (void)stopUpdateGPMPinErrorCoordinator { |
| [_updateGPMPinErrorCoordinator stop]; |
| _updateGPMPinErrorCoordinator = nil; |
| } |
| |
| // Dismisses the view controller displayed by trusted vault client backend for |
| // the update GPM Pin flow. |
| - (void)dismissUpdateGPMPinViewController { |
| [_settingsNavigationController.topViewController |
| dismissViewControllerAnimated:YES |
| completion:nil]; |
| } |
| |
| // Starts the deletion all credentials flow after the user confirmed the |
| // corresponding alerts. |
| - (void)onStartDeletionFlowConfirmed { |
| [_mediator userDidStartDeleteFlow]; |
| } |
| |
| // Starts the export passwords flow after the user confirmed the corresponding |
| // alert. |
| - (void)onStartExportFlowConfirmed { |
| [_mediator userDidStartExportFlow]; |
| } |
| |
| // Cancels the password export flow. |
| - (void)onExportFlowCancelled { |
| [_mediator exportFlowCanceled]; |
| } |
| |
| // Handles update GPM Pin flow completion. If there is an `error` other than |
| // user dismissing the flow by clicking "Cancel", presents the error alert. |
| // Otherwise, dismisses the UI. |
| - (void)updateGPMPinFinishedWithError:(NSError*)error { |
| if (error && error.code != kErrorUserDismissedUpdateGPMPinFlow) { |
| [self startUpdateGPMPinErrorCoordinator]; |
| } else { |
| [self dismissUpdateGPMPinViewController]; |
| } |
| } |
| |
| // Starts the update GPM Pin flow. This should happen after succesful reauth. |
| - (void)updateGPMPinForAccount { |
| __weak __typeof(self) weakSelf = self; |
| TrustedVaultClientBackendFactory::GetForProfile(self.browser->GetProfile()) |
| ->UpdateGPMPinForAccount( |
| _identity, trusted_vault::SecurityDomainId::kPasskeys, |
| _settingsNavigationController, |
| password_manager::CreatePasswordManagerTitleView( |
| l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER)), |
| base::BindOnce(^(NSError* error) { |
| [weakSelf updateGPMPinFinishedWithError:error]; |
| })); |
| } |
| |
| - (void)showReauthDialogForDeletion { |
| if (![_reauthModule canAttemptReauth]) { |
| [self |
| showSetPasscodeDialogWithContent: |
| l10n_util::GetNSString( |
| IDS_IOS_SETTINGS_DELETE_ALL_CREDENTIALS_SET_UP_PASSCODE_CONTENT)]; |
| } else { |
| [self startAuthentication]; |
| } |
| } |
| |
| - (void)startAuthentication { |
| __weak __typeof(self) weakSelf = self; |
| void (^onReauthFinished)(ReauthenticationResult) = |
| ^(ReauthenticationResult result) { |
| // Reauth can't be skipped for this flow. |
| CHECK_NE(result, ReauthenticationResult::kSkipped); |
| |
| if (result == ReauthenticationResult::kSuccess) { |
| [weakSelf onStartDeletionFlowConfirmed]; |
| } |
| }; |
| |
| [_reauthModule |
| attemptReauthWithLocalizedReason: |
| l10n_util::GetNSString(IDS_IOS_SETTINGS_DELETE_ALL_CREDENTIALS) |
| canReusePreviousAuth:NO |
| handler:onReauthFinished]; |
| } |
| |
| @end |