diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 89f2f78b4..9246b3a5a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -43,7 +43,7 @@ 344F248420069E9C00CFB4F4 /* CountryCodeViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248220069E9B00CFB4F4 /* CountryCodeViewController.h */; }; 344F248520069E9C00CFB4F4 /* CountryCodeViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248320069E9B00CFB4F4 /* CountryCodeViewController.m */; }; 344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */; }; - 344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; }; + 344F248A20069F0600CFB4F4 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; 344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */; }; 344F248D2007CCD600CFB4F4 /* DisplayableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */; }; 344F248F2007D7F200CFB4F4 /* OWSMessagesBubbleImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248E2007D7F200CFB4F4 /* OWSMessagesBubbleImageFactory.swift */; }; @@ -345,6 +345,8 @@ 45CB2FA81CB7146C00E1B343 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */; }; 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */; }; 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D231761DC7E8F10034FA89 /* SessionResetJob.swift */; }; + 45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */; }; + 45D308AD2049A439000189E4 /* PinEntryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 45D308AC2049A439000189E4 /* PinEntryView.m */; }; 45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */; }; 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; }; 45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */; }; @@ -926,6 +928,9 @@ 45CB2FA71CB7146C00E1B343 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = "Launch Screen.storyboard"; path = "Signal/src/util/Launch Screen.storyboard"; sourceTree = SOURCE_ROOT; }; 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncPushTokensJob.swift; sourceTree = ""; }; 45D231761DC7E8F10034FA89 /* SessionResetJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionResetJob.swift; sourceTree = ""; }; + 45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWS2FAReminderViewController.swift; sourceTree = ""; }; + 45D308AB2049A439000189E4 /* PinEntryView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PinEntryView.h; sourceTree = ""; }; + 45D308AC2049A439000189E4 /* PinEntryView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PinEntryView.m; sourceTree = ""; }; 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompareSafetyNumbersActivity.swift; sourceTree = ""; }; 45E282DE1D08E67800ADD4C8 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = translations/gl.lproj/Localizable.strings; sourceTree = ""; }; 45E282DF1D08E6CC00ADD4C8 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = translations/id.lproj/Localizable.strings; sourceTree = ""; }; @@ -1514,6 +1519,7 @@ 340CB2251EAC25820001CAA1 /* UpdateGroupViewController.h */, 340CB2261EAC25820001CAA1 /* UpdateGroupViewController.m */, 34D1F0BE1F8EC1760066283D /* Utils */, + 45D2AC01204885170033C692 /* OWS2FAReminderViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1919,6 +1925,8 @@ 450D19111F85236600970622 /* RemoteVideoView.h */, 450D19121F85236600970622 /* RemoteVideoView.m */, 4523149F1F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift */, + 45D308AB2049A439000189E4 /* PinEntryView.h */, + 45D308AC2049A439000189E4 /* PinEntryView.m */, ); name = Views; path = views; @@ -3086,6 +3094,7 @@ 452314A01F7E9E18003A428C /* DirectionalPanGestureRecognizer.swift in Sources */, 34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */, 34CA1C271F7156F300E51C51 /* MessageDetailViewController.swift in Sources */, + 45D2AC02204885170033C692 /* OWS2FAReminderViewController.swift in Sources */, 34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */, 34D1F0B71F87F8850066283D /* OWSGenericAttachmentView.m in Sources */, 34B3F8801E8DF1700035BE1A /* InviteFlow.swift in Sources */, @@ -3101,6 +3110,7 @@ 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */, 34D1F0B41F86D31D0066283D /* ConversationCollectionView.m in Sources */, 34B3F8821E8DF1700035BE1A /* NewContactThreadViewController.m in Sources */, + 45D308AD2049A439000189E4 /* PinEntryView.m in Sources */, 45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */, 45AE48511E0732D6004D96C2 /* TurnServerInfo.swift in Sources */, 34B3F8771E8DF1700035BE1A /* ContactsPicker.swift in Sources */, diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 254dc626d..57707a5fc 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -9,6 +9,7 @@ #import "DebugLogger.h" #import "MainAppContext.h" #import "NotificationsManager.h" +#import "OWS2FASettingsViewController.h" #import "OWSBackup.h" #import "OWSNavigationController.h" #import "Pastelog.h" @@ -31,6 +32,7 @@ #import #import #import +#import #import #import #import @@ -660,6 +662,19 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; [OWSSyncPushTokensJob runWithAccountManager:SignalApp.sharedApp.accountManager preferences:[Environment preferences]]; } + + if ([OWS2FAManager sharedManager].isDueForReminder) { + if (!self.hasInitialRootViewController || self.window.rootViewController == nil) { + DDLogDebug( + @"%@ Skipping 2FA reminder since there isn't yet an initial view controller", self.logTag); + } else { + UIViewController *rootViewController = self.window.rootViewController; + UINavigationController *reminderNavController = + [OWS2FAReminderViewController wrappedInNavController]; + + [rootViewController presentViewController:reminderNavController animated:YES completion:nil]; + } + } }); } diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 2541750cb..333a03288 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -14,7 +14,6 @@ #import "FingerprintViewController.h" #import "HomeViewController.h" #import "MediaDetailViewController.h" -#import "NSString+OWS.h" #import "NotificationSettingsViewController.h" #import "NotificationsManager.h" #import "OWSAnyTouchGestureRecognizer.h" @@ -25,6 +24,7 @@ #import "OWSNavigationController.h" #import "OWSProgressView.h" #import "OWSWebRTCDataProtos.pb.h" +#import "PinEntryView.h" #import "PrivacySettingsTableViewController.h" #import "ProfileViewController.h" #import "PushManager.h" diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift b/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift index 0b4fe6803..5a12c2e38 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUIFileBrowser.swift @@ -103,7 +103,7 @@ class DebugUIFileBrowser: OWSTableViewController { return attributes.map { (fileAttribute: FileAttributeKey, value: Any) in let title = fileAttribute.rawValue.replacingOccurrences(of: "NSFile", with: "") return OWSTableItem(title: "\(title): \(value)") { - OWSAlerts.showAlert(withTitle: title, message: "\(value)") + OWSAlerts.showAlert(title: title, message: "\(value)") } } } catch { @@ -132,7 +132,7 @@ class DebugUIFileBrowser: OWSTableViewController { } guard let inputString = textField.text, inputString.count >= 4 else { - OWSAlerts.showAlert(withTitle: "new file name missing or less than 4 chars") + OWSAlerts.showAlert(title: "new file name missing or less than 4 chars") return } @@ -177,7 +177,7 @@ class DebugUIFileBrowser: OWSTableViewController { } guard let inputString = textField.text, inputString.count >= 4 else { - OWSAlerts.showAlert(withTitle: "new file dir missing or less than 4 chars") + OWSAlerts.showAlert(title: "new file dir missing or less than 4 chars") return } @@ -206,7 +206,7 @@ class DebugUIFileBrowser: OWSTableViewController { return } - OWSAlerts.showConfirmationAlert(withTitle: "Delete \(strongSelf.fileURL.path)?") { _ in + OWSAlerts.showConfirmationAlert(title: "Delete \(strongSelf.fileURL.path)?") { _ in Logger.debug("\(strongSelf.logTag) deleting file at \(strongSelf.fileURL.path)") do { try strongSelf.fileManager.removeItem(atPath: strongSelf.fileURL.path) @@ -295,7 +295,7 @@ class DebugUIFileBrowser: OWSTableViewController { } guard let inputString = textField.text, inputString.count >= 4 else { - OWSAlerts.showAlert(withTitle: "file name missing or less than 4 chars") + OWSAlerts.showAlert(title: "file name missing or less than 4 chars") return } @@ -333,7 +333,7 @@ class DebugUIFileBrowser: OWSTableViewController { } guard let inputString = textField.text, inputString.count >= 4 else { - OWSAlerts.showAlert(withTitle: "dir name missing or less than 4 chars") + OWSAlerts.showAlert(title: "dir name missing or less than 4 chars") return } diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m index 40eb0cce6..fc0f5d120 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIMisc.m @@ -106,6 +106,16 @@ NS_ASSUME_NONNULL_BEGIN animated:YES]; }]]; + [items addObject:[OWSTableItem itemWithTitle:@"Show 2FA Reminder" + actionBlock:^() { + UINavigationController *navController = + [OWS2FAReminderViewController wrappedInNavController]; + [[[UIApplication sharedApplication] frontmostViewController] + presentViewController:navController + animated:YES + completion:nil]; + }]]; + #ifdef DEBUG [items addObject:[OWSTableItem subPageItemWithText:@"Share UIImage" actionBlock:^(UIViewController *viewController) { diff --git a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift index 7cc6d18ce..3f9f13953 100644 --- a/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift +++ b/Signal/src/ViewControllers/GifPicker/GifPickerViewController.swift @@ -454,7 +454,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect progressiveSearchTimer = nil guard let text = searchBar.text else { - OWSAlerts.showAlert(withTitle: NSLocalizedString("ALERT_ERROR_TITLE", + OWSAlerts.showAlert(title: NSLocalizedString("ALERT_ERROR_TITLE", comment: ""), message: NSLocalizedString("GIF_PICKER_VIEW_MISSING_QUERY", comment: "Alert message shown when user tries to search for GIFs without entering any search terms.")) diff --git a/Signal/src/ViewControllers/OWS2FARegistrationViewController.m b/Signal/src/ViewControllers/OWS2FARegistrationViewController.m index 50ec4ce91..7466741d3 100644 --- a/Signal/src/ViewControllers/OWS2FARegistrationViewController.m +++ b/Signal/src/ViewControllers/OWS2FARegistrationViewController.m @@ -3,24 +3,20 @@ // #import "OWS2FARegistrationViewController.h" +#import "PinEntryView.h" #import "ProfileViewController.h" #import "Signal-Swift.h" #import -#import -#import -#import +#import #import #import NS_ASSUME_NONNULL_BEGIN -@interface OWS2FARegistrationViewController () +@interface OWS2FARegistrationViewController () @property (nonatomic, readonly) AccountManager *accountManager; - -@property (nonatomic) UITextField *pinTextfield; - -@property (nonatomic) OWSFlatButton *submitButton; +@property (nonatomic) PinEntryView *entryView; @end @@ -60,171 +56,54 @@ NS_ASSUME_NONNULL_BEGIN self.view.backgroundColor = UIColor.whiteColor; - [self createContents]; + PinEntryView *entryView = [PinEntryView new]; + self.entryView = entryView; + entryView.delegate = self; + [self.view addSubview:entryView]; + + entryView.instructionsText = NSLocalizedString( + @"REGISTER_2FA_INSTRUCTIONS", @"Instructions to enter the 'two-factor auth pin' in the 2FA registration view."); + + // Layout + + [entryView autoPinEdgesToSuperviewMargins]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; - - [self updateEnabling]; } - (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 = [UIColor blackColor]; - 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 = [UITextField new]; - self.pinTextfield.textColor = [UIColor blackColor]; - self.pinTextfield.placeholder - = NSLocalizedString(@"2FA_PIN_DEFAULT_TEXT", @"Text field placeholder when entering a 'two-factor auth pin'."); - 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.view addSubview:self.pinTextfield]; -} - -- (UILabel *)createForgotLink -{ - UILabel *label = [UILabel new]; - label.textColor = [UIColor ows_materialBlueColor]; - NSString *text = NSLocalizedString( - @"REGISTER_2FA_FORGOT_PIN", @"Label for 'I forgot my PIN' link in the 2FA registration view."); - label.attributedText = [[NSAttributedString alloc] - initWithString:text - attributes:@{ - NSForegroundColorAttributeName : [UIColor ows_materialBlueColor], - NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) - }]; - label.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)]; - label.numberOfLines = 0; - label.lineBreakMode = NSLineBreakByWordWrapping; - label.textAlignment = NSTextAlignmentCenter; - label.userInteractionEnabled = YES; - [label addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self - action:@selector(forgotPinLinkTapped:)]]; - [self.view addSubview:label]; - return label; -} - -- (void)createSubmitButton -{ - const CGFloat kSubmitButtonHeight = 47.f; - // NOTE: We use ows_signalBrandBlueColor instead of ows_materialBlueColor - // throughout the onboarding flow to be consistent with the headers. - OWSFlatButton *submitButton = - [OWSFlatButton buttonWithTitle:NSLocalizedString(@"REGISTER_2FA_SUBMIT_BUTTON", - @"Label for 'submit' button in the 2FA registration view.") - font:[OWSFlatButton fontForHeight:kSubmitButtonHeight] - titleColor:[UIColor whiteColor] - backgroundColor:[UIColor ows_signalBrandBlueColor] - target:self - selector:@selector(submitButtonWasPressed)]; - self.submitButton = submitButton; - [self.view addSubview:self.submitButton]; - [self.submitButton autoSetDimension:ALDimensionHeight toSize:kSubmitButtonHeight]; -} - -- (CGFloat)hMargin -{ - return 20.f; -} - -- (void)createContents -{ - const CGFloat kVSpacing = 30.f; - - NSString *instructionsText = NSLocalizedString( - @"REGISTER_2FA_INSTRUCTIONS", @"Instructions to enter the 'two-factor auth pin' in the 2FA registration view."); - UILabel *instructionsLabel = [self createLabelWithText:instructionsText]; - [instructionsLabel autoPinTopToSuperviewWithMargin:kVSpacing]; - [instructionsLabel autoPinWidthToSuperviewWithMargin:self.hMargin]; - - UILabel *createForgotLink = [self createForgotLink]; - [createForgotLink autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:instructionsLabel withOffset:5]; - [createForgotLink autoPinWidthToSuperviewWithMargin:self.hMargin]; - - [self createPinTextfield]; - [self.pinTextfield autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:createForgotLink withOffset:kVSpacing]; - [self.pinTextfield autoPinWidthToSuperviewWithMargin: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 autoPinWidthToSuperviewWithMargin:self.hMargin]; - [underscoreView autoSetDimension:ALDimensionHeight toSize:1.f]; - - [self createSubmitButton]; - [self.submitButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:underscoreView withOffset:kVSpacing]; - [self.submitButton autoPinWidthToSuperviewWithMargin:self.hMargin]; - - [self updateEnabling]; -} - -- (void)updateEnabling -{ - [self.submitButton setEnabled:self.hasValidPin]; + [self.entryView makePinTextFieldFirstResponder]; } -#pragma mark - UITextFieldDelegate +#pragma mark - PinEntryViewDelegate -- (BOOL)textField:(UITextField *)textField - shouldChangeCharactersInRange:(NSRange)range - replacementString:(NSString *)insertionText +- (void)pinEntryView:(PinEntryView *)entryView submittedPinCode:(NSString *)pinCode { + OWSAssert(self.entryView.hasValidPin); - [ViewControllerUtils ows2FAPINTextField:textField - shouldChangeCharactersInRange:range - replacementString:insertionText]; - - [self updateEnabling]; - - return NO; + [self tryToRegisterWithPinCode:pinCode]; } -#pragma mark - Events - -- (void)submitButtonWasPressed +- (void)pinEntryViewForgotPinLinkTapped:(PinEntryView *)entryView { - OWSAssert(self.hasValidPin); - - [self tryToRegister]; + NSString *alertBody = NSLocalizedString(@"REGISTER_2FA_FORGOT_PIN_ALERT_MESSAGE", + @"Alert message explaining what happens if you forget your 'two-factor auth pin'."); + [OWSAlerts showAlertWithTitle:nil message:alertBody]; } -- (BOOL)hasValidPin -{ - return self.pinTextfield.text.length >= kMin2FAPinLength; -} +#pragma mark - Registration -- (void)tryToRegister +- (void)tryToRegisterWithPinCode:(NSString *)pinCode { - OWSAssert(self.hasValidPin); + OWSAssert(self.entryView.hasValidPin); OWSAssert(self.verificationCode.length > 0); - NSString *pin = self.pinTextfield.text; - OWSAssert(pin.length > 0); + OWSAssert(pinCode.length > 0); DDLogInfo(@"%@ %s", self.logTag, __PRETTY_FUNCTION__); @@ -235,10 +114,11 @@ NS_ASSUME_NONNULL_BEGIN canCancel:NO backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { OWSProdInfo([OWSAnalyticsEvents registrationRegisteringCode]); - [self.accountManager registerWithVerificationCode:self.verificationCode pin:pin] + [self.accountManager registerWithVerificationCode:self.verificationCode pin:pinCode] .then(^{ OWSAssertIsOnMainThread(); OWSProdInfo([OWSAnalyticsEvents registrationRegisteringSubmittedCode]); + [[OWS2FAManager sharedManager] mark2FAAsEnabledWithPin:pinCode]; DDLogInfo(@"%@ Successfully registered Signal account.", weakSelf.logTag); dispatch_async(dispatch_get_main_queue(), ^{ @@ -263,7 +143,7 @@ NS_ASSUME_NONNULL_BEGIN @"register with 'two-factor auth' failed.") message:error.localizedDescription]; - [weakSelf.pinTextfield becomeFirstResponder]; + [weakSelf.entryView makePinTextFieldFirstResponder]; }]; }); }); @@ -275,16 +155,6 @@ NS_ASSUME_NONNULL_BEGIN [ProfileViewController presentForRegistration:self.navigationController]; } -- (void)forgotPinLinkTapped:(UIGestureRecognizer *)sender -{ - if (sender.state == UIGestureRecognizerStateRecognized) { - [OWSAlerts - showAlertWithTitle:nil - message:NSLocalizedString(@"REGISTER_2FA_FORGOT_PIN_ALERT_MESSAGE", - @"Alert message explaining what happens if you forget your 'two-factor auth pin'.")]; - } -} - @end NS_ASSUME_NONNULL_END diff --git a/Signal/src/ViewControllers/OWS2FAReminderViewController.swift b/Signal/src/ViewControllers/OWS2FAReminderViewController.swift new file mode 100644 index 000000000..510088052 --- /dev/null +++ b/Signal/src/ViewControllers/OWS2FAReminderViewController.swift @@ -0,0 +1,110 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import UIKit + +@objc +public class OWS2FAReminderViewController: UIViewController, PinEntryViewDelegate { + + private var ows2FAManager: OWS2FAManager { + return OWS2FAManager.shared() + } + + var pinEntryView: PinEntryView! + + @objc + public class func wrappedInNavController() -> UINavigationController { + let navController = UINavigationController() + navController.pushViewController(OWS2FAReminderViewController(), animated: false) + + return navController + } + + override public func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + pinEntryView.makePinTextFieldFirstResponder() + } + + override public func loadView() { + assert(ows2FAManager.pinCode != nil) + + self.navigationItem.title = NSLocalizedString("REMINDER_2FA_NAV_TITLE", comment: "Navbar title for when user is peridoically prompted to enter their registration lock PIN") + + self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(didPressCloseButton)) + + let view = UIView() + self.view = view + view.backgroundColor = .white + + let pinEntryView = PinEntryView() + self.pinEntryView = pinEntryView + pinEntryView.delegate = self + let instructionsText = NSLocalizedString("REMINDER_2FA_BODY", comment: "Body text for when user is peridoically prompted to enter their registration lock PIN") + pinEntryView.instructionsText = instructionsText + + view.addSubview(pinEntryView) + + pinEntryView.autoPinWidthToSuperview(withMargin: 20) + pinEntryView.autoPin(toTopLayoutGuideOf: self, withInset: 0) + pinEntryView.autoPin(toBottomLayoutGuideOf: self, withInset: 0) + } + + // MARK: PinEntryViewDelegate + public func pinEntryView(_ entryView: PinEntryView, submittedPinCode pinCode: String) { + Logger.info("\(logTag) in \(#function)") + if checkResult(pinCode: pinCode) { + didSubmitCorrectPin() + } else { + didSubmitWrongPin() + } + } + + //textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + public func pinEntryView(_ entryView: PinEntryView, pinCodeDidChange pinCode: String) { + // optimistically match, without having to press "done" + if checkResult(pinCode: pinCode) { + didSubmitCorrectPin() + } + } + + public func pinEntryViewForgotPinLinkTapped(_ entryView: PinEntryView) { + Logger.info("\(logTag) in \(#function)") + let alertBody = NSLocalizedString("REMINDER_2FA_FORGOT_PIN_ALERT_MESSAGE", + comment: "Alert message explaining what happens if you forget your 'two-factor auth pin'") + OWSAlerts.showAlert(title:nil, message:alertBody) + } + + // MARK: Helpers + + @objc + private func didPressCloseButton(sender: UIButton) { + Logger.info("\(logTag) in \(#function)") + // We'll ask again next time they launch + self.dismiss(animated: true) + } + + private func checkResult(pinCode: String) -> Bool { + return pinCode == ows2FAManager.pinCode + } + + private func didSubmitCorrectPin() { + Logger.info("\(logTag) in \(#function) noWrongGuesses: \(noWrongGuesses)") + + self.dismiss(animated: true) + + OWS2FAManager.shared().updateRepetitionInterval(withWasSuccessful: noWrongGuesses) + } + + var noWrongGuesses = true + private func didSubmitWrongPin() { + noWrongGuesses = false + Logger.info("\(logTag) in \(#function)") + let alertTitle = NSLocalizedString("REMINDER_2FA_WRONG_PIN_ALERT_TITLE", + comment: "Alert title after wrong guess for 'two-factor auth pin' reminder activity") + let alertBody = NSLocalizedString("REMINDER_2FA_WRONG_PIN_ALERT_BODY", + comment: "Alert body after wrong guess for 'two-factor auth pin' reminder activity") + OWSAlerts.showAlert(title: alertTitle, message: alertBody) + self.pinEntryView.clearText() + } +} diff --git a/Signal/src/ViewControllers/OWS2FASettingsViewController.m b/Signal/src/ViewControllers/OWS2FASettingsViewController.m index 979cc0e00..17921f7c4 100644 --- a/Signal/src/ViewControllers/OWS2FASettingsViewController.m +++ b/Signal/src/ViewControllers/OWS2FASettingsViewController.m @@ -392,7 +392,7 @@ NS_ASSUME_NONNULL_BEGIN presentFromViewController:self canCancel:NO backgroundBlock:^(ModalActivityIndicatorViewController *modalActivityIndicator) { - [OWS2FAManager.sharedManager enable2FAWithPin:self.candidatePin + [OWS2FAManager.sharedManager requestEnable2FAWithPin:self.candidatePin success:^{ [modalActivityIndicator dismissWithCompletion:^{ [weakSelf showCompleteUI]; @@ -408,10 +408,10 @@ NS_ASSUME_NONNULL_BEGIN [weakSelf updateTableContents]; [OWSAlerts - showAlertWithTitle:NSLocalizedString(@"ALERT_ERROR_TITLE", @"") - message:NSLocalizedString(@"ENABLE_2FA_VIEW_COULD_NOT_ENABLE_2FA", + showAlertWithTitle:NSLocalizedString(@"ENABLE_2FA_VIEW_COULD_NOT_ENABLE_2FA", @"Error indicating that attempt to enable 'two-factor " - @"auth' failed.")]; + @"auth' failed.") + message:error.localizedDescription]; }]; }]; }]; diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index c1425177c..6320e8dec 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -1102,7 +1102,7 @@ protocol CallServiceObserver: class { // We don't need to worry about the user granting or remoting this permission // during a call while the app is in the background, because changing this // permission kills the app. - OWSAlerts.showAlert(withTitle: NSLocalizedString("MISSING_CAMERA_PERMISSION_TITLE", comment: "Alert title when camera is not authorized"), + OWSAlerts.showAlert(title: NSLocalizedString("MISSING_CAMERA_PERMISSION_TITLE", comment: "Alert title when camera is not authorized"), message: NSLocalizedString("MISSING_CAMERA_PERMISSION_MESSAGE", comment: "Alert body when camera is not authorized")) } }) diff --git a/Signal/src/views/PinEntryView.h b/Signal/src/views/PinEntryView.h new file mode 100644 index 000000000..fd6c31a12 --- /dev/null +++ b/Signal/src/views/PinEntryView.h @@ -0,0 +1,30 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +@class PinEntryView; + +@protocol PinEntryViewDelegate + +- (void)pinEntryView:(PinEntryView *)entryView submittedPinCode:(NSString *)pinCode; +- (void)pinEntryViewForgotPinLinkTapped:(PinEntryView *)entryView; + +@optional +- (void)pinEntryView:(PinEntryView *)entryView pinCodeDidChange:(NSString *)pinCode; + +@end + +@interface PinEntryView : UIView + +@property (nonatomic, weak, nullable) id delegate; +@property (nonatomic, readonly) BOOL hasValidPin; +@property (nullable, nonatomic) NSString *instructionsText; + +- (void)clearText; +- (BOOL)makePinTextFieldFirstResponder; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/src/views/PinEntryView.m b/Signal/src/views/PinEntryView.m new file mode 100644 index 000000000..daa1c50c4 --- /dev/null +++ b/Signal/src/views/PinEntryView.m @@ -0,0 +1,205 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +#import "PinEntryView.h" +#import "Signal-Swift.h" +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PinEntryView () + +@property (nonatomic) UITextField *pinTextfield; +@property (nonatomic) OWSFlatButton *submitButton; +@property (nonatomic) UILabel *instructionsLabel; + +@end + +@implementation PinEntryView : UIView + +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + if (!self) { + return self; + } + + [self createContents]; + + return self; +} + +#pragma mark - view creation + +- (UILabel *)createLabelWithText:(nullable NSString *)text +{ + UILabel *label = [UILabel new]; + label.textColor = [UIColor blackColor]; + label.text = text; + label.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)]; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.textAlignment = NSTextAlignmentCenter; + [self addSubview:label]; + return label; +} + +- (void)createPinTextfield +{ + self.pinTextfield = [UITextField new]; + self.pinTextfield.textColor = [UIColor blackColor]; + self.pinTextfield.placeholder + = NSLocalizedString(@"2FA_PIN_DEFAULT_TEXT", @"Text field placeholder when entering a 'two-factor auth pin'."); + 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 addSubview:self.pinTextfield]; +} + +- (UILabel *)createForgotLink +{ + UILabel *label = [UILabel new]; + label.textColor = [UIColor ows_materialBlueColor]; + NSString *text = NSLocalizedString( + @"REGISTER_2FA_FORGOT_PIN", @"Label for 'I forgot my PIN' link in the 2FA registration view."); + label.attributedText = [[NSAttributedString alloc] + initWithString:text + attributes:@{ + NSForegroundColorAttributeName : [UIColor ows_materialBlueColor], + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle | NSUnderlinePatternSolid) + }]; + label.font = [UIFont ows_regularFontWithSize:ScaleFromIPhone5To7Plus(14.f, 16.f)]; + label.numberOfLines = 0; + label.lineBreakMode = NSLineBreakByWordWrapping; + label.textAlignment = NSTextAlignmentCenter; + label.userInteractionEnabled = YES; + [label addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self + action:@selector(forgotPinLinkTapped:)]]; + [self addSubview:label]; + return label; +} + +- (void)createSubmitButton +{ + const CGFloat kSubmitButtonHeight = 47.f; + // NOTE: We use ows_signalBrandBlueColor instead of ows_materialBlueColor + // throughout the onboarding flow to be consistent with the headers. + OWSFlatButton *submitButton = + [OWSFlatButton buttonWithTitle:NSLocalizedString(@"REGISTER_2FA_SUBMIT_BUTTON", + @"Label for 'submit' button in the 2FA registration view.") + font:[OWSFlatButton fontForHeight:kSubmitButtonHeight] + titleColor:[UIColor whiteColor] + backgroundColor:[UIColor ows_signalBrandBlueColor] + target:self + selector:@selector(submitButtonWasPressed)]; + self.submitButton = submitButton; + [self addSubview:submitButton]; + [self.submitButton autoSetDimension:ALDimensionHeight toSize:kSubmitButtonHeight]; +} + +- (nullable NSString *)instructionsText +{ + return self.instructionsLabel.text; +} + +- (void)setInstructionsText:(nullable NSString *)instructionsText +{ + self.instructionsLabel.text = instructionsText; +} + +- (void)createContents +{ + const CGFloat kVSpacing = 30.f; + + UILabel *instructionsLabel = [self createLabelWithText:nil]; + self.instructionsLabel = instructionsLabel; + [instructionsLabel autoPinTopToSuperviewWithMargin:kVSpacing]; + [instructionsLabel autoPinWidthToSuperview]; + + UILabel *createForgotLink = [self createForgotLink]; + [createForgotLink autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:instructionsLabel withOffset:5]; + [createForgotLink autoPinWidthToSuperview]; + + [self createPinTextfield]; + [self.pinTextfield autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:createForgotLink withOffset:kVSpacing]; + [self.pinTextfield autoPinWidthToSuperview]; + + UIView *underscoreView = [UIView new]; + underscoreView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.f]; + [self addSubview:underscoreView]; + [underscoreView autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.pinTextfield withOffset:3]; + [underscoreView autoPinWidthToSuperview]; + [underscoreView autoSetDimension:ALDimensionHeight toSize:1.f]; + + [self createSubmitButton]; + [self.submitButton autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:underscoreView withOffset:kVSpacing]; + [self.submitButton autoPinWidthToSuperview]; + [self updateIsSubmitEnabled]; +} + +#pragma mark - UITextFieldDelegate + +- (BOOL)textField:(UITextField *)textField + shouldChangeCharactersInRange:(NSRange)range + replacementString:(NSString *)insertionText +{ + + [ViewControllerUtils ows2FAPINTextField:textField + shouldChangeCharactersInRange:range + replacementString:insertionText]; + + [self updateIsSubmitEnabled]; + + if (self.delegate && [self.delegate respondsToSelector:@selector(pinEntryView:pinCodeDidChange:)]) { + [self.delegate pinEntryView:self pinCodeDidChange:textField.text]; + } + + return NO; +} + +- (void)updateIsSubmitEnabled; +{ + [self.submitButton setEnabled:self.hasValidPin]; +} + +- (BOOL)makePinTextFieldFirstResponder +{ + return [self.pinTextfield becomeFirstResponder]; +} + +- (BOOL)hasValidPin +{ + return self.pinTextfield.text.length >= kMin2FAPinLength; +} + +- (void)clearText +{ + self.pinTextfield.text = @""; + [self updateIsSubmitEnabled]; +} + +#pragma mark - Events + +- (void)submitButtonWasPressed +{ + [self.delegate pinEntryView:self submittedPinCode:self.pinTextfield.text]; +} + +- (void)forgotPinLinkTapped:(UIGestureRecognizer *)sender +{ + if (sender.state == UIGestureRecognizerStateRecognized) { + [self.delegate pinEntryViewForgotPinLinkTapped:self]; + } +} + + +@end + +NS_ASSUME_NONNULL_END diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index bbd9fdf00..08e45c912 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1468,6 +1468,21 @@ /* No comment provided by engineer. */ "RELAY_REGISTERED_ERROR_RECOVERY" = "The phone number you are trying to register has already been registered on another server, please unregister from there and try again."; +/* Body text for when user is peridoically prompted to enter their registration lock PIN */ +"REMINDER_2FA_BODY" = "Registration Lock is enabled for your phone number."; + +/* Alert message explaining what happens if you forget your 'two-factor auth pin' */ +"REMINDER_2FA_FORGOT_PIN_ALERT_MESSAGE" = "Registration Lock helps protect your phone number from unauthorized registration attempts. This feature can be disabled at any time in your Signal privacy settings."; + +/* Navbar title for when user is peridoically prompted to enter their registration lock PIN */ +"REMINDER_2FA_NAV_TITLE" = "Enter Your Registration Lock PIN"; + +/* Alert body after wrong guess for 'two-factor auth pin' reminder activity */ +"REMINDER_2FA_WRONG_PIN_ALERT_BODY" = "You can set a new PIN in your privacy settings."; + +/* Alert title after wrong guess for 'two-factor auth pin' reminder activity */ +"REMINDER_2FA_WRONG_PIN_ALERT_TITLE" = "That is not the correct PIN."; + /* No comment provided by engineer. */ "REREGISTER_FOR_PUSH" = "Re-register for push notifications"; diff --git a/SignalMessaging/SignalMessaging.h b/SignalMessaging/SignalMessaging.h index d89d5c580..ccea401d5 100644 --- a/SignalMessaging/SignalMessaging.h +++ b/SignalMessaging/SignalMessaging.h @@ -44,3 +44,4 @@ FOUNDATION_EXPORT const unsigned char SignalMessagingVersionString[]; #import #import #import +#import diff --git a/SignalMessaging/views/OWSAlerts.swift b/SignalMessaging/views/OWSAlerts.swift index c9c161909..1d367cfab 100644 --- a/SignalMessaging/views/OWSAlerts.swift +++ b/SignalMessaging/views/OWSAlerts.swift @@ -23,17 +23,17 @@ import Foundation } @objc - public class func showAlert(withTitle title: String) { - self.showAlert(withTitle: title, message: nil, buttonTitle: nil) + public class func showAlert(title: String) { + self.showAlert(title: title, message: nil, buttonTitle: nil) } @objc - public class func showAlert(withTitle title: String?, message: String) { - self.showAlert(withTitle: title, message: message, buttonTitle: nil) + public class func showAlert(title: String?, message: String) { + self.showAlert(title: title, message: message, buttonTitle: nil) } @objc - public class func showAlert(withTitle title: String?, message: String? = nil, buttonTitle: String? = nil, buttonAction: ((UIAlertAction) -> Void)? = nil) { + public class func showAlert(title: String?, message: String? = nil, buttonTitle: String? = nil, buttonAction: ((UIAlertAction) -> Void)? = nil) { let actionTitle = buttonTitle ?? NSLocalizedString("OK", comment: "") let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -42,7 +42,7 @@ import Foundation } @objc - public class func showConfirmationAlert(withTitle title: String, message: String? = nil, proceedTitle: String? = nil, proceedAction: @escaping (UIAlertAction) -> Void) { + public class func showConfirmationAlert(title: String, message: String? = nil, proceedTitle: String? = nil, proceedAction: @escaping (UIAlertAction) -> Void) { assert(title.count > 0) let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -88,7 +88,7 @@ import Foundation Environment.preferences().setIOSUpgradeNagDate(Date()) - OWSAlerts.showAlert(withTitle: NSLocalizedString("UPGRADE_IOS_ALERT_TITLE", + OWSAlerts.showAlert(title: NSLocalizedString("UPGRADE_IOS_ALERT_TITLE", comment: "Title for the alert indicating that user should upgrade iOS."), message: NSLocalizedString("UPGRADE_IOS_ALERT_MESSAGE", comment: "Message for the alert indicating that user should upgrade iOS.")) diff --git a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h index 4df5f5046..bbbdffabb 100644 --- a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h +++ b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.h @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)hasObjectForKey:(NSString *)key inCollection:(NSString *)collection; - (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection; - (BOOL)boolForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(BOOL)defaultValue; +- (double)doubleForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(double)defaultValue; - (int)intForKey:(NSString *)key inCollection:(NSString *)collection; - (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection; - (nullable NSDate *)dateForKey:(NSString *)key inCollection:(NSString *)collection; @@ -31,6 +32,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)setObject:(id)object forKey:(NSString *)key inCollection:(NSString *)collection; - (void)setBool:(BOOL)value forKey:(NSString *)key inCollection:(NSString *)collection; +- (void)setDouble:(double)value forKey:(NSString *)key inCollection:(NSString *)collection; - (void)removeObjectForKey:(NSString *)string inCollection:(NSString *)collection; - (void)setInt:(int)integer forKey:(NSString *)key inCollection:(NSString *)collection; - (void)setDate:(NSDate *)value forKey:(NSString *)key inCollection:(NSString *)collection; diff --git a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m index 44d11aa68..c90134deb 100644 --- a/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m +++ b/SignalServiceKit/src/Storage/YapDatabaseConnection+OWS.m @@ -34,13 +34,14 @@ NS_ASSUME_NONNULL_BEGIN return object; } -- (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection ofExpectedType:(Class) class { +- (nullable id)objectForKey:(NSString *)key inCollection:(NSString *)collection ofExpectedType:(Class)class +{ id _Nullable value = [self objectForKey:key inCollection:collection]; OWSAssert(!value || [value isKindOfClass:class]); return value; } - - (nullable NSDictionary *)dictionaryForKey : (NSString *)key inCollection : (NSString *)collection +- (nullable NSDictionary *)dictionaryForKey:(NSString *)key inCollection:(NSString *)collection { return [self objectForKey:key inCollection:collection ofExpectedType:[NSDictionary class]]; } @@ -61,6 +62,12 @@ NS_ASSUME_NONNULL_BEGIN return value ? [value boolValue] : defaultValue; } +- (double)doubleForKey:(NSString *)key inCollection:(NSString *)collection defaultValue:(double)defaultValue +{ + NSNumber *_Nullable value = [self objectForKey:key inCollection:collection ofExpectedType:[NSNumber class]]; + return value ? [value doubleValue] : defaultValue; +} + - (nullable NSData *)dataForKey:(NSString *)key inCollection:(NSString *)collection { return [self objectForKey:key inCollection:collection ofExpectedType:[NSData class]]; @@ -117,7 +124,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)setObject:(id)object forKey:(NSString *)key inCollection:(NSString *)collection { - OWSAssert(object); OWSAssert(key.length > 0); OWSAssert(collection.length > 0); @@ -134,6 +140,14 @@ NS_ASSUME_NONNULL_BEGIN [self setObject:@(value) forKey:key inCollection:collection]; } +- (void)setDouble:(double)value forKey:(NSString *)key inCollection:(NSString *)collection +{ + OWSAssert(key.length > 0); + OWSAssert(collection.length > 0); + + [self setObject:@(value) forKey:key inCollection:collection]; +} + - (void)removeObjectForKey:(NSString *)key inCollection:(NSString *)collection { OWSAssert(key.length > 0); diff --git a/SignalServiceKit/src/Util/OWS2FAManager.h b/SignalServiceKit/src/Util/OWS2FAManager.h index 9a2828b64..3eba7b085 100644 --- a/SignalServiceKit/src/Util/OWS2FAManager.h +++ b/SignalServiceKit/src/Util/OWS2FAManager.h @@ -16,14 +16,23 @@ typedef void (^OWS2FAFailure)(NSError *error); + (instancetype)sharedManager; +@property (nullable, nonatomic, readonly) NSString *pinCode; + - (BOOL)is2FAEnabled; +- (BOOL)isDueForReminder; + +// Request with service +- (void)requestEnable2FAWithPin:(NSString *)pin + success:(nullable OWS2FASuccess)success + failure:(nullable OWS2FAFailure)failure; -- (void)enable2FAWithPin:(NSString *)pin - success:(nullable OWS2FASuccess)success - failure:(nullable OWS2FAFailure)failure; +// Sore local settings if, used during registration +- (void)mark2FAAsEnabledWithPin:(NSString *)pin; - (void)disable2FAWithSuccess:(nullable OWS2FASuccess)success failure:(nullable OWS2FAFailure)failure; +- (void)updateRepetitionIntervalWithWasSuccessful:(BOOL)wasSuccessful; + @end NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWS2FAManager.m b/SignalServiceKit/src/Util/OWS2FAManager.m index a886f2f2a..6c93f2797 100644 --- a/SignalServiceKit/src/Util/OWS2FAManager.m +++ b/SignalServiceKit/src/Util/OWS2FAManager.m @@ -15,6 +15,12 @@ NSString *const NSNotificationName_2FAStateDidChange = @"NSNotificationName_2FAS NSString *const kOWS2FAManager_Collection = @"kOWS2FAManager_Collection"; NSString *const kOWS2FAManager_IsEnabledKey = @"kOWS2FAManager_IsEnabledKey"; +NSString *const kOWS2FAManager_LastSuccessfulReminderDateKey = @"kOWS2FAManager_LastSuccessfulReminderDateKey"; +NSString *const kOWS2FAManager_PinCode = @"kOWS2FAManager_PinCode"; +NSString *const kOWS2FAManager_RepetitionInterval = @"kOWS2FAManager_RepetitionInterval"; + +const NSUInteger kHourSecs = 60 * 60; +const NSUInteger kDaySecs = kHourSecs * 24; @interface OWS2FAManager () @@ -81,7 +87,15 @@ NSString *const kOWS2FAManager_IsEnabledKey = @"kOWS2FAManager_IsEnabledKey"; userInfo:nil]; } -- (void)enable2FAWithPin:(NSString *)pin success:(nullable OWS2FASuccess)success failure:(nullable OWS2FAFailure)failure +- (void)mark2FAAsEnabledWithPin:(NSString *)pin +{ + [self setIs2FAEnabled:YES]; + [self storePinCode:pin]; +} + +- (void)requestEnable2FAWithPin:(NSString *)pin + success:(nullable OWS2FASuccess)success + failure:(nullable OWS2FAFailure)failure { OWSAssert(pin.length > 0); OWSAssert(success); @@ -92,8 +106,7 @@ NSString *const kOWS2FAManager_IsEnabledKey = @"kOWS2FAManager_IsEnabledKey"; success:^(NSURLSessionDataTask *task, id responseObject) { OWSAssertIsOnMainThread(); - [self setIs2FAEnabled:YES]; - + [self mark2FAAsEnabledWithPin:pin]; if (success) { success(); } @@ -129,6 +142,115 @@ NSString *const kOWS2FAManager_IsEnabledKey = @"kOWS2FAManager_IsEnabledKey"; }]; } + +#pragma mark - Reminders + +- (void)storePinCode:(nullable NSString *)pinCode +{ + [self.dbConnection setObject:pinCode forKey:kOWS2FAManager_PinCode inCollection:kOWS2FAManager_Collection]; +} + +- (nullable NSString *)pinCode +{ + return [self.dbConnection objectForKey:kOWS2FAManager_PinCode inCollection:kOWS2FAManager_Collection]; +} + +- (nullable NSDate *)lastSuccessfulReminderDate +{ + return [self.dbConnection dateForKey:kOWS2FAManager_LastSuccessfulReminderDateKey + inCollection:kOWS2FAManager_Collection]; +} + +- (void)setLastSuccessfulReminderDate:(nullable NSDate *)date +{ + DDLogDebug(@"%@ Seting setLastSuccessfulReminderDate:%@", self.logTag, date); + [self.dbConnection setDate:date + forKey:kOWS2FAManager_LastSuccessfulReminderDateKey + inCollection:kOWS2FAManager_Collection]; +} + +- (BOOL)isDueForReminder +{ + if (!self.is2FAEnabled) { + return NO; + } + + return self.nextReminderDate.timeIntervalSinceNow < 0; +} + +- (NSDate *)nextReminderDate +{ + NSDate *lastSuccessfulReminderDate = self.lastSuccessfulReminderDate ?: [NSDate distantPast]; + + return [lastSuccessfulReminderDate dateByAddingTimeInterval:self.repetitionInterval]; +} + +- (NSArray *)allRepetitionIntervals +{ + // Keep sorted monotonically increasing. + return @[ + @(6 * kHourSecs), + @(12 * kHourSecs), + @(1 * kDaySecs), + @(3 * kDaySecs), + @(7 * kDaySecs), + ]; +} + +- (double)defaultRepetitionInterval +{ + return self.allRepetitionIntervals.firstObject.doubleValue; +} + +- (NSTimeInterval)repetitionInterval +{ + return [self.dbConnection doubleForKey:kOWS2FAManager_RepetitionInterval + inCollection:kOWS2FAManager_Collection + defaultValue:self.defaultRepetitionInterval]; +} + +- (void)updateRepetitionIntervalWithWasSuccessful:(BOOL)wasSuccessful +{ + if (wasSuccessful) { + self.lastSuccessfulReminderDate = [NSDate new]; + } + + NSTimeInterval oldInterval = self.repetitionInterval; + NSTimeInterval newInterval = [self adjustRepetitionInterval:oldInterval wasSuccessful:wasSuccessful]; + + DDLogInfo(@"%@ %@ guess. Updating repetition interval: %f -> %f", + self.logTag, + (wasSuccessful ? @"successful" : @"failed"), + oldInterval, + newInterval); + [self.dbConnection setDouble:newInterval + forKey:kOWS2FAManager_RepetitionInterval + inCollection:kOWS2FAManager_Collection]; +} + +- (NSTimeInterval)adjustRepetitionInterval:(NSTimeInterval)oldInterval wasSuccessful:(BOOL)wasSuccessful +{ + NSArray *allIntervals = self.allRepetitionIntervals; + + NSUInteger oldIndex = + [allIntervals indexOfObjectPassingTest:^BOOL(NSNumber *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + return oldInterval >= (NSTimeInterval)obj.doubleValue; + }]; + + NSUInteger newIndex; + if (wasSuccessful) { + newIndex = oldIndex + 1; + } else { + newIndex = oldIndex - 1; + } + + // clamp to be valid + newIndex = MAX(0, MIN(allIntervals.count - 1, newIndex)); + + NSTimeInterval newInterval = allIntervals[newIndex].doubleValue; + return newInterval; +} + @end NS_ASSUME_NONNULL_END diff --git a/SignalShareExtension/ShareViewController.swift b/SignalShareExtension/ShareViewController.swift index 6f391160b..c879228ff 100644 --- a/SignalShareExtension/ShareViewController.swift +++ b/SignalShareExtension/ShareViewController.swift @@ -492,7 +492,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed let alertTitle = NSLocalizedString("SHARE_EXTENSION_UNABLE_TO_BUILD_ATTACHMENT_ALERT_TITLE", comment: "Shown when trying to share content to a Signal user for the share extension. Followed by failure details.") - OWSAlerts.showAlert(withTitle: alertTitle, + OWSAlerts.showAlert(title: alertTitle, message: error.localizedDescription, buttonTitle: CommonStrings.cancelButton) { _ in strongSelf.shareViewWasCancelled()