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

#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#import "base/apple/foundation_util.h"
#import "ios/chrome/common/app_group/app_group_metrics.h"
#import "ios/chrome/common/constants.h"
#import "ios/chrome/common/credential_provider/credential.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/highlight_button.h"
#import "ios/chrome/credential_provider_extension/metrics_util.h"
#import "ios/chrome/credential_provider_extension/ui/feature_flags.h"
#import "ios/chrome/credential_provider_extension/ui/password_note_cell.h"
#import "ios/chrome/credential_provider_extension/ui/tooltip_view.h"
#import "ios/chrome/credential_provider_extension/ui/ui_util.h"

namespace {

// Desired space between the bottom of the nav bar and the top of the table
// view.
const CGFloat kTableViewTopSpace = 14;

NSString* kCellIdentifier = @"cdvcCell";

NSString* const kMaskedPassword = @"••••••••";

typedef NS_ENUM(NSInteger, RowIdentifier) {
  RowIdentifierURL,
  RowIdentifierUsername,
  RowIdentifierPassword,
  RowIdentifierNote,
  NumRows,
  RowIdentifierUserDisplayName,
  RowIdentifierCreationDate,
};

}  // namespace

@interface CredentialDetailsViewController () <TooltipViewDelegate,
                                               UITableViewDataSource>

// Current credential.
@property(nonatomic, weak) id<Credential> credential;

// Current clear password or nil (while locked).
@property(nonatomic, strong) NSString* clearPassword;

@end

@implementation CredentialDetailsViewController

@synthesize delegate;

- (instancetype)init {
  self = [super initWithStyle:UITableViewStyleInsetGrouped];
  return self;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  UIColor* backgroundColor =
      [UIColor colorNamed:kGroupedPrimaryBackgroundColor];
  self.view.backgroundColor = backgroundColor;
  UINavigationBarAppearance* appearance =
      [[UINavigationBarAppearance alloc] init];
  [appearance configureWithDefaultBackground];
  appearance.backgroundColor = backgroundColor;
  self.navigationItem.scrollEdgeAppearance = appearance;
  self.navigationItem.rightBarButtonItem = [self navigationEnterButton];
  // UITableViewStyleInsetGrouped adds space to the top of the table view by
  // default. Remove that space and add in the desired amount.
  self.tableView.contentInset = UIEdgeInsetsMake(
      -kUITableViewInsetGroupedTopSpace + kTableViewTopSpace, 0, 0, 0);
  self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];

  NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
  [defaultCenter addObserver:self
                    selector:@selector(hidePassword)
                        name:UIApplicationDidEnterBackgroundNotification
                      object:nil];
}

#pragma mark - CredentialDetailsConsumer

- (void)presentCredential:(id<Credential>)credential {
  self.credential = credential;
  self.clearPassword = nil;
  self.title = credential.serviceName;
  [self.tableView reloadData];
}

#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView*)tableView
    numberOfRowsInSection:(NSInteger)section {
  return RowIdentifier::NumRows;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  RowIdentifier rowIdentifier = [self rowIdentifier:indexPath.row];
  if (rowIdentifier == RowIdentifier::RowIdentifierNote) {
    PasswordNoteCell* cell =
        [tableView dequeueReusableCellWithIdentifier:PasswordNoteCell.reuseID];
    if (!cell) {
      cell = [[PasswordNoteCell alloc] initWithStyle:UITableViewCellStyleValue1
                                     reuseIdentifier:PasswordNoteCell.reuseID];
    }
    [cell configureCell];
    cell.textView.text = self.credential.note;
    cell.textView.editable = NO;
    return cell;
  }

  UITableViewCell* cell =
      [tableView dequeueReusableCellWithIdentifier:kCellIdentifier];
  if (!cell) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
                                  reuseIdentifier:kCellIdentifier];
  }

  cell.textLabel.textColor = [UIColor colorNamed:kTextPrimaryColor];
  cell.detailTextLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
  cell.accessibilityTraits |= UIAccessibilityTraitButton;

  switch (rowIdentifier) {
    case RowIdentifier::RowIdentifierURL:
      cell.accessoryView = nil;
      cell.textLabel.text =
          NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_URL", @"URL");
      cell.detailTextLabel.text = self.credential.serviceIdentifier;
      break;
    case RowIdentifier::RowIdentifierUsername:
      cell.accessoryView = nil;
      cell.textLabel.text = NSLocalizedString(
          @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_USERNAME", @"Username");
      cell.detailTextLabel.text = self.credential.username;
      break;
    case RowIdentifier::RowIdentifierPassword:
      cell.accessoryView = [self passwordIconButton];
      cell.textLabel.text = NSLocalizedString(
          @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_PASSWORD", @"Password");
      cell.detailTextLabel.text = [self password];
      break;
    case RowIdentifier::RowIdentifierNote:
      break;
    case RowIdentifier::RowIdentifierUserDisplayName:
      cell.accessoryView = nil;
      cell.textLabel.text = NSLocalizedString(
          @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_USER_DISPLAY_NAME",
          @"User Display Name");
      cell.detailTextLabel.text = self.credential.userDisplayName;
      break;
    case RowIdentifier::RowIdentifierCreationDate:
      cell.accessoryView = nil;
      cell.textLabel.text = NSLocalizedString(
          @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_SHOW_CREATION_DATE",
          @"Passkey");
      cell.detailTextLabel.text = [self creationDate];
      break;
    default:
      break;
  }

  return cell;
}

#pragma mark - UITableViewDelegate

- (void)tableView:(UITableView*)tableView
    didSelectRowAtIndexPath:(NSIndexPath*)indexPath {
  // Callout menu don't show up in the extension in iOS13, even
  // though you can find it in the View Hierarchy. Using custom one.
  UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];

  switch ([self rowIdentifier:indexPath.row]) {
    case RowIdentifier::RowIdentifierURL:
      [self showTootip:NSLocalizedString(
                           @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_COPY", @"Copy")
            atBottomOf:cell
                action:@selector(copyURL)];
      break;
    case RowIdentifier::RowIdentifierUsername:
      [self showTootip:NSLocalizedString(
                           @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_COPY", @"Copy")
            atBottomOf:cell
                action:@selector(copyUsername)];
      break;
    case RowIdentifier::RowIdentifierPassword:
      if (self.clearPassword) {
        [self
            showTootip:NSLocalizedString(
                           @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_COPY", @"Copy")
            atBottomOf:cell
                action:@selector(copyPassword)];
      } else {
        [self
            showTootip:NSLocalizedString(
                           @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_SHOW_PASSWORD",
                           @"Show Password")
            atBottomOf:cell
                action:@selector(showPassword)];
      }
      break;
    case RowIdentifier::RowIdentifierUserDisplayName:
      [self showTootip:NSLocalizedString(
                           @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_COPY", @"Copy")
            atBottomOf:cell
                action:@selector(copyUserDisplayName)];
      break;
    case RowIdentifier::RowIdentifierCreationDate:
      [self showTootip:NSLocalizedString(
                           @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_COPY", @"Copy")
            atBottomOf:cell
                action:@selector(copyCreationDate)];
      break;
    default:
      break;
  }
}

#pragma mark - TooltipViewDelegate

- (void)tooltipViewWillDismiss:(TooltipView*)tooltipView {
  NSIndexPath* selectedIndexPath = self.tableView.indexPathForSelectedRow;
  [self.tableView deselectRowAtIndexPath:selectedIndexPath animated:YES];
}

#pragma mark - Private

// Returns the identifier for the provided row.
// Returns "RowIdentifier::NumRows" if the provided row is invalid.
- (RowIdentifier)rowIdentifier:(NSInteger)row {
  switch (row) {
    case 0:
      return RowIdentifier::RowIdentifierURL;
    case 1:
      return RowIdentifier::RowIdentifierUsername;
    case 2:
      return self.credential.isPasskey
                 ? RowIdentifier::RowIdentifierUserDisplayName
                 : RowIdentifier::RowIdentifierPassword;
    case 3:
      return self.credential.isPasskey
                 ? RowIdentifier::RowIdentifierCreationDate
                 : RowIdentifier::RowIdentifierNote;
    default:
      return RowIdentifier::NumRows;
  }
}

// Copy credential URL to clipboard.
- (void)copyURL {
  UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
  generalPasteboard.string = self.credential.serviceIdentifier;
  UpdateUMACountForKey(app_group::kCredentialExtensionCopyURLCount);
}

// Copy credential Username to clipboard.
- (void)copyUsername {
  UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
  generalPasteboard.string = self.credential.username;
  UpdateUMACountForKey(app_group::kCredentialExtensionCopyUsernameCount);
}

// Copy credential User Display Name to clipboard.
- (void)copyUserDisplayName {
  UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
  generalPasteboard.string = self.credential.userDisplayName;
  UpdateUMACountForKey(app_group::kCredentialExtensionCopyUserDisplayNameCount);
}

// Copy creation date to clipboard.
- (void)copyCreationDate {
  UIPasteboard* generalPasteboard = [UIPasteboard generalPasteboard];
  generalPasteboard.string = [self creationDate];
  UpdateUMACountForKey(app_group::kCredentialExtensionCopyCreationDateCount);
}

// Copy password to clipboard.
- (void)copyPassword {
  NSDictionary* item = @{UTTypePlainText.identifier : self.clearPassword};
  NSDate* expirationDate =
      [NSDate dateWithTimeIntervalSinceNow:kSecurePasteboardExpiration];
  NSDictionary* options = @{UIPasteboardOptionExpirationDate : expirationDate};
  [[UIPasteboard generalPasteboard] setItems:@[ item ] options:options];
  UpdateUMACountForKey(app_group::kCredentialExtensionCopyPasswordCount);
}

// Initiate process to show password unobfuscated.
- (void)showPassword {
  [self passwordIconButtonTapped:nil event:nil];
}

// Alert the delegate that the user wants to enter this password.
- (void)enterPassword {
  [self.delegate userSelectedCredential:self.credential];
}

// Creates a cancel button for the navigation item.
- (UIBarButtonItem*)navigationCancelButton {
  UIBarButtonItem* cancelButton = [[UIBarButtonItem alloc]
      initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
                           target:self.delegate
                           action:@selector(navigationCancelButtonWasPressed:)];
  cancelButton.tintColor = [UIColor colorNamed:kBlueColor];
  return cancelButton;
}

// Creates an enter button for the navigation item
- (UIBarButtonItem*)navigationEnterButton {
  NSString* title =
      NSLocalizedString(@"IDS_IOS_CREDENTIAL_PROVIDER_USE", @"Use");
  UIBarButtonItem* enterButton =
      [[UIBarButtonItem alloc] initWithTitle:title
                                       style:UIBarButtonItemStyleDone
                                      target:self
                                      action:@selector(enterPassword)];
  enterButton.tintColor = [UIColor colorNamed:kBlueColor];
  return enterButton;
}

- (NSString*)creationDate {
  NSString* baseLocalizedString = NSLocalizedString(
      @"IDS_IOS_CREDENTIAL_PROVIDER_DETAILS_CREATION_DATE", @"00/00/00");
  // TODO(crbug.com/330355124): Format the date properly.
  NSString* date =
      [NSString stringWithFormat:@"%lld", self.credential.creationTime];
  return [baseLocalizedString stringByReplacingOccurrencesOfString:@"$1"
                                                        withString:date];
}

// Returns the string to display as password.
- (NSString*)password {
  return self.clearPassword ? self.clearPassword : kMaskedPassword;
}

// Creates a button to be displayed as accessory of the password row item.
- (UIView*)passwordIconButton {
  UIImage* image =
      [UIImage imageNamed:self.clearPassword ? @"password_hide_icon"
                                             : @"password_reveal_icon"];
  image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];

  HighlightButton* button = [HighlightButton buttonWithType:UIButtonTypeCustom];
  button.frame = CGRectMake(0.0, 0.0, image.size.width, image.size.height);
  [button setBackgroundImage:image forState:UIControlStateNormal];
  [button setTintColor:[UIColor colorNamed:kBlueColor]];
  [button addTarget:self
                action:@selector(passwordIconButtonTapped:event:)
      forControlEvents:UIControlEventTouchUpInside];

  button.pointerInteractionEnabled = YES;
  button.pointerStyleProvider = ^UIPointerStyle*(
      UIButton* theButton, __unused UIPointerEffect* proposedEffect,
      __unused UIPointerShape* proposedShape) {
    UITargetedPreview* preview =
        [[UITargetedPreview alloc] initWithView:theButton];
    UIPointerHighlightEffect* effect =
        [UIPointerHighlightEffect effectWithPreview:preview];
    UIPointerShape* shape =
        [UIPointerShape shapeWithRoundedRect:theButton.frame
                                cornerRadius:theButton.frame.size.width / 2];
    return [UIPointerStyle styleWithEffect:effect shape:shape];
  };

  return button;
}

// Called when show/hine password icon is tapped.
- (void)passwordIconButtonTapped:(id)sender event:(id)event {
  // Only password reveal / hide is an accessory, so no need to check
  // indexPath.
  if (self.clearPassword) {
    self.clearPassword = nil;
    [self updatePasswordRow];
  } else {
    UpdateUMACountForKey(app_group::kCredentialExtensionShowPasswordCount);
    [self.delegate unlockPasswordForCredential:self.credential
                             completionHandler:^(NSString* password) {
                               self.clearPassword = password;
                               [self updatePasswordRow];
                             }];
  }
}

// Hides the password and toggles the "Show/Hide" button.
- (void)hidePassword {
  if (!self.clearPassword)
    return;
  self.clearPassword = nil;
  [self updatePasswordRow];
}

// Updates the password row.
- (void)updatePasswordRow {
  NSIndexPath* indexPath =
      [NSIndexPath indexPathForRow:RowIdentifier::RowIdentifierPassword
                         inSection:0];
  [self.tableView reloadRowsAtIndexPaths:@[ indexPath ]
                        withRowAnimation:UITableViewRowAnimationAutomatic];
}

- (void)showTootip:(NSString*)message
        atBottomOf:(UITableViewCell*)cell
            action:(SEL)action {
  TooltipView* tooltip = [[TooltipView alloc] initWithKeyWindow:self.view
                                                         target:self
                                                         action:action];
  tooltip.delegate = self;
  [tooltip showMessage:message atBottomOf:cell];
  UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
                                  tooltip);
}

@end