// Copyright 2023 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/ui/settings/password/reauthentication/reauthentication_view_controller.h"
#import "base/check.h"
#import "base/metrics/histogram_macros.h"
#import "ios/chrome/browser/shared/ui/elements/branded_navigation_item_title_view.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/settings/password/create_password_manager_title_view.h"
#import "ios/chrome/browser/ui/settings/password/reauthentication/reauthentication_constants.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_event.h"
#import "ios/chrome/common/ui/reauthentication/reauthentication_protocol.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
@implementation ReauthenticationViewController {
id<ReauthenticationProtocol> _reauthModule;
BOOL _reauthUponPresentation;
}
- (instancetype)initWithReauthenticationModule:
(id<ReauthenticationProtocol>)reauthenticationModule
reauthUponPresentation:(BOOL)reauthUponPresentation {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_reauthModule = reauthenticationModule;
_reauthUponPresentation = reauthUponPresentation;
self.navigationItem.hidesBackButton = YES;
// This view does not support large titles as it uses a custom title view.
self.navigationItem.largeTitleDisplayMode =
UINavigationItemLargeTitleDisplayModeNever;
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.accessibilityIdentifier =
password_manager::kReauthenticationViewControllerAccessibilityIdentifier;
// Set background color matching the one used in the settings UI.
self.view.backgroundColor =
[UIColor colorNamed:kGroupedPrimaryBackgroundColor];
[self setUpTitle];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// Restore navigation bar background color to its default value.
// The view controller under self in the stack could have changed it.
self.navigationController.navigationBar.backgroundColor = nil;
if (_reauthUponPresentation) {
[self recordAuthenticationEvent:ReauthenticationEvent::kAttempt];
if (@available(iOS 18, *)) {
// TODO(crbug.com/347330366): Mock reauth will make
// -triggerLocalAuthentication return immediately, which means a VC is
// pushed and popped, following by pushing another VC. When doing this on
// iOS18, the top accessory views are not visible. This is either unsafe
// in UIKit and needs to be changed, or is a bug in iOS18, and may still
// require a workaround. During this investigation, simply defer calling
// -triggerLocalAuthentication to the next runloop on iOS18. This may be
// unsafe and is a short-term only solution to greening a large number of
// tests in one place.
dispatch_async(dispatch_get_main_queue(), ^{
[self triggerLocalAuthentication];
});
} else {
[self triggerLocalAuthentication];
}
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Wait until the view is in the hierarchy to present the alert, otherwise it
// won't be shown.
if (_reauthUponPresentation) {
_reauthUponPresentation = NO;
[self showSetUpPasscodeDialogIfNeeded];
}
}
#pragma mark - ReauthenticationViewController
- (void)requestAuthentication {
[self recordAuthenticationEvent:ReauthenticationEvent::kAttempt];
if ([_reauthModule canAttemptReauth]) {
[self triggerLocalAuthentication];
} else {
[self showSetUpPasscodeDialogIfNeeded];
}
}
#pragma mark - Private
// Forwards reauthentication result to the delegate.
- (void)handleReauthenticationResult:(ReauthenticationResult)result {
// Reauth can't be skipped for this surface.
CHECK(result != ReauthenticationResult::kSkipped);
BOOL success = result == ReauthenticationResult::kSuccess;
[self recordAuthenticationEvent:success ? ReauthenticationEvent::kSuccess
: ReauthenticationEvent::kFailure];
[self.delegate reauthenticationDidFinishWithSuccess:success];
}
// Sets a custom title view with the Password Manager logo.
- (void)setUpTitle {
self.title = l10n_util::GetNSString(IDS_IOS_PASSWORD_MANAGER);
self.navigationItem.titleView =
password_manager::CreatePasswordManagerTitleView(/*title=*/self.title);
}
// Requests the delegate to show passcode request alert if no Local
// Authentication is available.
- (void)showSetUpPasscodeDialogIfNeeded {
if ([_reauthModule canAttemptReauth]) {
return;
}
[self recordAuthenticationEvent:ReauthenticationEvent::kMissingPasscode];
[self.delegate showSetUpPasscodeDialog];
}
// Records reauthentication event metrics.
- (void)recordAuthenticationEvent:(ReauthenticationEvent)event {
UMA_HISTOGRAM_ENUMERATION(password_manager::kReauthenticationUIEventHistogram,
event);
}
// Starts the native UI for Local Authentication.
- (void)triggerLocalAuthentication {
if (![_reauthModule canAttemptReauth]) {
return;
}
// Hide keyboard otherwise the first responder can get focused after getting
// the authentication result.
[GetFirstResponder() resignFirstResponder];
__weak __typeof(self) weakSelf = self;
[_reauthModule
attemptReauthWithLocalizedReason:
l10n_util::GetNSString(IDS_IOS_SETTINGS_PASSWORD_REAUTH_REASON_SHOW)
canReusePreviousAuth:NO
handler:^(ReauthenticationResult result) {
[weakSelf handleReauthenticationResult:result];
}];
}
@end