chromium/ios/chrome/credential_provider_extension/ui/credential_list_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/credential_provider_extension/ui/credential_list_coordinator.h"

#import <AuthenticationServices/AuthenticationServices.h>
#import <UIKit/UIKit.h>

#import "ios/chrome/common/app_group/app_group_constants.h"
#import "ios/chrome/common/credential_provider/constants.h"
#import "ios/chrome/common/ui/confirmation_alert/confirmation_alert_action_handler.h"
#import "ios/chrome/credential_provider_extension/passkey_util.h"
#import "ios/chrome/credential_provider_extension/password_util.h"
#import "ios/chrome/credential_provider_extension/reauthentication_handler.h"
#import "ios/chrome/credential_provider_extension/ui/credential_details_consumer.h"
#import "ios/chrome/credential_provider_extension/ui/credential_details_view_controller.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_mediator.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_ui_handler.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_view_controller.h"
#import "ios/chrome/credential_provider_extension/ui/credential_response_handler.h"
#import "ios/chrome/credential_provider_extension/ui/empty_credentials_view_controller.h"
#import "ios/chrome/credential_provider_extension/ui/feature_flags.h"
#import "ios/chrome/credential_provider_extension/ui/new_password_coordinator.h"

@interface CredentialListCoordinator () <ConfirmationAlertActionHandler,
                                         CredentialListUIHandler,
                                         CredentialDetailsConsumerDelegate,
                                         NewPasswordCoordinatorDelegate>

// Base view controller from where `viewController` is presented.
@property(nonatomic, weak) UIViewController* baseViewController;

// The view controller of this coordinator.
@property(nonatomic, strong) UINavigationController* viewController;

// The mediator of this coordinator.
@property(nonatomic, strong) CredentialListMediator* mediator;

// Interface for the persistent credential store.
@property(nonatomic, weak) id<CredentialStore> credentialStore;

// The service identifiers to prioritize in a match is found.
@property(nonatomic, strong)
    NSArray<ASCredentialServiceIdentifier*>* serviceIdentifiers;

// Information about a passkey credential request.
@property(nonatomic, strong)
    ASPasskeyCredentialRequestParameters* requestParameters API_AVAILABLE(
        ios(17.0));

// Coordinator that shows a view for the user to create a new password.
@property(nonatomic, strong) NewPasswordCoordinator* createPasswordCoordinator;

// Interface for `reauthenticationModule`, handling mostly the case when no
// hardware for authentication is available.
@property(nonatomic, weak) ReauthenticationHandler* reauthenticationHandler;

// The handler to use when a credential is selected.
@property(nonatomic, weak) id<CredentialResponseHandler>
    credentialResponseHandler;

@end

@implementation CredentialListCoordinator

- (instancetype)
    initWithBaseViewController:(UIViewController*)baseViewController
               credentialStore:(id<CredentialStore>)credentialStore
            serviceIdentifiers:
                (NSArray<ASCredentialServiceIdentifier*>*)serviceIdentifiers
       reauthenticationHandler:(ReauthenticationHandler*)reauthenticationHandler
     credentialResponseHandler:
         (id<CredentialResponseHandler>)credentialResponseHandler {
  self = [super init];
  if (self) {
    _baseViewController = baseViewController;
    _serviceIdentifiers = serviceIdentifiers;
    _credentialStore = credentialStore;
    _reauthenticationHandler = reauthenticationHandler;
    _credentialResponseHandler = credentialResponseHandler;
  }
  return self;
}

- (void)start {
  CredentialListViewController* credentialListViewController =
      [[CredentialListViewController alloc] init];
  self.mediator = [[CredentialListMediator alloc]
               initWithConsumer:credentialListViewController
                      UIHandler:self
                credentialStore:self.credentialStore
             serviceIdentifiers:self.serviceIdentifiers
      credentialResponseHandler:self.credentialResponseHandler];

  self.viewController = [[UINavigationController alloc]
      initWithRootViewController:credentialListViewController];
  self.viewController.modalPresentationStyle =
      UIModalPresentationCurrentContext;
  [self.baseViewController presentViewController:self.viewController
                                        animated:NO
                                      completion:nil];
  [self.mediator fetchCredentials];
}

- (void)stop {
  if (self.createPasswordCoordinator) {
    [self.createPasswordCoordinator stop];
    self.createPasswordCoordinator = nil;
  }
  [self.viewController.presentingViewController
      dismissViewControllerAnimated:NO
                         completion:nil];
  self.viewController = nil;
  self.mediator = nil;
}

#pragma mark - CredentialListUIHandler

- (void)showEmptyCredentials {
  EmptyCredentialsViewController* emptyCredentialsViewController =
      [[EmptyCredentialsViewController alloc] init];
  emptyCredentialsViewController.modalPresentationStyle =
      UIModalPresentationOverCurrentContext;
  emptyCredentialsViewController.actionHandler = self;
  [self.viewController presentViewController:emptyCredentialsViewController
                                    animated:YES
                                  completion:nil];
}

- (void)userSelectedCredential:(id<Credential>)credential {
  [self reauthenticateIfNeededWithCompletionHandler:^(
            ReauthenticationResult result) {
    if (result != ReauthenticationResult::kFailure) {
      if (!credential.isPasskey) {
        ASPasswordCredential* passwordCredential =
            [ASPasswordCredential credentialWithUser:credential.username
                                            password:credential.password];
        [self.credentialResponseHandler
            userSelectedPassword:passwordCredential];
      } else if (@available(iOS 17.0, *)) {
        // TODO(crbug.com/330355124): Handle
        // self.requestParameters.userVerificationPreference.

        __weak __typeof(self) weakSelf = self;
        FetchKeyCompletionBlock completion = ^(NSData* securityDomainSecret) {
          CredentialListCoordinator* strongSelf = weakSelf;
          if (!strongSelf) {
            return;
          }

          ASPasskeyAssertionCredential* passkeyCredential =
              PerformPasskeyAssertion(
                  credential, strongSelf.requestParameters.clientDataHash,
                  strongSelf.allowedCredentials, securityDomainSecret);
          [strongSelf.credentialResponseHandler
              userSelectedPasskey:passkeyCredential];
        };

        FetchSecurityDomainSecret(completion);
      }
    }
  }];
}

- (void)showDetailsForCredential:(id<Credential>)credential {
  CredentialDetailsViewController* detailsViewController =
      [[CredentialDetailsViewController alloc] init];
  detailsViewController.delegate = self;
  [detailsViewController presentCredential:credential];

  [self.viewController pushViewController:detailsViewController animated:YES];
}

- (void)showCreateNewPasswordUI {
  self.createPasswordCoordinator = [[NewPasswordCoordinator alloc]
      initWithBaseViewController:self.viewController
              serviceIdentifiers:self.serviceIdentifiers
             existingCredentials:self.credentialStore
       credentialResponseHandler:self.credentialResponseHandler];
  self.createPasswordCoordinator.delegate = self;
  [self.createPasswordCoordinator start];
}

- (NSArray<NSData*>*)allowedCredentials {
  if (@available(iOS 17.0, *)) {
    return self.requestParameters.allowedCredentials;
  } else {
    return nil;
  }
}

- (BOOL)isRequestingPasskey {
  if (@available(iOS 17.0, *)) {
    return self.requestParameters != nil;
  } else {
    return NO;
  }
}

#pragma mark - CredentialDetailsConsumerDelegate

- (void)navigationCancelButtonWasPressed:(UIButton*)button {
  [self.credentialResponseHandler
      userCancelledRequestWithErrorCode:ASExtensionErrorCodeUserCanceled];
}

- (void)unlockPasswordForCredential:(id<Credential>)credential
                  completionHandler:(void (^)(NSString*))completionHandler {
  [self reauthenticateIfNeededWithCompletionHandler:^(
            ReauthenticationResult result) {
    if (result != ReauthenticationResult::kFailure) {
      completionHandler(credential.password);
    }
  }];
}

#pragma mark - ConfirmationAlertActionHandler

- (void)confirmationAlertDismissAction {
  // Finish the extension. There is no recovery from the empty credentials
  // state.
  [self.credentialResponseHandler
      userCancelledRequestWithErrorCode:ASExtensionErrorCodeUserCanceled];
}

- (void)confirmationAlertPrimaryAction {
  // No-op.
}

#pragma mark - NewPasswordCoordinatorDelegate

- (void)dismissNewPasswordCoordinator:
    (NewPasswordCoordinator*)newPasswordCoordinator {
  [self.createPasswordCoordinator stop];
  self.createPasswordCoordinator = nil;
}

#pragma mark - Private

// Asks user for hardware reauthentication if needed.
- (void)reauthenticateIfNeededWithCompletionHandler:
    (void (^)(ReauthenticationResult))completionHandler {
  [self.reauthenticationHandler
      verifyUserWithCompletionHandler:completionHandler
      presentReminderOnViewController:self.viewController];
}

@end