// 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_mediator.h"
#import <AuthenticationServices/AuthenticationServices.h>
#import "ios/chrome/common/credential_provider/credential_store.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_consumer.h"
#import "ios/chrome/credential_provider_extension/ui/credential_list_ui_handler.h"
#import "ios/chrome/credential_provider_extension/ui/credential_response_handler.h"
#import "ios/chrome/credential_provider_extension/ui/feature_flags.h"
#import "ios/chrome/credential_provider_extension/ui/ui_util.h"
@interface CredentialListMediator () <CredentialListHandler>
// The UI Handler of the feature.
@property(nonatomic, weak) id<CredentialListUIHandler> UIHandler;
// The consumer for this mediator.
@property(nonatomic, weak) id<CredentialListConsumer> consumer;
// Interface for the persistent credential store.
@property(nonatomic, weak) id<CredentialStore> credentialStore;
// The service identifiers to be prioritized.
@property(nonatomic, strong)
NSArray<ASCredentialServiceIdentifier*>* serviceIdentifiers;
// List of suggested credentials.
@property(nonatomic, copy) NSArray<id<Credential>>* suggestedCredentials;
// List of all credentials.
@property(nonatomic, copy) NSArray<id<Credential>>* allCredentials;
// The response handler for any credential actions.
@property(nonatomic, weak) id<CredentialResponseHandler>
credentialResponseHandler;
@end
@implementation CredentialListMediator
- (instancetype)initWithConsumer:(id<CredentialListConsumer>)consumer
UIHandler:(id<CredentialListUIHandler>)UIHandler
credentialStore:(id<CredentialStore>)credentialStore
serviceIdentifiers:
(NSArray<ASCredentialServiceIdentifier*>*)serviceIdentifiers
credentialResponseHandler:
(id<CredentialResponseHandler>)credentialResponseHandler {
self = [super init];
if (self) {
_serviceIdentifiers = serviceIdentifiers ?: @[];
_UIHandler = UIHandler;
_consumer = consumer;
_consumer.delegate = self;
_credentialStore = credentialStore;
_credentialResponseHandler = credentialResponseHandler;
}
return self;
}
- (void)fetchCredentials {
[self.consumer
setTopPrompt:PromptForServiceIdentifiers(self.serviceIdentifiers)];
dispatch_queue_t priorityQueue =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(priorityQueue, ^{
self.allCredentials = [self fetchAllCredentials];
self.suggestedCredentials = [self.UIHandler isRequestingPasskey]
? [self filterPasskeyCredentials]
: [self filterPasswordCredentials];
dispatch_async(dispatch_get_main_queue(), ^{
[self presentCredentials];
});
});
}
#pragma mark - CredentialListHandler
- (void)navigationCancelButtonWasPressed:(UIButton*)button {
[self.credentialResponseHandler
userCancelledRequestWithErrorCode:ASExtensionErrorCodeUserCanceled];
}
- (void)userSelectedCredential:(id<Credential>)credential {
[self.UIHandler userSelectedCredential:credential];
}
- (void)updateResultsWithFilter:(NSString*)filter {
// TODO(crbug.com/40215043): Remove the serviceIdentifier check once the
// new password screen properly supports user url entry.
BOOL showNewPasswordOption = !filter.length &&
IsPasswordCreationUserEnabled() &&
self.serviceIdentifiers.count > 0;
if (!filter.length) {
[self.consumer presentSuggestedCredentials:self.suggestedCredentials
allCredentials:self.allCredentials
showSearchBar:YES
showNewPasswordOption:showNewPasswordOption];
return;
}
NSMutableArray<id<Credential>>* suggested = [[NSMutableArray alloc] init];
for (id<Credential> credential in self.suggestedCredentials) {
if ([credential.serviceName localizedStandardContainsString:filter] ||
[credential.username localizedStandardContainsString:filter]) {
[suggested addObject:credential];
}
}
NSMutableArray<id<Credential>>* all = [[NSMutableArray alloc] init];
for (id<Credential> credential in self.allCredentials) {
if ([credential.serviceName localizedStandardContainsString:filter] ||
[credential.username localizedStandardContainsString:filter]) {
[all addObject:credential];
}
}
[self.consumer presentSuggestedCredentials:suggested
allCredentials:all
showSearchBar:YES
showNewPasswordOption:showNewPasswordOption];
}
- (void)showDetailsForCredential:(id<Credential>)credential {
[self.UIHandler showDetailsForCredential:credential];
}
- (void)newPasswordWasSelected {
[self.UIHandler showCreateNewPasswordUI];
}
#pragma mark - Private
// Returns all credentials from the credential store, filtered by request type
// and sorted by service name.
- (NSArray<id<Credential>>*)fetchAllCredentials {
BOOL isRequestingPasskey = [self.UIHandler isRequestingPasskey];
// Only use passwords or passkeys, depending on what's requested.
NSArray<id<Credential>>* credentials = [self.credentialStore.credentials
filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
id<Credential> credential,
NSDictionary* bindings) {
return credential.isPasskey == isRequestingPasskey;
}]];
credentials = [credentials sortedArrayUsingComparator:^NSComparisonResult(
id<Credential> obj1, id<Credential> obj2) {
return isRequestingPasskey ? [obj1.rpId compare:obj2.rpId]
: [obj1.serviceName compare:obj2.serviceName];
}];
return credentials;
}
// Returns the list of allowed passkey credentials for the relying party.
- (NSArray<id<Credential>>*)filterPasskeyCredentials {
// If the allowedCredentials array is empty, then the relying party accepts
// any passkey credential.
NSArray<NSData*>* allowedCredentials = [self.UIHandler allowedCredentials];
if (allowedCredentials.count == 0) {
return self.allCredentials;
}
NSMutableArray* filteredCredentials = [[NSMutableArray alloc] init];
for (id<Credential> credential in self.allCredentials) {
if ([allowedCredentials containsObject:credential.credentialId]) {
[filteredCredentials addObject:credential];
}
}
return filteredCredentials;
}
// Returns the list of allowed password credentials for the service identifier.
- (NSArray<id<Credential>>*)filterPasswordCredentials {
NSMutableArray* filteredCredentials = [[NSMutableArray alloc] init];
for (id<Credential> credential in self.allCredentials) {
for (ASCredentialServiceIdentifier* identifier in self.serviceIdentifiers) {
if (credential.serviceName &&
[identifier.identifier
localizedStandardContainsString:credential.serviceName]) {
[filteredCredentials addObject:credential];
break;
}
if (credential.serviceIdentifier &&
[identifier.identifier
localizedStandardContainsString:credential.serviceIdentifier]) {
[filteredCredentials addObject:credential];
break;
}
}
}
return filteredCredentials;
}
// Tells the consumer to show the passed in suggested and all credentials.
- (void)presentCredentials {
// TODO(crbug.com/40215043): Remove the serviceIdentifier check once the
// new password screen properly supports user url entry.
BOOL canCreatePassword = ![self.UIHandler isRequestingPasskey] &&
IsPasswordCreationUserEnabled() &&
self.serviceIdentifiers.count > 0;
if (!canCreatePassword && !self.allCredentials.count) {
[self.UIHandler showEmptyCredentials];
return;
}
[self.consumer presentSuggestedCredentials:self.suggestedCredentials
allCredentials:self.allCredentials
showSearchBar:self.allCredentials.count > 0
showNewPasswordOption:canCreatePassword];
}
@end