chromium/ios/web_view/shell/shell_autofill_delegate.m

// Copyright 2017 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/web_view/shell/shell_autofill_delegate.h"

#import <UIKit/UIKit.h>

#import "ios/web_view/shell/shell_risk_data_loader.h"

@interface ShellAutofillDelegate ()

// Autofill controller.
@property(nonatomic, strong) CWVAutofillController* autofillController;

// Risk data loader.
@property(nonatomic, strong) ShellRiskDataLoader* riskDataLoader;

// Returns an action for a suggestion.
- (UIAlertAction*)actionForSuggestion:(CWVAutofillSuggestion*)suggestion
                              atIndex:(NSInteger)index;

@end

@implementation ShellAutofillDelegate

@synthesize autofillController = _autofillController;
@synthesize riskDataLoader = _riskDataLoader;

- (instancetype)init {
  self = [super init];
  if (self) {
    _riskDataLoader = [[ShellRiskDataLoader alloc] init];
  }
  return self;
}

#pragma mark - CWVAutofillControllerDelegate methods

- (void)autofillController:(CWVAutofillController*)autofillController
    didFocusOnFieldWithIdentifier:(NSString*)fieldIdentifier
                        fieldType:(NSString*)fieldType
                         formName:(NSString*)formName
                          frameID:(NSString*)frameID
                            value:(NSString*)value
                    userInitiated:(BOOL)userInitiated {
  _autofillController = autofillController;

  __weak ShellAutofillDelegate* weakSelf = self;
  id completionHandler = ^(NSArray<CWVAutofillSuggestion*>* suggestions) {
    ShellAutofillDelegate* strongSelf = weakSelf;
    if (!suggestions.count || !strongSelf) {
      return;
    }

    UIAlertController* alertController = [UIAlertController
        alertControllerWithTitle:@"Pick a suggestion"
                         message:nil
                  preferredStyle:UIAlertControllerStyleActionSheet];
    alertController.popoverPresentationController.sourceView =
        [self anyKeyWindow];
    CGRect bounds = [self anyKeyWindow].bounds;
    alertController.popoverPresentationController.sourceRect =
        CGRectMake(CGRectGetWidth(bounds) / 2, 60, 1, 1);
    UIAlertAction* cancelAction =
        [UIAlertAction actionWithTitle:@"Cancel"
                                 style:UIAlertActionStyleCancel
                               handler:nil];
    [alertController addAction:cancelAction];
    for (NSUInteger i = 0; i < suggestions.count; ++i) {
      CWVAutofillSuggestion* suggestion = suggestions[i];
      [alertController addAction:[self actionForSuggestion:suggestion
                                                   atIndex:i]];
    }

    [[self anyKeyWindow].rootViewController
        presentViewController:alertController
                     animated:YES
                   completion:nil];
  };
  [autofillController fetchSuggestionsForFormWithName:formName
                                      fieldIdentifier:fieldIdentifier
                                            fieldType:fieldType
                                              frameID:frameID
                                    completionHandler:completionHandler];
}

- (void)autofillController:(CWVAutofillController*)autofillController
    didInputInFieldWithIdentifier:(NSString*)fieldIdentifier
                        fieldType:(NSString*)fieldType
                         formName:(NSString*)formName
                          frameID:(NSString*)frameID
                            value:(NSString*)value
                    userInitiated:(BOOL)userInitiated {
  // TODO(crbug.com/40224850): Fetching suggestions has an important side effect
  // of calling PasswordFormManager::UpdateStateOnUserInput. This will ensure
  // that the typed information can be remembered during the save dialogue.
  // Make this method a no-op once the bug is fixed.
  id completionHandler = ^(NSArray<CWVAutofillSuggestion*>* suggestions) {
    NSLog(@"%@ suggestions: %@", NSStringFromSelector(_cmd), suggestions);
  };
  [autofillController fetchSuggestionsForFormWithName:formName
                                      fieldIdentifier:fieldIdentifier
                                            fieldType:fieldType
                                              frameID:frameID
                                    completionHandler:completionHandler];
}

- (void)autofillController:(CWVAutofillController*)autofillController
    didBlurOnFieldWithIdentifier:(NSString*)fieldIdentifier
                       fieldType:(NSString*)fieldType
                        formName:(NSString*)formName
                         frameID:(NSString*)frameID
                           value:(NSString*)value
                   userInitiated:(BOOL)userInitiated {
  // Not implemented.
}

- (void)autofillController:(CWVAutofillController*)autofillController
     didSubmitFormWithName:(NSString*)formName
                   frameID:(NSString*)frameID
             userInitiated:(BOOL)userInitiated {
  // Not implemented.
}

- (void)autofillController:(CWVAutofillController*)autofillController
              didFindForms:(NSArray<CWVAutofillForm*>*)forms
                   frameID:(NSString*)frameID {
  if (forms.count == 0) {
    return;
  }

  NSArray<NSString*>* debugDescriptions =
      [forms valueForKey:NSStringFromSelector(@selector(debugDescription))];
  NSLog(@"Found forms in frame %@\n%@", frameID, debugDescriptions);
}

- (void)autofillController:(CWVAutofillController*)autofillController
    saveCreditCardWithSaver:(CWVCreditCardSaver*)saver {
  CWVCreditCard* creditCard = saver.creditCard;
  UIAlertController* alertController =
      [UIAlertController alertControllerWithTitle:@"Save card?"
                                          message:creditCard.debugDescription
                                   preferredStyle:UIAlertControllerStyleAlert];
  __weak UIAlertController* weakAlertController = alertController;
  __weak ShellAutofillDelegate* weakSelf = self;
  UIAlertAction* allowAction = [UIAlertAction
      actionWithTitle:@"Allow"
                style:UIAlertActionStyleDefault
              handler:^(UIAlertAction* _Nonnull action) {
                NSString* cardHolderFullName =
                    weakAlertController.textFields[0].text;
                NSString* expirationMonth =
                    weakAlertController.textFields[1].text;
                NSString* expirationYear =
                    weakAlertController.textFields[2].text;
                [saver acceptWithCardHolderFullName:cardHolderFullName
                                    expirationMonth:expirationMonth
                                     expirationYear:expirationYear
                                           riskData:weakSelf.riskDataLoader
                                                        .riskData
                                  completionHandler:^(BOOL cardSaved) {
                                    if (!cardSaved) {
                                      NSLog(@"Failed to save: %@",
                                            saver.creditCard);
                                    }
                                  }];
              }];
  UIAlertAction* cancelAction =
      [UIAlertAction actionWithTitle:@"Cancel"
                               style:UIAlertActionStyleCancel
                             handler:^(UIAlertAction* _Nonnull action) {
                               [saver decline];
                             }];
  [alertController addAction:allowAction];
  [alertController addAction:cancelAction];

  [alertController
      addTextFieldWithConfigurationHandler:^(UITextField* textField) {
        textField.placeholder = @"Card holder full name";
        textField.keyboardType = UIKeyboardTypeDefault;
      }];
  [alertController
      addTextFieldWithConfigurationHandler:^(UITextField* textField) {
        textField.placeholder = @"Expiration month (MM)";
        textField.keyboardType = UIKeyboardTypeNumberPad;
      }];
  [alertController
      addTextFieldWithConfigurationHandler:^(UITextField* textField) {
        textField.placeholder = @"Expiration year (YYYY)";
        textField.keyboardType = UIKeyboardTypeNumberPad;
      }];

  [[self anyKeyWindow].rootViewController presentViewController:alertController
                                                       animated:YES
                                                     completion:nil];
}

- (void)autofillController:(CWVAutofillController*)autofillController
    decideSavePolicyForPassword:(CWVPassword*)password
                decisionHandler:(void (^)(CWVPasswordUserDecision decision))
                                    decisionHandler {
  UIAlertController* alertController =
      [UIAlertController alertControllerWithTitle:@"Save password?"
                                          message:password.debugDescription
                                   preferredStyle:UIAlertControllerStyleAlert];

  UIAlertAction* noAction = [UIAlertAction
      actionWithTitle:@"Not this time"
                style:UIAlertActionStyleCancel
              handler:^(UIAlertAction* _Nonnull action) {
                decisionHandler(CWVPasswordUserDecisionNotThisTime);
              }];
  [alertController addAction:noAction];

  UIAlertAction* neverAction =
      [UIAlertAction actionWithTitle:@"Never"
                               style:UIAlertActionStyleDefault
                             handler:^(UIAlertAction* _Nonnull action) {
                               decisionHandler(CWVPasswordUserDecisionNever);
                             }];
  [alertController addAction:neverAction];

  UIAlertAction* yesAction =
      [UIAlertAction actionWithTitle:@"Save"
                               style:UIAlertActionStyleDefault
                             handler:^(UIAlertAction* _Nonnull action) {
                               decisionHandler(CWVPasswordUserDecisionYes);
                             }];
  [alertController addAction:yesAction];

  [[self anyKeyWindow].rootViewController presentViewController:alertController
                                                       animated:YES
                                                     completion:nil];
}

- (void)autofillController:(CWVAutofillController*)autofillController
    decideUpdatePolicyForPassword:(CWVPassword*)password
                  decisionHandler:(void (^)(CWVPasswordUserDecision decision))
                                      decisionHandler {
  UIAlertController* alertController =
      [UIAlertController alertControllerWithTitle:@"Update password?"
                                          message:password.debugDescription
                                   preferredStyle:UIAlertControllerStyleAlert];

  UIAlertAction* noAction = [UIAlertAction
      actionWithTitle:@"Not this time"
                style:UIAlertActionStyleCancel
              handler:^(UIAlertAction* _Nonnull action) {
                decisionHandler(CWVPasswordUserDecisionNotThisTime);
              }];
  [alertController addAction:noAction];

  UIAlertAction* yesAction =
      [UIAlertAction actionWithTitle:@"Update"
                               style:UIAlertActionStyleDefault
                             handler:^(UIAlertAction* _Nonnull action) {
                               decisionHandler(CWVPasswordUserDecisionYes);
                             }];
  [alertController addAction:yesAction];

  [[self anyKeyWindow].rootViewController presentViewController:alertController
                                                       animated:YES
                                                     completion:nil];
}

- (void)autofillController:(CWVAutofillController*)autofillController
    verifyCreditCardWithVerifier:(CWVCreditCardVerifier*)verifier {
  [[self anyKeyWindow] endEditing:YES];

  UIAlertController* alertController =
      [UIAlertController alertControllerWithTitle:@"Verify Card"
                                          message:@"Enter CVC"
                                   preferredStyle:UIAlertControllerStyleAlert];

  __weak UIAlertController* weakAlertController = alertController;
  __weak ShellAutofillDelegate* weakSelf = self;
  UIAlertAction* submit = [UIAlertAction
      actionWithTitle:@"Confirm"
                style:UIAlertActionStyleDefault
              handler:^(UIAlertAction* action) {
                UITextField* textField =
                    weakAlertController.textFields.firstObject;
                NSString* CVC = textField.text;
                [verifier verifyWithCVC:CVC
                        expirationMonth:nil
                         expirationYear:nil
                               riskData:weakSelf.riskDataLoader.riskData
                      completionHandler:^(NSError* error) {
                        if (error) {
                          NSLog(@"Card %@ failed to verify error: %@",
                                verifier.creditCard, error);
                        }
                      }];
              }];

  [alertController addAction:submit];

  UIAlertAction* cancel =
      [UIAlertAction actionWithTitle:@"Cancel"
                               style:UIAlertActionStyleCancel
                             handler:nil];
  [alertController addAction:cancel];

  [alertController
      addTextFieldWithConfigurationHandler:^(UITextField* textField) {
        textField.placeholder = @"CVC";
        textField.keyboardType = UIKeyboardTypeNumberPad;
      }];

  [[self anyKeyWindow].rootViewController presentViewController:alertController
                                                       animated:YES
                                                     completion:nil];
}

- (void)autofillController:(CWVAutofillController*)autofillController
    notifyUserOfPasswordLeakOnURL:(NSURL*)URL
                         leakType:(CWVPasswordLeakType)leakType
                         username:(NSString*)username {
  NSLog(@"Password on %@ is leaked for username %@!", URL, username);
}

- (void)autofillController:(CWVAutofillController*)autofillController
    suggestGeneratedPassword:(NSString*)generatedPassword
             decisionHandler:(void (^)(BOOL accept))decisionHandler {
  NSLog(@"Accepting suggested password: %@", generatedPassword);
  decisionHandler(YES);
}

- (void)autofillController:(CWVAutofillController*)autofillController
    confirmSaveForNewAutofillProfile:(CWVAutofillProfile*)newProfile
                          oldProfile:(nullable CWVAutofillProfile*)oldProfile
                     decisionHandler:
                         (void (^)(CWVAutofillProfileUserDecision decision))
                             decisionHandler {
  NSString* message =
      [NSString stringWithFormat:@"new: %@\nold: %@",
                                 newProfile.debugDescription, oldProfile];
  UIAlertController* alertController = [UIAlertController
      alertControllerWithTitle:@"Confirm save for new profile?"
                       message:message
                preferredStyle:UIAlertControllerStyleAlert];

  UIAlertAction* accept = [UIAlertAction
      actionWithTitle:@"Accept"
                style:UIAlertActionStyleDefault
              handler:^(UIAlertAction* action) {
                decisionHandler(CWVAutofillProfileUserDecisionAccepted);
              }];
  [alertController addAction:accept];

  UIAlertAction* decline = [UIAlertAction
      actionWithTitle:@"Decline"
                style:UIAlertActionStyleCancel
              handler:^(UIAlertAction* action) {
                decisionHandler(CWVAutofillProfileUserDecisionDeclined);
              }];
  [alertController addAction:decline];

  [[self anyKeyWindow].rootViewController presentViewController:alertController
                                                       animated:YES
                                                     completion:nil];
}

#pragma mark - Private Methods

- (UIAlertAction*)actionForSuggestion:(CWVAutofillSuggestion*)suggestion
                              atIndex:(NSInteger)index {
  NSString* title =
      [NSString stringWithFormat:@"%@ %@", suggestion.value,
                                 suggestion.displayDescription ?: @""];
  __weak ShellAutofillDelegate* weakSelf = self;
  return [UIAlertAction
      actionWithTitle:title
                style:UIAlertActionStyleDefault
              handler:^(UIAlertAction* action) {
                ShellAutofillDelegate* strongSelf = weakSelf;
                if (!strongSelf) {
                  return;
                }
                [strongSelf.autofillController acceptSuggestion:suggestion
                                                        atIndex:index
                                              completionHandler:nil];
                [[self anyKeyWindow] endEditing:YES];
              }];
}

#pragma mark - Private

- (UIWindow*)anyKeyWindow {
  for (UIWindowScene* windowScene in UIApplication.sharedApplication
           .connectedScenes) {
    NSAssert([windowScene isKindOfClass:[UIWindowScene class]],
             @"UIScene is not a UIWindowScene: %@", windowScene);
    for (UIWindow* window in windowScene.windows) {
      if (window.isKeyWindow) {
        return window;
      }
    }
  }

  return nil;
}

@end