session-ios/Signal/src/ViewControllers/OWS2FASettingsViewController.m

456 lines
16 KiB
Objective-C

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
#import "OWS2FASettingsViewController.h"
#import "OWSTableViewController.h"
#import "Session-Swift.h"
#import "SignalMessaging.h"
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalMessaging/SignalMessaging.h>
#import <SignalMessaging/UIColor+OWS.h>
#import <SignalMessaging/UIFont+OWS.h>
#import <SignalMessaging/UIView+OWS.h>
#import <SessionServiceKit/NSString+SSK.h>
#import <SessionServiceKit/OWS2FAManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface OWS2FASettingsViewController () <UITextFieldDelegate>
@property (nonatomic, weak) UIViewController *root2FAViewController;
@property (nonatomic) UITextField *pinTextfield;
@property (nonatomic) OWSTableViewController *tableViewController;
@end
#pragma mark -
@implementation OWS2FASettingsViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [Theme backgroundColor];
self.title = NSLocalizedString(@"ENABLE_2FA_VIEW_TITLE", @"Title for the 'enable two factor auth PIN' views.");
[self createContents];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(stateDidChange:)
name:NSNotificationName_2FAStateDidChange
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)createContents
{
for (UIView *subview in self.view.subviews) {
[subview removeFromSuperview];
}
switch (self.mode) {
case OWS2FASettingsMode_Status:
[self createStatusContents];
break;
case OWS2FASettingsMode_SelectPIN:
[self createSelectCodeContents];
break;
case OWS2FASettingsMode_ConfirmPIN:
[self createConfirmCodeContents];
break;
}
}
- (void)viewWillAppear:(BOOL)animated
{
switch (self.mode) {
case OWS2FASettingsMode_Status:
break;
case OWS2FASettingsMode_SelectPIN:
case OWS2FASettingsMode_ConfirmPIN:
OWSAssertDebug(![OWS2FAManager.sharedManager is2FAEnabled]);
break;
}
[super viewWillAppear:animated];
// If we're using a table, refresh its contents.
[self updateTableContents];
[self updateNavigationItems];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// If we're using a PIN textfield, select it.
[self.pinTextfield becomeFirstResponder];
}
- (UILabel *)createLabelWithText:(NSString *)text
{
UILabel *label = [UILabel new];
label.textColor = [Theme primaryColor];
label.text = text;
label.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)];
label.numberOfLines = 0;
label.lineBreakMode = NSLineBreakByWordWrapping;
label.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:label];
return label;
}
- (void)createPinTextfield
{
self.pinTextfield = [OWSTextField new];
self.pinTextfield.textColor = [Theme primaryColor];
self.pinTextfield.font = [UIFont ows_mediumFontWithSize:ScaleFromIPhone5To7Plus(30.f, 36.f)];
self.pinTextfield.textAlignment = NSTextAlignmentCenter;
self.pinTextfield.keyboardType = UIKeyboardTypeNumberPad;
self.pinTextfield.delegate = self;
self.pinTextfield.secureTextEntry = YES;
self.pinTextfield.textAlignment = NSTextAlignmentCenter;
[self.pinTextfield addTarget:self
action:@selector(textFieldDidChange:)
forControlEvents:UIControlEventEditingChanged];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, _pinTextfield);
[self.view addSubview:self.pinTextfield];
}
- (void)createTableView
{
self.tableViewController = [OWSTableViewController new];
[self.view addSubview:self.tableViewController.view];
}
- (void)createStatusContents
{
const CGFloat kVSpacing = 30.f;
// TODO: Add hero image?
// TODO: Tweak background color?
NSString *instructions = ([OWS2FAManager.sharedManager is2FAEnabled]
? NSLocalizedString(@"ENABLE_2FA_VIEW_STATUS_ENABLED_INSTRUCTIONS",
@"Indicates that user has 'two factor auth pin' enabled.")
: NSLocalizedString(@"ENABLE_2FA_VIEW_STATUS_DISABLED_INSTRUCTIONS",
@"Indicates that user has 'two factor auth pin' disabled."));
UILabel *instructionsLabel = [self createLabelWithText:instructions];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, instructionsLabel);
[self createTableView];
[instructionsLabel autoPinToTopLayoutGuideOfViewController:self withInset:kVSpacing];
[instructionsLabel autoPinEdgeToSuperviewSafeArea:ALEdgeLeading withInset:self.hMargin];
[instructionsLabel autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing withInset:self.hMargin];
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeLeading];
[self.tableViewController.view autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing];
[self.tableViewController.view autoPinEdge:ALEdgeTop
toEdge:ALEdgeBottom
ofView:instructionsLabel
withOffset:kVSpacing];
[self.tableViewController.view autoPinToBottomLayoutGuideOfViewController:self withInset:0];
[self updateTableContents];
}
- (void)createSelectCodeContents
{
[self createEnterPINContentsWithInstructions:NSLocalizedString(@"ENABLE_2FA_VIEW_SELECT_PIN_INSTRUCTIONS",
@"Indicates that user should select a 'two factor auth pin'.")];
}
- (void)createConfirmCodeContents
{
[self
createEnterPINContentsWithInstructions:NSLocalizedString(@"ENABLE_2FA_VIEW_CONFIRM_PIN_INSTRUCTIONS",
@"Indicates that user should confirm their 'two factor auth pin'.")];
}
- (CGFloat)hMargin
{
return 20.f;
}
- (void)createEnterPINContentsWithInstructions:(NSString *)instructionsText
{
const CGFloat kVSpacing = 30.f;
UILabel *instructionsLabel = [self createLabelWithText:instructionsText];
[self createPinTextfield];
[instructionsLabel autoPinTopToSuperviewMarginWithInset:kVSpacing];
[instructionsLabel autoPinEdgeToSuperviewSafeArea:ALEdgeLeading withInset:self.hMargin];
[instructionsLabel autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing withInset:self.hMargin];
SET_SUBVIEW_ACCESSIBILITY_IDENTIFIER(self, instructionsLabel);
[self.pinTextfield autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:instructionsLabel withOffset:kVSpacing];
[self.pinTextfield autoPinEdgeToSuperviewSafeArea:ALEdgeLeading withInset:self.hMargin];
[self.pinTextfield autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing withInset:self.hMargin];
UIView *underscoreView = [UIView new];
underscoreView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.f];
[self.view addSubview:underscoreView];
[underscoreView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.pinTextfield withOffset:3];
[underscoreView autoPinEdgeToSuperviewSafeArea:ALEdgeLeading withInset:self.hMargin];
[underscoreView autoPinEdgeToSuperviewSafeArea:ALEdgeTrailing withInset:self.hMargin];
[underscoreView autoSetDimension:ALDimensionHeight toSize:1.f];
[self updateNavigationItems];
}
- (void)updateTableContents
{
__weak OWS2FASettingsViewController *weakSelf = self;
// Only some modes use a table.
switch (self.mode) {
case OWS2FASettingsMode_Status: {
OWSTableContents *contents = [OWSTableContents new];
OWSTableSection *section = [OWSTableSection new];
if ([OWS2FAManager.sharedManager is2FAEnabled]) {
[section
addItem:[OWSTableItem disclosureItemWithText:
NSLocalizedString(@"ENABLE_2FA_VIEW_DISABLE_2FA",
@"Label for the 'enable two-factor auth' item in the settings view")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"enable_2fa")
actionBlock:^{
[weakSelf tryToDisable2FA];
}]];
} else {
[section addItem:[OWSTableItem
disclosureItemWithText:
NSLocalizedString(@"ENABLE_2FA_VIEW_ENABLE_2FA",
@"Label for the 'enable two-factor auth' item in the settings view")
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"disable_2fa")
actionBlock:^{
[weakSelf showEnable2FAWorkUI];
}]];
}
[contents addSection:section];
self.tableViewController.contents = contents;
break;
}
case OWS2FASettingsMode_SelectPIN:
case OWS2FASettingsMode_ConfirmPIN:
return;
}
}
- (BOOL)shouldHaveNextButton
{
switch (self.mode) {
case OWS2FASettingsMode_Status:
return NO;
case OWS2FASettingsMode_SelectPIN:
case OWS2FASettingsMode_ConfirmPIN:
return [self hasValidPin];
}
}
- (void)updateNavigationItems
{
// Note: This affects how the "back" button will look if another
// view is pushed on top of this one, not how the "back"
// button looks when this view is visible.
UIBarButtonItem *backButton =
[[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"BACK_BUTTON", @"button text for back button")
style:UIBarButtonItemStylePlain
target:self
action:@selector(backButtonWasPressed)
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"back")];
self.navigationItem.backBarButtonItem = backButton;
if (self.shouldHaveNextButton) {
UIBarButtonItem *nextButton = [[UIBarButtonItem alloc]
initWithTitle:NSLocalizedString(@"ENABLE_2FA_VIEW_NEXT_BUTTON",
@"Label for the 'next' button in the 'enable two factor auth' views.")
style:UIBarButtonItemStylePlain
target:self
action:@selector(nextButtonWasPressed)
accessibilityIdentifier:ACCESSIBILITY_IDENTIFIER_WITH_NAME(self, @"next")];
self.navigationItem.rightBarButtonItem = nextButton;
} else {
self.navigationItem.rightBarButtonItem = nil;
}
}
#pragma mark - UITextFieldDelegate
- (BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)insertionText
{
[ViewControllerUtils ows2FAPINTextField:textField
shouldChangeCharactersInRange:range
replacementString:insertionText];
[self updateNavigationItems];
return NO;
}
- (void)textFieldDidChange:(id)sender
{
[self updateNavigationItems];
}
#pragma mark - Events
- (void)nextButtonWasPressed
{
switch (self.mode) {
case OWS2FASettingsMode_Status:
OWSFailDebug(@"status mode should not have a next button.");
return;
case OWS2FASettingsMode_SelectPIN: {
OWSAssertDebug(self.hasValidPin);
OWS2FASettingsViewController *vc = [OWS2FASettingsViewController new];
vc.mode = OWS2FASettingsMode_ConfirmPIN;
vc.candidatePin = self.pinTextfield.text;
OWSAssertDebug(self.root2FAViewController);
vc.root2FAViewController = self.root2FAViewController;
[self.navigationController pushViewController:vc animated:YES];
break;
}
case OWS2FASettingsMode_ConfirmPIN: {
OWSAssertDebug(self.hasValidPin);
if ([self.pinTextfield.text isEqualToString:self.candidatePin]) {
[self tryToEnable2FA];
} else {
// Clear the PIN so that the user can try again.
self.pinTextfield.text = nil;
[OWSAlerts showErrorAlertWithMessage:
NSLocalizedString(@"ENABLE_2FA_VIEW_PIN_DOES_NOT_MATCH",
@"Error indicating that the entered 'two-factor auth PINs' do not match.")];
}
break;
}
}
}
- (BOOL)hasValidPin
{
return self.pinTextfield.text.length >= kMin2FAPinLength;
}
- (void)showEnable2FAWorkUI
{
OWSAssertDebug(![OWS2FAManager.sharedManager is2FAEnabled]);
OWSLogInfo(@"");
OWS2FASettingsViewController *vc = [OWS2FASettingsViewController new];
vc.mode = OWS2FASettingsMode_SelectPIN;
vc.root2FAViewController = self;
[self.navigationController pushViewController:vc animated:YES];
}
- (void)tryToDisable2FA
{
OWSLogInfo(@"");
__weak OWS2FASettingsViewController *weakSelf = self;
[ModalActivityIndicatorViewController
presentFromViewController:self
canCancel:NO
backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) {
[OWS2FAManager.sharedManager disable2FAWithSuccess:^{
[modalActivityIndicator dismissWithCompletion:^{
// TODO: Should we show an alert?
[weakSelf updateTableContents];
}];
}
failure:^(NSError *error) {
[modalActivityIndicator dismissWithCompletion:^{
[weakSelf updateTableContents];
[OWSAlerts showErrorAlertWithMessage:
NSLocalizedString(@"ENABLE_2FA_VIEW_COULD_NOT_DISABLE_2FA",
@"Error indicating that attempt to disable 'two-factor "
@"auth' failed.")];
}];
}];
}];
}
- (void)tryToEnable2FA
{
OWSAssertDebug(self.candidatePin.length > 0);
OWSLogInfo(@"");
__weak OWS2FASettingsViewController *weakSelf = self;
[ModalActivityIndicatorViewController
presentFromViewController:self
canCancel:NO
backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) {
[OWS2FAManager.sharedManager requestEnable2FAWithPin:self.candidatePin
success:^{
[modalActivityIndicator dismissWithCompletion:^{
[weakSelf showCompleteUI];
}];
}
failure:^(NSError *error) {
[modalActivityIndicator dismissWithCompletion:^{
// The client may have fallen out of sync with the service.
// Try to get back to a known good state by disabling 2FA
// whenever enabling it fails.
[OWS2FAManager.sharedManager disable2FAWithSuccess:nil failure:nil];
[weakSelf updateTableContents];
[OWSAlerts showErrorAlertWithMessage:
NSLocalizedString(@"ENABLE_2FA_VIEW_COULD_NOT_ENABLE_2FA",
@"Error indicating that attempt to enable 'two-factor "
@"auth' failed.")];
}];
}];
}];
}
- (void)showCompleteUI
{
OWSAssertDebug([OWS2FAManager.sharedManager is2FAEnabled]);
OWSAssertDebug(self.root2FAViewController);
OWSLogInfo(@"");
[self.navigationController popToViewController:self.root2FAViewController animated:YES];
}
- (void)backButtonWasPressed
{
[self.navigationController popViewControllerAnimated:YES];
}
- (void)stateDidChange:(NSNotification *)notification
{
OWSLogInfo(@"");
if (self.mode == OWS2FASettingsMode_Status) {
[self createContents];
}
}
@end
NS_ASSUME_NONNULL_END