Sketch out the 'onboarding 2FA' view.
This commit is contained in:
parent
9d0813d7b9
commit
0b55ecc682
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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. */
|
||||
|
|
Loading…
Reference in New Issue