967 lines
35 KiB
Swift
967 lines
35 KiB
Swift
//
|
|
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import PromiseKit
|
|
|
|
@objc
|
|
public class OnboardingCountryState: NSObject {
|
|
public let countryName: String
|
|
public let callingCode: String
|
|
public let countryCode: String
|
|
|
|
@objc
|
|
public init(countryName: String,
|
|
callingCode: String,
|
|
countryCode: String) {
|
|
self.countryName = countryName
|
|
self.callingCode = callingCode
|
|
self.countryCode = countryCode
|
|
}
|
|
|
|
public static var defaultValue: OnboardingCountryState {
|
|
AssertIsOnMainThread()
|
|
|
|
var countryCode: String = PhoneNumber.defaultCountryCode()
|
|
if let lastRegisteredCountryCode = OnboardingController.lastRegisteredCountryCode(),
|
|
lastRegisteredCountryCode.count > 0 {
|
|
countryCode = lastRegisteredCountryCode
|
|
}
|
|
|
|
let callingCodeNumber: NSNumber = PhoneNumberUtil.sharedThreadLocal().nbPhoneNumberUtil.getCountryCode(forRegion: countryCode)
|
|
let callingCode = "\(COUNTRY_CODE_PREFIX)\(callingCodeNumber)"
|
|
|
|
var countryName = NSLocalizedString("UNKNOWN_COUNTRY_NAME", comment: "Label for unknown countries.")
|
|
if let countryNameDerived = PhoneNumberUtil.countryName(fromCountryCode: countryCode) {
|
|
countryName = countryNameDerived
|
|
}
|
|
|
|
return OnboardingCountryState(countryName: countryName, callingCode: callingCode, countryCode: countryCode)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class OnboardingPhoneNumber: NSObject {
|
|
public let e164: String
|
|
public let userInput: String
|
|
|
|
@objc
|
|
public init(e164: String,
|
|
userInput: String) {
|
|
self.e164 = e164
|
|
self.userInput = userInput
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class OnboardingNavigationController: OWSNavigationController {
|
|
let onboardingController: OnboardingController
|
|
|
|
@objc
|
|
public init(onboardingController: OnboardingController) {
|
|
self.onboardingController = onboardingController
|
|
super.init()
|
|
if let nextMilestone = onboardingController.nextMilestone {
|
|
setViewControllers([onboardingController.nextViewController(milestone: nextMilestone)], animated: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public class OnboardingController: NSObject {
|
|
|
|
// MARK: - Dependencies
|
|
|
|
private var tsAccountManager: TSAccountManager {
|
|
return TSAccountManager.sharedInstance()
|
|
}
|
|
|
|
private static var tsAccountManager: TSAccountManager {
|
|
return TSAccountManager.sharedInstance()
|
|
}
|
|
|
|
private var accountManager: AccountManager {
|
|
return AppEnvironment.shared.accountManager
|
|
}
|
|
|
|
private var contactsManager: OWSContactsManager {
|
|
return Environment.shared.contactsManager
|
|
}
|
|
|
|
private var backup: OWSBackup {
|
|
return AppEnvironment.shared.backup
|
|
}
|
|
|
|
private var databaseStorage: SDSDatabaseStorage {
|
|
return .shared
|
|
}
|
|
|
|
private var profileManager: ProfileManagerProtocol {
|
|
return SSKEnvironment.shared.profileManager
|
|
}
|
|
|
|
private var ows2FAManager: OWS2FAManager {
|
|
return .shared()
|
|
}
|
|
|
|
// MARK: -
|
|
public enum OnboardingMode {
|
|
case provisioning
|
|
case registering
|
|
}
|
|
|
|
public static let defaultOnboardingMode: OnboardingMode = UIDevice.current.isIPad ? .provisioning : .registering
|
|
public var onboardingMode: OnboardingMode
|
|
public var isOnboardingModeOverriden: Bool {
|
|
return onboardingMode != OnboardingController.defaultOnboardingMode
|
|
}
|
|
|
|
public class func ascertainOnboardingMode() -> OnboardingMode {
|
|
if tsAccountManager.isRegisteredPrimaryDevice {
|
|
return .registering
|
|
} else if tsAccountManager.isRegistered {
|
|
return .provisioning
|
|
} else {
|
|
return defaultOnboardingMode
|
|
}
|
|
}
|
|
|
|
convenience override init() {
|
|
let onboardingMode = OnboardingController.ascertainOnboardingMode()
|
|
self.init(onboardingMode: onboardingMode)
|
|
}
|
|
|
|
init(onboardingMode: OnboardingMode) {
|
|
self.onboardingMode = onboardingMode
|
|
super.init()
|
|
Logger.info("onboardingMode: \(onboardingMode), requiredMilestones: \(requiredMilestones), completedMilestones: \(completedMilestones), nextMilestone: \(nextMilestone as Optional)")
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
enum OnboardingMilestone {
|
|
case verifiedPhoneNumber
|
|
case verifiedLinkedDevice
|
|
case restorePin
|
|
case setupProfile
|
|
case setupPin
|
|
}
|
|
|
|
var requiredMilestones: [OnboardingMilestone] {
|
|
switch onboardingMode {
|
|
case .provisioning:
|
|
return [.verifiedLinkedDevice]
|
|
case .registering:
|
|
var milestones: [OnboardingMilestone] = [.verifiedPhoneNumber, .setupProfile]
|
|
|
|
let hasPendingPinRestoration = databaseStorage.read {
|
|
KeyBackupService.hasPendingRestoration(transaction: $0)
|
|
}
|
|
|
|
if hasPendingPinRestoration {
|
|
milestones.insert(.restorePin, at: 0)
|
|
}
|
|
|
|
if FeatureFlags.pinsForNewUsers || hasPendingPinRestoration {
|
|
let hasBackupKeyRequestFailed = databaseStorage.read {
|
|
KeyBackupService.hasBackupKeyRequestFailed(transaction: $0)
|
|
}
|
|
|
|
if hasBackupKeyRequestFailed {
|
|
Logger.info("skipping setupPin since a previous request failed")
|
|
} else if hasPendingPinRestoration {
|
|
milestones.insert(.setupPin, at: 1)
|
|
} else {
|
|
milestones.append(.setupPin)
|
|
}
|
|
}
|
|
|
|
return milestones
|
|
}
|
|
}
|
|
|
|
@objc
|
|
var isComplete: Bool {
|
|
guard !tsAccountManager.isOnboarded() else {
|
|
Logger.debug("previously completed onboarding")
|
|
return true
|
|
}
|
|
|
|
guard nextMilestone != nil else {
|
|
Logger.debug("no remaining milestones")
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
var nextMilestone: OnboardingMilestone? {
|
|
requiredMilestones.first { !completedMilestones.contains($0) }
|
|
}
|
|
|
|
var completedMilestones: [OnboardingMilestone] {
|
|
var milestones: [OnboardingMilestone] = []
|
|
|
|
if tsAccountManager.isRegisteredPrimaryDevice {
|
|
milestones.append(.verifiedPhoneNumber)
|
|
} else if tsAccountManager.isRegistered {
|
|
milestones.append(.verifiedLinkedDevice)
|
|
}
|
|
|
|
if profileManager.hasProfileName {
|
|
milestones.append(.setupProfile)
|
|
}
|
|
|
|
if KeyBackupService.hasMasterKey {
|
|
milestones.append(.restorePin)
|
|
milestones.append(.setupPin)
|
|
}
|
|
|
|
return milestones
|
|
}
|
|
|
|
@objc
|
|
public func markAsOnboarded() {
|
|
guard !tsAccountManager.isOnboarded() else { return }
|
|
self.databaseStorage.asyncWrite {
|
|
Logger.info("completed onboarding")
|
|
self.tsAccountManager.setIsOnboarded(true, transaction: $0)
|
|
}
|
|
}
|
|
|
|
func showNextMilestone(navigationController: UINavigationController) {
|
|
guard let nextMilestone = nextMilestone else {
|
|
SignalApp.shared().showConversationSplitView()
|
|
markAsOnboarded()
|
|
return
|
|
}
|
|
|
|
let viewController = nextViewController(milestone: nextMilestone)
|
|
|
|
// *replace* the existing VC's. There's no going back once you've passed a milestone.
|
|
navigationController.setViewControllers([viewController], animated: true)
|
|
}
|
|
|
|
fileprivate func nextViewController(milestone: OnboardingMilestone) -> UIViewController {
|
|
Logger.info("milestone: \(milestone)")
|
|
switch milestone {
|
|
case .verifiedPhoneNumber, .verifiedLinkedDevice:
|
|
return OnboardingSplashViewController(onboardingController: self)
|
|
case .setupProfile:
|
|
return buildProfileViewController()
|
|
case .restorePin:
|
|
return Onboarding2FAViewController(onboardingController: self, isUsingKBS: true)
|
|
case .setupPin:
|
|
return buildPinSetupViewController()
|
|
}
|
|
}
|
|
|
|
// MARK: - Transitions
|
|
|
|
public func onboardingSplashDidComplete(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
let view = OnboardingPermissionsViewController(onboardingController: self)
|
|
viewController.navigationController?.pushViewController(view, animated: true)
|
|
}
|
|
|
|
public func onboardingSplashRequestedModeSwitch(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
let view = OnboardingModeSwitchConfirmationViewController(onboardingController: self)
|
|
viewController.navigationController?.pushViewController(view, animated: true)
|
|
}
|
|
|
|
public func toggleModeSwitch(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
let wasOverridden = isOnboardingModeOverriden
|
|
switch onboardingMode {
|
|
case .provisioning:
|
|
onboardingMode = .registering
|
|
case .registering:
|
|
onboardingMode = .provisioning
|
|
}
|
|
|
|
if wasOverridden {
|
|
onboardingMode = OnboardingController.defaultOnboardingMode
|
|
viewController.navigationController?.popToRootViewController(animated: true)
|
|
} else {
|
|
let view = OnboardingPermissionsViewController(onboardingController: self)
|
|
viewController.navigationController?.pushViewController(view, animated: true)
|
|
}
|
|
}
|
|
|
|
public func onboardingPermissionsWasSkipped(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
pushStartDeviceRegistrationView(onto: navigationController)
|
|
}
|
|
|
|
public func onboardingPermissionsDidComplete(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
pushStartDeviceRegistrationView(onto: navigationController)
|
|
}
|
|
|
|
func pushStartDeviceRegistrationView(onto navigationController: UINavigationController) {
|
|
AssertIsOnMainThread()
|
|
|
|
if onboardingMode == .provisioning {
|
|
let view = OnboardingTransferChoiceViewController(onboardingController: self)
|
|
navigationController.pushViewController(view, animated: true)
|
|
} else {
|
|
let view = OnboardingPhoneNumberViewController(onboardingController: self)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
}
|
|
|
|
public func requestingVerificationDidSucceed(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
let view = OnboardingVerificationViewController(onboardingController: self)
|
|
viewController.navigationController?.pushViewController(view, animated: true)
|
|
}
|
|
|
|
public func onboardingDidRequireCaptcha(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("Missing navigationController.")
|
|
return
|
|
}
|
|
|
|
// The service could demand CAPTCHA from the "phone number" view or later
|
|
// from the "code verification" view. The "Captcha" view should always appear
|
|
// immediately after the "phone number" view.
|
|
while navigationController.viewControllers.count > 1 &&
|
|
!(navigationController.topViewController is OnboardingPhoneNumberViewController) {
|
|
navigationController.popViewController(animated: false)
|
|
}
|
|
|
|
let view = OnboardingCaptchaViewController(onboardingController: self)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
|
|
@objc
|
|
public func verificationDidComplete(fromView view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
Logger.info("")
|
|
guard let navigationController = view.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
// At this point, the user has been prompted for contact access
|
|
// and has valid service credentials.
|
|
// We start the contact fetch/intersection now so that by the time
|
|
// they get to conversation list we can show meaningful contact in
|
|
// the suggested contact bubble.
|
|
contactsManager.fetchSystemContactsOnceIfAlreadyAuthorized()
|
|
|
|
if tsAccountManager.isReregistering {
|
|
showNextMilestone(navigationController: navigationController)
|
|
} else {
|
|
checkCanImportBackup(fromView: view)
|
|
}
|
|
}
|
|
|
|
public func linkingDidComplete(from viewController: UIViewController) {
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
showNextMilestone(navigationController: navigationController)
|
|
}
|
|
|
|
func buildProfileViewController() -> ProfileViewController {
|
|
return ProfileViewController(mode: .registration) { [weak self] profileVC in
|
|
guard let self = self else { return }
|
|
|
|
guard let navigationController = profileVC.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
self.showNextMilestone(navigationController: navigationController)
|
|
}
|
|
}
|
|
|
|
func buildPinSetupViewController() -> PinSetupViewController {
|
|
return PinSetupViewController.creating { [weak self] pinSetupVC, _ in
|
|
guard let self = self else { return }
|
|
|
|
guard let navigationController = pinSetupVC.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
self.showNextMilestone(navigationController: navigationController)
|
|
}
|
|
}
|
|
|
|
private func showBackupRestoreView(fromView view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = view.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
let restoreView = BackupRestoreViewController()
|
|
navigationController.setViewControllers([restoreView], animated: true)
|
|
}
|
|
|
|
private func checkCanImportBackup(fromView view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = view.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
backup.checkCanImport({ (canImport) in
|
|
Logger.info("canImport: \(canImport)")
|
|
|
|
if canImport {
|
|
self.backup.setHasPendingRestoreDecision(true)
|
|
|
|
self.showBackupRestoreView(fromView: view)
|
|
} else {
|
|
self.showNextMilestone(navigationController: navigationController)
|
|
}
|
|
}, failure: { (_) in
|
|
self.showBackupCheckFailedAlert(fromView: view)
|
|
})
|
|
}
|
|
|
|
private func showBackupCheckFailedAlert(fromView view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = view.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
let alert = ActionSheetController(title: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_TITLE",
|
|
comment: "Title for alert shown when the app failed to check for an existing backup."),
|
|
message: NSLocalizedString("CHECK_FOR_BACKUP_FAILED_MESSAGE",
|
|
comment: "Message for alert shown when the app failed to check for an existing backup."))
|
|
alert.addAction(ActionSheetAction(title: NSLocalizedString("REGISTER_FAILED_TRY_AGAIN", comment: ""),
|
|
style: .default) { (_) in
|
|
self.checkCanImportBackup(fromView: view)
|
|
})
|
|
alert.addAction(ActionSheetAction(title: NSLocalizedString("CHECK_FOR_BACKUP_DO_NOT_RESTORE", comment: "The label for the 'do not restore backup' button."),
|
|
style: .destructive) { (_) in
|
|
self.showNextMilestone(navigationController: navigationController)
|
|
})
|
|
view.presentActionSheet(alert)
|
|
}
|
|
|
|
public func onboardingDidRequire2FAPin(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
guard !(navigationController.topViewController is Onboarding2FAViewController) else {
|
|
// 2fa view is already presented, we don't need to push it again.
|
|
return
|
|
}
|
|
|
|
let view = Onboarding2FAViewController(onboardingController: self, isUsingKBS: kbsAuth != nil)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
|
|
@objc
|
|
public func profileWasSkipped(fromView view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
showConversationSplitView(view: view)
|
|
}
|
|
|
|
@objc
|
|
public func profileDidComplete(fromView view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
showConversationSplitView(view: view)
|
|
}
|
|
|
|
private func showConversationSplitView(view: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let navigationController = view.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
// In production, this view will never be presented in a modal.
|
|
// During testing (debug UI, etc.), it may be a modal.
|
|
let isModal = navigationController.presentingViewController != nil
|
|
if isModal {
|
|
view.dismiss(animated: true, completion: {
|
|
SignalApp.shared().showConversationSplitView()
|
|
})
|
|
} else {
|
|
SignalApp.shared().showConversationSplitView()
|
|
}
|
|
}
|
|
|
|
// MARK: - State
|
|
|
|
public private(set) var countryState: OnboardingCountryState = .defaultValue
|
|
|
|
public private(set) var phoneNumber: OnboardingPhoneNumber?
|
|
|
|
public private(set) var captchaToken: String?
|
|
|
|
public private(set) var verificationCode: String?
|
|
|
|
public private(set) var twoFAPin: String?
|
|
|
|
private var kbsAuth: RemoteAttestationAuth?
|
|
|
|
public private(set) var verificationRequestCount: UInt = 0
|
|
|
|
@objc
|
|
public func update(countryState: OnboardingCountryState) {
|
|
AssertIsOnMainThread()
|
|
|
|
self.countryState = countryState
|
|
}
|
|
|
|
@objc
|
|
public func update(phoneNumber: OnboardingPhoneNumber) {
|
|
AssertIsOnMainThread()
|
|
|
|
self.phoneNumber = phoneNumber
|
|
}
|
|
|
|
@objc
|
|
public func update(captchaToken: String) {
|
|
AssertIsOnMainThread()
|
|
|
|
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"
|
|
private static let kKeychainKey_LastRegisteredCountryCode = "kKeychainKey_LastRegisteredCountryCode"
|
|
private static let kKeychainKey_LastRegisteredPhoneNumber = "kKeychainKey_LastRegisteredPhoneNumber"
|
|
|
|
private class func debugValue(forKey key: String) -> String? {
|
|
AssertIsOnMainThread()
|
|
|
|
guard OWSIsDebugBuild() else {
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
let value = try CurrentAppContext().keychainStorage().string(forService: kKeychainService_LastRegistered, key: key)
|
|
return value
|
|
} catch {
|
|
// The value may not be present in the keychain.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private class func setDebugValue(_ value: String, forKey key: String) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard OWSIsDebugBuild() else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
try CurrentAppContext().keychainStorage().set(string: value, service: kKeychainService_LastRegistered, key: key)
|
|
} catch {
|
|
owsFailDebug("Error: \(error)")
|
|
}
|
|
}
|
|
|
|
public class func lastRegisteredCountryCode() -> String? {
|
|
return debugValue(forKey: kKeychainKey_LastRegisteredCountryCode)
|
|
}
|
|
|
|
private class func setLastRegisteredCountryCode(value: String) {
|
|
setDebugValue(value, forKey: kKeychainKey_LastRegisteredCountryCode)
|
|
}
|
|
|
|
public class func lastRegisteredPhoneNumber() -> String? {
|
|
return debugValue(forKey: kKeychainKey_LastRegisteredPhoneNumber)
|
|
}
|
|
|
|
private class func setLastRegisteredPhoneNumber(value: String) {
|
|
setDebugValue(value, forKey: kKeychainKey_LastRegisteredPhoneNumber)
|
|
}
|
|
|
|
// MARK: - Registration
|
|
|
|
public func requestVerification(fromViewController: UIViewController, isSMS: Bool) {
|
|
AssertIsOnMainThread()
|
|
|
|
guard let phoneNumber = phoneNumber else {
|
|
owsFailDebug("Missing phoneNumber.")
|
|
return
|
|
}
|
|
|
|
// We eagerly update this state, regardless of whether or not the
|
|
// registration request succeeds.
|
|
OnboardingController.setLastRegisteredCountryCode(value: countryState.countryCode)
|
|
OnboardingController.setLastRegisteredPhoneNumber(value: phoneNumber.userInput)
|
|
|
|
let captchaToken = self.captchaToken
|
|
self.verificationRequestCount += 1
|
|
ModalActivityIndicatorViewController.present(fromViewController: fromViewController,
|
|
canCancel: true) { modal in
|
|
firstly {
|
|
self.accountManager.requestAccountVerification(recipientId: phoneNumber.e164,
|
|
captchaToken: captchaToken,
|
|
isSMS: isSMS)
|
|
}.done {
|
|
modal.dismiss {
|
|
self.requestingVerificationDidSucceed(viewController: fromViewController)
|
|
}
|
|
}.catch { error in
|
|
Logger.error("Error: \(error)")
|
|
modal.dismiss {
|
|
self.requestingVerificationDidFail(viewController: fromViewController, error: error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func requestingVerificationDidFail(viewController: UIViewController, error: Error) {
|
|
if let statusCode = error.httpStatusCode {
|
|
switch statusCode {
|
|
case 400:
|
|
OWSActionSheets.showActionSheet(title: NSLocalizedString("REGISTRATION_ERROR", comment: ""),
|
|
message: NSLocalizedString("REGISTRATION_NON_VALID_NUMBER", comment: ""))
|
|
return
|
|
case 413:
|
|
OWSActionSheets.showActionSheet(title: nil,
|
|
message: NSLocalizedString("REGISTER_RATE_LIMITING_BODY", comment: "action sheet body"))
|
|
return
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if case AccountServiceClientError.captchaRequired = error {
|
|
return onboardingDidRequireCaptcha(viewController: viewController)
|
|
}
|
|
|
|
let nsError = error as NSError
|
|
owsFailDebug("unexpected error: \(nsError)")
|
|
OWSActionSheets.showActionSheet(title: nsError.localizedDescription,
|
|
message: nsError.localizedRecoverySuggestion)
|
|
}
|
|
|
|
// MARK: - Transfer
|
|
|
|
func transferAccount(fromViewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = fromViewController.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
guard !(navigationController.topViewController is OnboardingTransferQRCodeViewController) else {
|
|
// qr code view is already presented, we don't need to push it again.
|
|
return
|
|
}
|
|
|
|
let view = OnboardingTransferQRCodeViewController(onboardingController: self)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
|
|
func accountTransferInProgress(fromViewController: UIViewController, progress: Progress) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = fromViewController.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
guard !(navigationController.topViewController is OnboardingTransferProgressViewController) else {
|
|
// qr code view is already presented, we don't need to push it again.
|
|
return
|
|
}
|
|
|
|
let view = OnboardingTransferProgressViewController(onboardingController: self, progress: progress)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
|
|
public func presentTransferOptions(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
guard !(navigationController.topViewController is OnboardingTransferChoiceViewController) else {
|
|
// transfer view is already presented, we don't need to push it again.
|
|
return
|
|
}
|
|
|
|
let view = OnboardingTransferChoiceViewController(onboardingController: self)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
|
|
// MARK: - Verification
|
|
|
|
public enum VerificationOutcome: Equatable {
|
|
case success
|
|
case invalidVerificationCode
|
|
case invalid2FAPin
|
|
case invalidV2RegistrationLockPin(remainingAttempts: UInt32)
|
|
case exhaustedV2RegistrationLockAttempts
|
|
}
|
|
|
|
private var hasPendingRestoration: Bool {
|
|
databaseStorage.read { KeyBackupService.hasPendingRestoration(transaction: $0) }
|
|
}
|
|
|
|
public func submitVerification(fromViewController: UIViewController,
|
|
checkForAvailableTransfer: Bool = true,
|
|
completion : @escaping (VerificationOutcome) -> Void) {
|
|
AssertIsOnMainThread()
|
|
|
|
// If we have credentials for KBS auth or we're trying to verify
|
|
// after registering, we need to restore our keys from KBS.
|
|
if kbsAuth != nil || hasPendingRestoration {
|
|
guard let twoFAPin = twoFAPin else {
|
|
owsFailDebug("We expected a 2fa attempt, but we don't have a code to try")
|
|
return completion(.invalid2FAPin)
|
|
}
|
|
|
|
// Clear all cached values before doing restores during onboarding,
|
|
// they could be stale from previous registrations.
|
|
databaseStorage.write { KeyBackupService.clearKeys(transaction: $0) }
|
|
|
|
KeyBackupService.restoreKeys(with: twoFAPin, and: self.kbsAuth).done {
|
|
// If we restored successfully clear out KBS auth, the server will give it
|
|
// to us again if we still need to do KBS operations.
|
|
self.kbsAuth = nil
|
|
|
|
if self.hasPendingRestoration {
|
|
self.accountManager.performInitialStorageServiceRestore()
|
|
.ensure { completion(.success) }
|
|
|
|
} else {
|
|
// We've restored our keys, we can now re-run this method to post our registration token
|
|
// We need to first mark reglock as enabled so we know to include the reglock token in our
|
|
// registration attempt.
|
|
self.databaseStorage.write { transaction in
|
|
self.ows2FAManager.markRegistrationLockV2Enabled(transaction: transaction)
|
|
}
|
|
self.submitVerification(fromViewController: fromViewController, completion: completion)
|
|
}
|
|
}.catch { error in
|
|
guard let error = error as? KeyBackupService.KBSError else {
|
|
owsFailDebug("unexpected response from KBS")
|
|
return completion(.invalid2FAPin)
|
|
}
|
|
|
|
switch error {
|
|
case .assertion:
|
|
owsFailDebug("unexpected response from KBS")
|
|
completion(.invalid2FAPin)
|
|
case .invalidPin(let remainingAttempts):
|
|
completion(.invalidV2RegistrationLockPin(remainingAttempts: remainingAttempts))
|
|
case .backupMissing:
|
|
// We don't have a backup for this person, it probably
|
|
// was deleted due to too many failed attempts. They'll
|
|
// have to retry after the registration lock window expires.
|
|
completion(.exhaustedV2RegistrationLockAttempts)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
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: twoFAPin,
|
|
checkForAvailableTransfer: checkForAvailableTransfer
|
|
)
|
|
.then { _ -> Promise<Void> in
|
|
// Re-enable 2FA and RegLock with the registered pin, if any
|
|
if let pin = twoFAPin {
|
|
self.databaseStorage.write { transaction in
|
|
OWS2FAManager.shared().markEnabled(pin: pin, transaction: transaction)
|
|
}
|
|
|
|
if OWS2FAManager.shared().mode == .V2 {
|
|
return OWS2FAManager.shared().enableRegistrationLockV2()
|
|
}
|
|
}
|
|
|
|
return Promise.value(())
|
|
}.done { (_) in
|
|
DispatchQueue.main.async {
|
|
modal.dismiss(completion: {
|
|
self.verificationDidComplete(fromView: fromViewController)
|
|
})
|
|
}
|
|
}.catch({ (error) in
|
|
Logger.error("Error: \(error)")
|
|
|
|
DispatchQueue.main.async {
|
|
modal.dismiss(completion: {
|
|
self.verificationFailed(fromViewController: fromViewController,
|
|
error: error as NSError,
|
|
completion: completion)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func verificationFailed(fromViewController: UIViewController, error: NSError,
|
|
completion : @escaping (VerificationOutcome) -> Void) {
|
|
AssertIsOnMainThread()
|
|
|
|
if error.domain == OWSSignalServiceKitErrorDomain &&
|
|
error.code == OWSErrorCode.registrationMissing2FAPIN.rawValue {
|
|
|
|
Logger.info("Missing 2FA PIN.")
|
|
|
|
// If we were provided KBS auth, we'll need to re-register using reg lock v2,
|
|
// store this for that path.
|
|
kbsAuth = error.userInfo[TSRemoteAttestationAuthErrorKey] as? RemoteAttestationAuth
|
|
|
|
// Since we were told we need 2fa, clear out any stored KBS keys so we can
|
|
// do a fresh verification.
|
|
SDSDatabaseStorage.shared.write { transaction in
|
|
KeyBackupService.clearKeys(transaction: transaction)
|
|
self.ows2FAManager.markRegistrationLockV2Disabled(transaction: transaction)
|
|
}
|
|
|
|
completion(.invalid2FAPin)
|
|
|
|
onboardingDidRequire2FAPin(viewController: fromViewController)
|
|
} else if error.domain == OWSSignalServiceKitErrorDomain &&
|
|
error.code == OWSErrorCode.registrationTransferAvailable.rawValue {
|
|
Logger.info("Transfer available")
|
|
|
|
presentTransferOptions(viewController: fromViewController)
|
|
|
|
completion(.success)
|
|
} else {
|
|
if error.domain == OWSSignalServiceKitErrorDomain &&
|
|
error.code == OWSErrorCode.userError.rawValue {
|
|
completion(.invalidVerificationCode)
|
|
}
|
|
|
|
Logger.verbose("error: \(error.domain) \(error.code)")
|
|
OWSActionSheets.showActionSheet(title: NSLocalizedString("REGISTRATION_VERIFICATION_FAILED_TITLE", comment: "Alert view title"),
|
|
message: error.localizedDescription,
|
|
fromViewController: fromViewController)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public extension UIView {
|
|
func addBottomStroke() -> UIView {
|
|
return addBottomStroke(color: Theme.middleGrayColor, strokeWidth: CGHairlineWidth())
|
|
}
|
|
|
|
func addBottomStroke(color: UIColor, strokeWidth: CGFloat) -> UIView {
|
|
let strokeView = UIView()
|
|
strokeView.backgroundColor = color
|
|
addSubview(strokeView)
|
|
strokeView.autoSetDimension(.height, toSize: strokeWidth)
|
|
strokeView.autoPinWidthToSuperview()
|
|
strokeView.autoPinEdge(toSuperviewEdge: .bottom)
|
|
return strokeView
|
|
}
|
|
}
|