Sketch out the 'onboarding 2FA' view.

This commit is contained in:
Matthew Chen 2019-02-18 11:02:03 -05:00
parent 9d0813d7b9
commit 0b55ecc682
6 changed files with 237 additions and 12 deletions

View File

@ -165,6 +165,7 @@
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956A21A301A100DCFE74 /* OWSBackupJob.m */; };
3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */; };
349EA07C2162AEA800F7B17F /* OWS111UDAttributesMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */; };
349ED990221B0194008045B0 /* Onboarding2FAViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 349ED98F221B0194008045B0 /* Onboarding2FAViewController.swift */; };
34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */; };
34A4C62022175C5C0042EF2E /* OnboardingProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4C61F22175C5C0042EF2E /* OnboardingProfileViewController.swift */; };
34A55F3720485465002CC6DE /* OWS2FARegistrationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */; };
@ -848,6 +849,7 @@
3496956C21A301A100DCFE74 /* OWSBackupImportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportJob.h; sourceTree = "<group>"; };
3496956D21A301A100DCFE74 /* OWSBackupIO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupIO.h; sourceTree = "<group>"; };
349EA07B2162AEA700F7B17F /* OWS111UDAttributesMigration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWS111UDAttributesMigration.swift; sourceTree = "<group>"; };
349ED98F221B0194008045B0 /* Onboarding2FAViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Onboarding2FAViewController.swift; sourceTree = "<group>"; };
34A4C61D221613D00042EF2E /* OnboardingVerificationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingVerificationViewController.swift; sourceTree = "<group>"; };
34A4C61F22175C5C0042EF2E /* OnboardingProfileViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingProfileViewController.swift; sourceTree = "<group>"; };
34A55F3520485464002CC6DE /* OWS2FARegistrationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWS2FARegistrationViewController.m; sourceTree = "<group>"; };
@ -1471,6 +1473,7 @@
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */,
340FC879204DAC8C007AEB0F /* CodeVerificationViewController.h */,
340FC877204DAC8C007AEB0F /* CodeVerificationViewController.m */,
349ED98F221B0194008045B0 /* Onboarding2FAViewController.swift */,
3448E1612213585C004B052E /* OnboardingBaseViewController.swift */,
3448E1652215B313004B052E /* OnboardingCaptchaViewController.swift */,
3448E15D221333F5004B052E /* OnboardingController.swift */,
@ -3530,6 +3533,7 @@
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */,
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
349ED990221B0194008045B0 /* Onboarding2FAViewController.swift in Sources */,
45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */,
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
452037D11EE84975004E4CDF /* DebugUISessionState.m in Sources */,

View File

@ -0,0 +1,176 @@
//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import UIKit
@objc
public class Onboarding2FAViewController: OnboardingBaseViewController {
private let pinTextField = UITextField()
private var pinStrokeNormal: UIView?
private var pinStrokeError: UIView?
private let validationWarningLabel = UILabel()
private var isPinInvalid = false {
didSet {
updateValidationWarnings()
}
}
override public func loadView() {
super.loadView()
view.backgroundColor = Theme.backgroundColor
view.layoutMargins = .zero
let titleLabel = self.titleLabel(text: NSLocalizedString("ONBOARDING_2FA_TITLE", comment: "Title of the 'onboarding 2FA' view."))
let explanationLabel1 = self.explanationLabel(explanationText: NSLocalizedString("ONBOARDING_2FA_EXPLANATION_1",
comment: "The first explanation in the 'onboarding 2FA' view."))
let explanationLabel2 = self.explanationLabel(explanationText: NSLocalizedString("ONBOARDING_2FA_EXPLANATION_2",
comment: "The first explanation in the 'onboarding 2FA' view."))
explanationLabel1.font = UIFont.ows_dynamicTypeCaption1
explanationLabel2.font = UIFont.ows_dynamicTypeCaption1
pinTextField.textAlignment = .center
pinTextField.delegate = self
pinTextField.keyboardType = .numberPad
pinTextField.textColor = Theme.primaryColor
pinTextField.font = UIFont.ows_dynamicTypeBodyClamped
pinTextField.setContentHuggingHorizontalLow()
pinTextField.setCompressionResistanceHorizontalLow()
pinTextField.autoSetDimension(.height, toSize: 40)
pinStrokeNormal = pinTextField.addBottomStroke()
pinStrokeError = pinTextField.addBottomStroke(color: .ows_destructiveRed, strokeWidth: 2)
validationWarningLabel.text = NSLocalizedString("ONBOARDING_PHONE_NUMBER_VALIDATION_WARNING",
comment: "Label indicating that the phone number is invalid in the 'onboarding phone number' view.")
validationWarningLabel.textColor = .ows_destructiveRed
validationWarningLabel.font = UIFont.ows_dynamicTypeSubheadlineClamped
validationWarningLabel.textAlignment = .center
let validationWarningRow = UIView()
validationWarningRow.addSubview(validationWarningLabel)
validationWarningLabel.ows_autoPinToSuperviewEdges()
validationWarningRow.autoSetDimension(.height, toSize: validationWarningLabel.font.lineHeight)
let forgotPinLink = self.linkButton(title: NSLocalizedString("ONBOARDING_2FA_FORGOT_PIN_LINK",
comment: "Label for the 'forgot 2FA PIN' link in the 'onboarding 2FA' view."),
selector: #selector(forgotPinLinkTapped))
let nextButton = self.button(title: NSLocalizedString("BUTTON_NEXT",
comment: "Label for the 'next' button."),
selector: #selector(nextPressed))
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
let stackView = UIStackView(arrangedSubviews: [
titleLabel,
UIView.spacer(withHeight: 10),
explanationLabel1,
UIView.spacer(withHeight: 10),
explanationLabel2,
topSpacer,
pinTextField,
UIView.spacer(withHeight: 10),
validationWarningRow,
bottomSpacer,
forgotPinLink,
UIView.spacer(withHeight: 10),
nextButton
])
stackView.axis = .vertical
stackView.alignment = .fill
stackView.layoutMargins = UIEdgeInsets(top: 20, left: 32, bottom: 20, right: 32)
stackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(stackView)
stackView.autoPinWidthToSuperview()
stackView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true)
// Ensure whitespace is balanced, so inputs are vertically centered.
topSpacer.autoMatch(.height, to: .height, of: bottomSpacer)
updateValidationWarnings()
}
public override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
_ = pinTextField.becomeFirstResponder()
}
// MARK: - Events
@objc func forgotPinLinkTapped() {
Logger.info("")
OWSAlerts.showAlert(title: nil, message: NSLocalizedString("REGISTER_2FA_FORGOT_PIN_ALERT_MESSAGE",
comment: "Alert message explaining what happens if you forget your 'two-factor auth pin'."))
}
@objc func nextPressed() {
Logger.info("")
tryToVerify()
}
private func tryToVerify() {
Logger.info("")
guard let pin = pinTextField.text?.ows_stripped(),
pin.count > 0 else {
isPinInvalid = true
return
}
isPinInvalid = false
onboardingController.update(twoFAPin: pin)
onboardingController.tryToVerify(fromViewController: self, completion: { (outcome) in
if outcome == .invalid2FAPin {
self.isPinInvalid = true
} else if outcome == .invalidVerificationCode {
owsFailDebug("Invalid verification code in 2FA view.")
}
})
}
private func updateValidationWarnings() {
AssertIsOnMainThread()
pinStrokeNormal?.isHidden = isPinInvalid
pinStrokeError?.isHidden = !isPinInvalid
validationWarningLabel.isHidden = !isPinInvalid
}
}
// MARK: -
extension Onboarding2FAViewController: UITextFieldDelegate {
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let newString = string.digitsOnly
var oldText = ""
if let textFieldText = textField.text {
oldText = textFieldText
}
let left = oldText.substring(to: range.location)
let right = oldText.substring(from: range.location + range.length)
textField.text = left + newString + right
isPinInvalid = false
// Inform our caller that we took care of performing the change.
return false
}
public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
tryToVerify()
return false
}
}

View File

@ -273,6 +273,10 @@ public class OnboardingController: NSObject {
public private(set) var captchaToken: String?
public private(set) var verificationCode: String?
public private(set) var twoFAPin: String?
@objc
public func update(countryState: OnboardingCountryState) {
AssertIsOnMainThread()
@ -294,6 +298,20 @@ public class OnboardingController: NSObject {
self.captchaToken = captchaToken
}
@objc
public func update(verificationCode: String) {
AssertIsOnMainThread()
self.verificationCode = verificationCode
}
@objc
public func update(twoFAPin: String) {
AssertIsOnMainThread()
self.twoFAPin = twoFAPin
}
// MARK: - Debug
private static let kKeychainService_LastRegistered = "kKeychainService_LastRegistered"
@ -405,26 +423,35 @@ public class OnboardingController: NSObject {
// MARK: - Verification
public enum VerificationOutcome {
case success
case invalidVerificationCode
case invalid2FAPin
}
public func tryToVerify(fromViewController: UIViewController,
verificationCode: String,
pin: String?,
isInvalidCodeCallback : @escaping () -> Void) {
completion : @escaping (VerificationOutcome) -> Void) {
AssertIsOnMainThread()
guard let phoneNumber = phoneNumber else {
owsFailDebug("Missing phoneNumber.")
return
}
guard let verificationCode = verificationCode else {
completion(.invalidVerificationCode)
return
}
// Ensure the account manager state is up-to-date.
//
// TODO: We could skip this in production.
tsAccountManager.phoneNumberAwaitingVerification = phoneNumber.e164
let twoFAPin = self.twoFAPin
ModalActivityIndicatorViewController.present(fromViewController: fromViewController,
canCancel: true) { (modal) in
self.accountManager.register(verificationCode: verificationCode, pin: pin)
self.accountManager.register(verificationCode: verificationCode, pin: twoFAPin)
.done { (_) in
DispatchQueue.main.async {
modal.dismiss(completion: {
@ -438,7 +465,7 @@ public class OnboardingController: NSObject {
modal.dismiss(completion: {
self.verificationFailed(fromViewController: fromViewController,
error: error as NSError,
isInvalidCodeCallback: isInvalidCodeCallback)
completion: completion)
})
}
}).retainUntilComplete()
@ -446,7 +473,7 @@ public class OnboardingController: NSObject {
}
private func verificationFailed(fromViewController: UIViewController, error: NSError,
isInvalidCodeCallback : @escaping () -> Void) {
completion : @escaping (VerificationOutcome) -> Void) {
AssertIsOnMainThread()
if error.domain == OWSSignalServiceKitErrorDomain &&
@ -454,11 +481,13 @@ public class OnboardingController: NSObject {
Logger.info("Missing 2FA PIN.")
completion(.invalid2FAPin)
onboardingDidRequire2FAPin(viewController: fromViewController)
} else {
if error.domain == OWSSignalServiceKitErrorDomain &&
error.code == OWSErrorCode.userError.rawValue {
isInvalidCodeCallback()
completion(.invalidVerificationCode)
}
Logger.verbose("error: \(error.domain) \(error.code)")

View File

@ -103,7 +103,7 @@ public class OnboardingPhoneNumberViewController: OnboardingBaseViewController {
let validationWarningRow = UIView()
validationWarningRow.addSubview(validationWarningLabel)
validationWarningLabel.autoPinHeightToSuperview()
validationWarningLabel.autoPinEdge(toSuperviewEdge: .trailing)
validationWarningLabel.autoPinEdge(toSuperviewEdge: .leading)
// TODO: Finalize copy.

View File

@ -251,7 +251,6 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController
private var codeState = CodeState.sent
private var titleLabel: UILabel?
private let phoneNumberTextField = UITextField()
private let onboardingCodeView = OnboardingCodeView()
private var codeStateLink: OWSFlatButton?
private let errorLabel = UILabel()
@ -477,13 +476,18 @@ public class OnboardingVerificationViewController: OnboardingBaseViewController
Logger.info("")
guard onboardingCodeView.isComplete else {
self.setHasInvalidCode(true)
return
}
setHasInvalidCode(false)
onboardingController.tryToVerify(fromViewController: self, verificationCode: onboardingCodeView.verificationCode, pin: nil, isInvalidCodeCallback: {
self.setHasInvalidCode(true)
onboardingController.update(verificationCode: onboardingCodeView.verificationCode)
onboardingController.tryToVerify(fromViewController: self, completion: { (outcome) in
if outcome == .invalidVerificationCode {
self.setHasInvalidCode(true)
}
})
}

View File

@ -1508,6 +1508,18 @@
/* No comment provided by engineer. */
"OK" = "OK";
/* The first explanation in the 'onboarding 2FA' view. */
"ONBOARDING_2FA_EXPLANATION_1" = "This phone number has Registration Lock enabled. Please enter the Registration Lock PIN.";
/* The first explanation in the 'onboarding 2FA' view. */
"ONBOARDING_2FA_EXPLANATION_2" = "Your Registration Lock PIN is separate from the automated verification code that was sent to your phone during the last step.";
/* Label for the 'forgot 2FA PIN' link in the 'onboarding 2FA' view. */
"ONBOARDING_2FA_FORGOT_PIN_LINK" = "I forgot my PIN";
/* Title of the 'onboarding 2FA' view. */
"ONBOARDING_2FA_TITLE" = "Registration Lock";
/* Title of the 'onboarding Captcha' view. */
"ONBOARDING_CAPTCHA_TITLE" = "We need to verify that you're human";
@ -1547,7 +1559,7 @@
/* Label for the link that lets users change their phone number in the onboarding views. */
"ONBOARDING_VERIFICATION_BACK_LINK" = "Wrong number?";
/* Format for the label of the 'pending code' label of the 'onboarding verification' view. Embeds {{the time until the code can be resent}}. */
/* Format for the label of the 'sent code' label of the 'onboarding verification' view. Embeds {{the time until the code can be resent}}. */
"ONBOARDING_VERIFICATION_CODE_COUNTDOWN_FORMAT" = "I didn't get a code (available in %@)";
/* Label indicating that the verification code is incorrect in the 'onboarding verification' view. */