Signal-iOS/Signal/src/ViewControllers/Registration/OnboardingController.swift
2021-10-22 19:06:15 -03:00

916 lines
33 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import UIKit
@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.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)
}
}
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
let superOrientations = super.supportedInterfaceOrientations
let onboardingOrientations: UIInterfaceOrientationMask = UIDevice.current.isIPad ? .all : .portrait
return superOrientations.intersection(onboardingOrientations)
}
}
@objc
public class OnboardingController: NSObject {
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 phoneNumberDiscoverability
case setupProfile
case setupPin
}
var requiredMilestones: [OnboardingMilestone] {
switch onboardingMode {
case .provisioning:
return [.verifiedLinkedDevice]
case .registering:
var milestones: [OnboardingMilestone] = [.verifiedPhoneNumber, .phoneNumberDiscoverability, .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 !FeatureFlags.phoneNumberDiscoverability || tsAccountManager.hasDefinedIsDiscoverableByPhoneNumber() {
milestones.append(.phoneNumberDiscoverability)
}
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:
if SSKPreferences.didDropYdb() {
return OnboardingDroppedYdbViewController(onboardingController: self)
} else {
return OnboardingSplashViewController(onboardingController: self)
}
case .setupProfile:
return OnboardingProfileCreationViewController(onboardingController: self)
case .restorePin:
return Onboarding2FAViewController(onboardingController: self, isUsingKBS: true)
case .setupPin:
return buildPinSetupViewController()
case .phoneNumberDiscoverability:
return OnboardingPhoneNumberDiscoverabilityViewController(onboardingController: self)
}
}
// 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("")
// TODO: Once notification work is complete, uncomment this.
// Self.notificationPresenter.cancelIncompleteRegistrationNotification()
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.
contactsManagerImpl.fetchSystemContactsOnceIfAlreadyAuthorized()
showNextMilestone(navigationController: navigationController)
}
public func linkingDidComplete(from viewController: UIViewController) {
guard let navigationController = viewController.navigationController else {
owsFailDebug("navigationController was unexpectedly nil")
return
}
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)
}
}
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 presentPhoneNumberConfirmationSheet(from vc: UIViewController, number: String, completion: @escaping (_ didApprove: Bool) -> Void) {
let titleFormat = NSLocalizedString(
"REGISTRATION_VIEW_PHONE_NUMBER_CONFIRMATION_ALERT_TITLE_FORMAT",
comment: "Title for confirmation alert during phone number registration. Embeds {{phone number}}.")
let message = NSLocalizedString(
"REGISTRATION_VIEW_PHONE_NUMBER_CONFIRMATION_ALERT_MESSAGE",
comment: "Message for confirmation alert during phone number registration.")
let editButtonTitle = NSLocalizedString(
"REGISTRATION_VIEW_PHONE_NUMBER_CONFIRMATION_EDIT_BUTTON",
comment: "A button allowing user to cancel registration and edit a phone number")
let sheet = ActionSheetController(title: String(format: titleFormat, number), message: message)
sheet.addAction(ActionSheetAction(title: CommonStrings.yesButton, style: .default, handler: { _ in
completion(true)
}))
sheet.addAction(ActionSheetAction(title: editButtonTitle, style: .default, handler: { _ in
completion(false)
}))
vc.present(sheet, animated: true, completion: nil)
}
public func requestVerification(
fromViewController: UIViewController,
isSMS: Bool,
completion: ((_ willTransition: Bool, _ error: Error?) -> Void)?) {
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
firstly { () -> Promise<Void> in
return self.accountManager.requestAccountVerification(recipientId: phoneNumber.e164,
captchaToken: captchaToken,
isSMS: isSMS)
}.done {
completion?(true, nil)
self.requestingVerificationDidSucceed(viewController: fromViewController)
}.catch { error in
Logger.error("Error: \(error)")
switch error {
case let error where error.httpStatusCode == 400:
completion?(false, error)
OWSActionSheets.showActionSheet(
title: NSLocalizedString("REGISTRATION_ERROR", comment: ""),
message: NSLocalizedString("REGISTRATION_NON_VALID_NUMBER", comment: ""))
case let error where error.httpStatusCode == 413:
completion?(false, error)
OWSActionSheets.showActionSheet(
title: nil,
message: NSLocalizedString("REGISTER_RATE_LIMITING_BODY", comment: "action sheet body"))
case let error where error.isNetworkFailureOrTimeout:
completion?(false, error)
OWSActionSheets.showActionSheet(
title: NSLocalizedString("REGISTRATION_ERROR_NETWORK_FAILURE_ALERT_TITLE",
comment: "Alert title for network failure during registration"),
message: NSLocalizedString("REGISTRATION_ERROR_NETWORK_FAILURE_ALERT_BODY",
comment: "Alert body for network failure during registration"))
case AccountServiceClientError.captchaRequired:
completion?(true, error)
self.onboardingDidRequireCaptcha(viewController: fromViewController)
default:
owsFailDebug("unexpected error: \(error)")
completion?(false, error)
OWSActionSheets.showActionSheet(title: error.userErrorDescription,
message: (error as 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,
showModal: 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 {
firstly {
self.accountManager.performInitialStorageServiceRestore()
}.recover { error in
Logger.warn("Timed out performing storage service restore: \(error). Ignoring...")
}.done {
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):
Logger.warn("Invalid V2 PIN, \(remainingAttempts) attempt(s) remaining")
completion(.invalidV2RegistrationLockPin(remainingAttempts: remainingAttempts))
case .backupMissing:
Logger.error("Invalid V2 PIN, attempts exhausted")
// 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
let promise = firstly {
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(())
}
if showModal {
ModalActivityIndicatorViewController.present(fromViewController: fromViewController, canCancel: true) { modal in
promise.done {
modal.dismiss {
self.verificationDidComplete(fromView: fromViewController)
}
}.catch { error in
modal.dismiss(completion: {
Logger.warn("Error: \(error)")
self.verificationFailed(
fromViewController: fromViewController,
error: error as NSError,
completion: completion)
})
}
}
} else {
promise.done {
self.verificationDidComplete(fromView: fromViewController)
}.catch { error in
Logger.warn("Error: \(error)")
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.userErrorDescription,
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
}
}