// // Copyright (c) 2022 Open Whisper Systems. All rights reserved. // import UIKit import SafariServices import SignalCoreKit import SignalServiceKit import SignalUI @objc(OWSPinSetupViewController) public class PinSetupViewController: OWSViewController { lazy private var titleLabel: UILabel = { let label = UILabel() label.textColor = Theme.primaryTextColor label.font = UIFont.ows_dynamicTypeTitle1Clamped.ows_semibold label.textAlignment = .center label.text = titleText return label }() lazy private var explanationLabel: LinkingTextView = { let explanationLabel = LinkingTextView() let explanationText: String switch mode { case .onboardingCreating, .creating: explanationText = NSLocalizedString("PIN_CREATION_EXPLANATION", comment: "The explanation in the 'pin creation' view.") case .changing: explanationText = NSLocalizedString("PIN_CREATION_RECREATION_EXPLANATION", comment: "The re-creation explanation in the 'pin creation' view.") case .confirming: explanationText = NSLocalizedString("PIN_CREATION_CONFIRMATION_EXPLANATION", comment: "The explanation of confirmation in the 'pin creation' view.") } // The font is too long to fit with dynamic type. Design is looking into // how to design this page to fit dynamic type. In the meantime, we have // to pin the font size. let explanationLabelFont = UIFont.systemFont(ofSize: 15) let attributedString = NSMutableAttributedString( string: explanationText, attributes: [ .font: explanationLabelFont, .foregroundColor: Theme.secondaryTextAndIconColor ] ) if !mode.isConfirming { explanationLabel.isUserInteractionEnabled = true attributedString.append(" ") attributedString.append( CommonStrings.learnMore, attributes: [ .link: URL(string: "https://support.signal.org/hc/articles/360007059792")!, .font: explanationLabelFont ] ) } explanationLabel.attributedText = attributedString explanationLabel.textAlignment = .center explanationLabel.accessibilityIdentifier = "pinCreation.explanationLabel" return explanationLabel }() private let topSpacer = UIView.vStretchingSpacer() private var proportionalSpacerConstraint: NSLayoutConstraint? private let pinTextField: UITextField = { let pinTextField = UITextField() pinTextField.textAlignment = .center pinTextField.textColor = Theme.primaryTextColor let font = UIFont.systemFont(ofSize: 17) pinTextField.font = font pinTextField.autoSetDimension(.height, toSize: font.lineHeight + 2 * 8.0) pinTextField.textContentType = .password pinTextField.isSecureTextEntry = true pinTextField.defaultTextAttributes.updateValue(5, forKey: .kern) pinTextField.keyboardAppearance = Theme.keyboardAppearance pinTextField.accessibilityIdentifier = "pinCreation.pinTextField" return pinTextField }() private lazy var pinTypeToggle: OWSFlatButton = { let pinTypeToggle = OWSFlatButton() pinTypeToggle.setTitle(font: .ows_dynamicTypeSubheadlineClamped, titleColor: Theme.accentBlueColor) pinTypeToggle.setBackgroundColors(upColor: .clear) pinTypeToggle.enableMultilineLabel() pinTypeToggle.button.clipsToBounds = true pinTypeToggle.button.layer.cornerRadius = 8 pinTypeToggle.contentEdgeInsets = UIEdgeInsets(hMargin: 4, vMargin: 8) pinTypeToggle.addTarget(target: self, selector: #selector(togglePinType)) pinTypeToggle.accessibilityIdentifier = "pinCreation.pinTypeToggle" return pinTypeToggle }() private lazy var nextButton: OWSFlatButton = { let nextButton = OWSFlatButton() nextButton.setTitle( title: CommonStrings.nextButton, font: UIFont.ows_dynamicTypeBodyClamped.ows_semibold, titleColor: .white) nextButton.setBackgroundColors(upColor: .ows_accentBlue) nextButton.button.clipsToBounds = true nextButton.button.layer.cornerRadius = 14 nextButton.contentEdgeInsets = UIEdgeInsets(hMargin: 4, vMargin: 14) nextButton.addTarget(target: self, selector: #selector(nextPressed)) nextButton.accessibilityIdentifier = "pinCreation.nextButton" return nextButton }() private let validationWarningLabel: UILabel = { let validationWarningLabel = UILabel() validationWarningLabel.textColor = .ows_accentRed validationWarningLabel.textAlignment = .center validationWarningLabel.font = UIFont.ows_dynamicTypeFootnoteClamped validationWarningLabel.numberOfLines = 0 validationWarningLabel.accessibilityIdentifier = "pinCreation.validationWarningLabel" return validationWarningLabel }() private let recommendationLabel: UILabel = { let recommendationLabel = UILabel() recommendationLabel.textColor = Theme.secondaryTextAndIconColor recommendationLabel.textAlignment = .center recommendationLabel.font = UIFont.ows_dynamicTypeFootnoteClamped recommendationLabel.numberOfLines = 0 recommendationLabel.accessibilityIdentifier = "pinCreation.recommendationLabel" return recommendationLabel }() private lazy var backButton: UIButton = { let topButtonImage = CurrentAppContext().isRTL ? #imageLiteral(resourceName: "NavBarBackRTL") : #imageLiteral(resourceName: "NavBarBack") let backButton = UIButton.withTemplateImage(topButtonImage, tintColor: Theme.secondaryTextAndIconColor) backButton.autoSetDimensions(to: CGSize(square: 40)) backButton.addTarget(self, action: #selector(navigateBack), for: .touchUpInside) return backButton }() private lazy var moreButton: UIButton = { let moreButton = UIButton.withTemplateImageName("more-horiz-24", tintColor: Theme.primaryIconColor) moreButton.autoSetDimensions(to: CGSize(square: 40)) moreButton.addTarget(self, action: #selector(didTapMoreButton), for: .touchUpInside) return moreButton }() private lazy var pinStrokeNormal = pinTextField.addBottomStroke() private lazy var pinStrokeError = pinTextField.addBottomStroke(color: .ows_accentRed, strokeWidth: 2) enum Mode: Equatable { case onboardingCreating case creating case changing case confirming(pinToMatch: String) var isChanging: Bool { guard case .changing = self else { return false } return true } var isConfirming: Bool { guard case .confirming = self else { return false } return true } } private let mode: Mode private let initialMode: Mode enum ValidationState { case valid case tooShort case mismatch case weak var isInvalid: Bool { return self != .valid } } private var validationState: ValidationState = .valid { didSet { updateValidationWarnings() } } private var pinType: KeyBackupService.PinType { didSet { updatePinType() } } // Called once pin setup has finished. Error will be nil upon success private let completionHandler: (PinSetupViewController, Error?) -> Void private let enableRegistrationLock: Bool init( mode: Mode, initialMode: Mode? = nil, pinType: KeyBackupService.PinType = .numeric, enableRegistrationLock: Bool = OWS2FAManager.shared.isRegistrationLockEnabled, completionHandler: @escaping (PinSetupViewController, Error?) -> Void ) { assert(TSAccountManager.shared.isRegisteredPrimaryDevice) self.mode = mode self.initialMode = initialMode ?? mode self.pinType = pinType self.enableRegistrationLock = enableRegistrationLock self.completionHandler = completionHandler super.init() if case .confirming = self.initialMode { owsFailDebug("pin setup flow should never start in the confirming state") } } @objc class func creatingRegistrationLock(completionHandler: @escaping (PinSetupViewController, Error?) -> Void) -> PinSetupViewController { return .init(mode: .creating, enableRegistrationLock: true, completionHandler: completionHandler) } @objc class func onboardingCreating(completionHandler: @escaping (PinSetupViewController, Error?) -> Void) -> PinSetupViewController { return .init(mode: .onboardingCreating, completionHandler: completionHandler) } @objc class func creating(completionHandler: @escaping (PinSetupViewController, Error?) -> Void) -> PinSetupViewController { return .init(mode: .creating, completionHandler: completionHandler) } @objc class func changing(completionHandler: @escaping (PinSetupViewController, Error?) -> Void) -> PinSetupViewController { return .init(mode: .changing, completionHandler: completionHandler) } override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) shouldIgnoreKeyboardChanges = false if let navigationBar = navigationController?.navigationBar as? OWSNavigationBar { navigationBar.navbarBackgroundColorOverride = backgroundColor navigationBar.switchToStyle(.solid, animated: true) } // Hide the nav bar when not changing. navigationController?.setNavigationBarHidden(!initialMode.isChanging, animated: false) title = titleText let topMargin: CGFloat = navigationController?.isNavigationBarHidden == false ? 0 : 32 let hMargin: CGFloat = UIDevice.current.isIPhone5OrShorter ? 13 : 26 view.layoutMargins = UIEdgeInsets(top: topMargin, leading: hMargin, bottom: 0, trailing: hMargin) if navigationController?.isNavigationBarHidden == false { [backButton, moreButton, titleLabel].forEach { $0.isHidden = true } } else { // If we're in onboarding mode, don't allow going back if case .onboardingCreating = mode { backButton.isHidden = true } else { backButton.isHidden = false } moreButton.isHidden = mode.isConfirming titleLabel.isHidden = false } } public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) // TODO: Maybe do this in will appear, to avoid the keyboard sliding in when the view is pushed? pinTextField.becomeFirstResponder() } private var backgroundColor: UIColor { presentingViewController == nil ? Theme.backgroundColor : Theme.tableView2PresentedBackgroundColor } override public func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) shouldIgnoreKeyboardChanges = true if let navigationBar = navigationController?.navigationBar as? OWSNavigationBar { navigationBar.switchToStyle(.default, animated: true) } navigationController?.setNavigationBarHidden(false, animated: false) } override public var preferredStatusBarStyle: UIStatusBarStyle { return Theme.isDarkThemeEnabled ? .lightContent : .default } override public func loadView() { owsAssertDebug(navigationController != nil, "This view should always be presented in a nav controller") view = UIView() view.backgroundColor = backgroundColor view.addSubview(backButton) backButton.autoPinEdge(toSuperviewSafeArea: .top) backButton.autoPinEdge(toSuperviewSafeArea: .leading) view.addSubview(moreButton) moreButton.autoPinEdge(toSuperviewSafeArea: .top) moreButton.autoPinEdge(toSuperviewSafeArea: .trailing) let titleSpacer = SpacerView(preferredHeight: 12) let pinFieldSpacer = SpacerView(preferredHeight: 11) let bottomSpacer = SpacerView(preferredHeight: 10) let pinToggleSpacer = SpacerView(preferredHeight: 24) let buttonSpacer = SpacerView(preferredHeight: 32) let stackView = UIStackView(arrangedSubviews: [ titleLabel, titleSpacer, explanationLabel, topSpacer, pinTextField, pinFieldSpacer, validationWarningLabel, recommendationLabel, bottomSpacer, pinTypeToggle, pinToggleSpacer, OnboardingBaseViewController.horizontallyWrap(primaryButton: nextButton), buttonSpacer ]) stackView.axis = .vertical stackView.alignment = .center view.addSubview(stackView) stackView.autoPinEdges(toSuperviewMarginsExcludingEdge: .bottom) autoPinView(toBottomOfViewControllerOrKeyboard: stackView, avoidNotch: true) [pinTextField, validationWarningLabel, recommendationLabel].forEach { $0.autoSetDimension(.width, toSize: 227) } [titleLabel, explanationLabel, pinTextField, validationWarningLabel, recommendationLabel, pinTypeToggle, nextButton] .forEach { $0.setCompressionResistanceVerticalHigh() } // Reduce priority of compression resistance for the spacer views // The array index serves as an ambiguous layout tiebreaker [titleSpacer, pinFieldSpacer, bottomSpacer, pinToggleSpacer, buttonSpacer].enumerated().forEach { $0.element.setContentCompressionResistancePriority(.defaultHigh - .init($0.offset), for: .vertical) } // Bottom spacer is the stack view item that grows when there's extra space // Ensure whitespace is balanced, so inputs are vertically centered. bottomSpacer.setContentHuggingPriority(.init(100), for: .vertical) proportionalSpacerConstraint = topSpacer.autoMatch(.height, to: .height, of: bottomSpacer) updateValidationWarnings() updatePinType() // Pin text field pinTextField.delegate = self } public override func viewDidLoad() { super.viewDidLoad() // Don't allow interactive dismissal. if #available(iOS 13, *) { isModalInPresentation = true } } var titleText: String { if mode.isConfirming { return NSLocalizedString("PIN_CREATION_CONFIRM_TITLE", comment: "Title of the 'pin creation' confirmation view.") } else if initialMode.isChanging { return NSLocalizedString("PIN_CREATION_CHANGING_TITLE", comment: "Title of the 'pin creation' recreation view.") } else { return NSLocalizedString("PIN_CREATION_TITLE", comment: "Title of the 'pin creation' view.") } } // MARK: - Events @objc func navigateBack() { Logger.info("") // If we're in creation mode AND we're the rootViewController, dismiss rather than pop if case .creating = mode, self.navigationController?.viewControllers.first == self { dismiss(animated: true, completion: nil) } else { navigationController?.popViewController(animated: true) } } @objc func didTapMoreButton(_ sender: UIButton) { let actionSheet = ActionSheetController() actionSheet.addAction(OWSActionSheets.cancelAction) proportionalSpacerConstraint?.isActive = false let pinnedHeightConstraint = topSpacer.autoSetDimension(.height, toSize: topSpacer.height) let learnMoreAction = ActionSheetAction( title: NSLocalizedString( "PIN_CREATION_LEARN_MORE", comment: "Learn more action on the pin creation view" ) ) { [weak self] _ in guard let self = self else { return } let vc = SFSafariViewController(url: URL(string: "https://support.signal.org/hc/articles/360007059792")!) self.present(vc, animated: true) { pinnedHeightConstraint.isActive = false self.proportionalSpacerConstraint?.isActive = true } } actionSheet.addAction(learnMoreAction) let skipAction = ActionSheetAction( title: NSLocalizedString( "PIN_CREATION_SKIP", comment: "Skip action on the pin creation view" ) ) { [weak self] _ in guard let self = self else { return } Self.disablePinWithConfirmation(fromViewController: self).done { [weak self] pinDisabled in guard pinDisabled, let self = self else { return } self.completionHandler(self, nil) pinnedHeightConstraint.isActive = false self.proportionalSpacerConstraint?.isActive = true }.catch { [weak self] error in guard let self = self else { return } OWSActionSheets.showActionSheet( title: NSLocalizedString("PIN_DISABLE_ERROR_TITLE", comment: "Error title indicating that the attempt to disable a PIN failed."), message: NSLocalizedString("PIN_DISABLE_ERROR_MESSAGE", comment: "Error body indicating that the attempt to disable a PIN failed.") ) { _ in self.completionHandler(self, error) pinnedHeightConstraint.isActive = false self.proportionalSpacerConstraint?.isActive = true } } } actionSheet.addAction(skipAction) presentActionSheet(actionSheet) } @objc func nextPressed() { Logger.info("") tryToContinue() } private func tryToContinue() { Logger.info("") guard let pin = pinTextField.text?.ows_stripped(), pin.count >= kMin2FAv2PinLength else { validationState = .tooShort return } if case .confirming(let pinToMatch) = mode, pinToMatch != pin { validationState = .mismatch return } if isWeakPin(pin) { validationState = .weak return } switch mode { case .onboardingCreating, .changing, .creating: let confirmingVC = PinSetupViewController( mode: .confirming(pinToMatch: pin), initialMode: initialMode, pinType: pinType, enableRegistrationLock: enableRegistrationLock, completionHandler: completionHandler ) navigationController?.pushViewController(confirmingVC, animated: true) case .confirming: enable2FAAndContinue(withPin: pin) } } private func isWeakPin(_ pin: String) -> Bool { let normalizedPin = KeyBackupService.normalizePin(pin) // We only check numeric pins for weakness guard normalizedPin.digitsOnly() == normalizedPin else { return false } var allTheSame = true var forwardSequential = true var reverseSequential = true var previousWholeNumberValue: Int? for character in normalizedPin { guard let current = character.wholeNumberValue else { owsFailDebug("numeric pin unexpectedly contatined non-numeric characters") break } defer { previousWholeNumberValue = current } guard let previous = previousWholeNumberValue else { continue } if previous != current { allTheSame = false } if previous + 1 != current { forwardSequential = false } if previous - 1 != current { reverseSequential = false } if !allTheSame && !forwardSequential && !reverseSequential { break } } return allTheSame || forwardSequential || reverseSequential } private func updateValidationWarnings() { AssertIsOnMainThread() pinStrokeNormal.isHidden = validationState.isInvalid pinStrokeError.isHidden = !validationState.isInvalid validationWarningLabel.isHidden = !validationState.isInvalid recommendationLabel.isHidden = validationState.isInvalid switch validationState { case .tooShort: switch pinType { case .numeric: validationWarningLabel.text = NSLocalizedString("PIN_CREATION_NUMERIC_HINT", comment: "Label indicating the user must use at least 4 digits") case .alphanumeric: validationWarningLabel.text = NSLocalizedString("PIN_CREATION_ALPHANUMERIC_HINT", comment: "Label indicating the user must use at least 4 characters") } case .mismatch: validationWarningLabel.text = NSLocalizedString("PIN_CREATION_MISMATCH_ERROR", comment: "Label indicating that the attempted PIN does not match the first PIN") case .weak: validationWarningLabel.text = NSLocalizedString("PIN_CREATION_WEAK_ERROR", comment: "Label indicating that the attempted PIN is too weak") default: break } } private func updatePinType() { AssertIsOnMainThread() pinTextField.text = nil validationState = .valid let recommendationLabelText: String switch pinType { case .numeric: pinTypeToggle.setTitle(title: NSLocalizedString("PIN_CREATION_CREATE_ALPHANUMERIC", comment: "Button asking if the user would like to create an alphanumeric PIN")) pinTextField.keyboardType = .asciiCapableNumberPad recommendationLabelText = NSLocalizedString("PIN_CREATION_NUMERIC_HINT", comment: "Label indicating the user must use at least 4 digits") case .alphanumeric: pinTypeToggle.setTitle(title: NSLocalizedString("PIN_CREATION_CREATE_NUMERIC", comment: "Button asking if the user would like to create an numeric PIN")) pinTextField.keyboardType = .default recommendationLabelText = NSLocalizedString("PIN_CREATION_ALPHANUMERIC_HINT", comment: "Label indicating the user must use at least 4 characters") } pinTextField.reloadInputViews() if mode.isConfirming { pinTypeToggle.isHidden = true recommendationLabel.text = NSLocalizedString("PIN_CREATION_PIN_CONFIRMATION_HINT", comment: "Label indication the user must confirm their PIN.") } else { pinTypeToggle.isHidden = false recommendationLabel.text = recommendationLabelText } } @objc func togglePinType() { switch pinType { case .numeric: pinType = .alphanumeric case .alphanumeric: pinType = .numeric } } private func enable2FAAndContinue(withPin pin: String) { Logger.debug("") pinTextField.resignFirstResponder() let progressView = AnimatedProgressView( loadingText: NSLocalizedString("PIN_CREATION_PIN_PROGRESS", comment: "Indicates the work we are doing while creating the user's pin") ) view.addSubview(progressView) progressView.autoPinWidthToSuperview() progressView.autoVCenterInSuperview() progressView.startAnimating { self.view.isUserInteractionEnabled = false self.nextButton.alpha = 0.5 self.pinTextField.alpha = 0 self.validationWarningLabel.alpha = 0 self.recommendationLabel.alpha = 0 } enum PinSetupError: Error { case networkFailure case enable2FA case enableRegistrationLock } firstly { () -> Promise in OWS2FAManager.shared.requestEnable2FA(withPin: pin, mode: .V2) }.recover { error in // If we have a network failure before even requesting to enable 2FA, we // can just ask the user to retry without altering any state. We can be // confident nothing has changed on the server. if case OWSHTTPError.networkFailure = error { // We only want to stop for network errors if we're past onboarding. // During onboarding, we want to let the user continue even when // a network issue is encountered during PIN creation. if self.initialMode != .onboardingCreating { throw PinSetupError.networkFailure } } owsFailDebug("Failed to enable 2FA with error: \(error)") // The client may have fallen out of sync with the service. // Try to get back to a known good state by disabling 2FA // whenever enabling it fails. OWS2FAManager.shared.disable2FA(success: nil, failure: nil) throw PinSetupError.enable2FA }.then { () -> Promise in if self.enableRegistrationLock { return OWS2FAManager.shared.enableRegistrationLockV2() } else { return Promise.value(()) } }.recover { error -> Promise in if error is PinSetupError { throw error } owsFailDebug("Failed to enable registration lock with error: \(error)") // If the registration lock wasn't already enabled, we have to notify // the user of the failure and not attempt to enable it later. Otherwise, // they would be left thinking they have registration lock enabled when // they do not for some window of time. guard OWS2FAManager.shared.isRegistrationLockV2Enabled else { throw PinSetupError.enableRegistrationLock } // Otherwise, attempt to update our account attributes. This may also fail, // but if it does it will flag our attributes as dirty so we upload them // ASAP (when we have the ability to talk to the service) and get the user // switched over to their new PIN for registration lock. TSAccountManager.shared.updateAccountAttributes().cauterize() return Promise.value(()) }.done { AssertIsOnMainThread() // The completion handler always dismisses this view, so we don't want to animate anything. progressView.stopAnimatingImmediately() self.completionHandler(self, nil) // Clear the experience upgrade if it was pending. SDSDatabaseStorage.shared.asyncWrite { transaction in ExperienceUpgradeManager.clearExperienceUpgrade(.introducingPins, transaction: transaction.unwrapGrdbWrite) } }.catch { error in AssertIsOnMainThread() progressView.stopAnimating(success: false) { self.nextButton.alpha = 1 self.pinTextField.alpha = 1 self.validationWarningLabel.alpha = 1 self.recommendationLabel.alpha = 1 } completion: { self.view.isUserInteractionEnabled = true progressView.removeFromSuperview() guard let error = error as? PinSetupError else { return owsFailDebug("Unexpected error during PIN setup \(error)") } // Show special reglock themed error message. switch error { case .enableRegistrationLock: OWSActionSheets.showActionSheet( title: NSLocalizedString( "PIN_CREATION_REGLOCK_ERROR_TITLE", comment: "Error title indicating that the attempt to create a PIN succeeded but enabling reglock failed."), message: NSLocalizedString( "PIN_CREATION_REGLOCK_ERROR_MESSAGE", comment: "Error body indicating that the attempt to create a PIN succeeded but enabling reglock failed.") ) { _ in self.completionHandler(self, error) } case .networkFailure: OWSActionSheets.showActionSheet( title: NSLocalizedString( "PIN_CREATION_NO_NETWORK_ERROR_TITLE", comment: "Error title indicating that the attempt to create a PIN failed due to network issues."), message: NSLocalizedString( "PIN_CREATION_NO_NETWORK_ERROR_MESSAGE", comment: "Error body indicating that the attempt to create a PIN failed due to network issues.") ) case .enable2FA: switch self.initialMode { case .onboardingCreating: OWSActionSheets.showActionSheet( title: NSLocalizedString( "PIN_CREATION_ERROR_TITLE", comment: "Error title indicating that the attempt to create a PIN failed."), message: NSLocalizedString( "PIN_CREATION_ERROR_MESSAGE", comment: "Error body indicating that the attempt to create a PIN failed.") ) { _ in self.completionHandler(self, error) } case .changing: OWSActionSheets.showActionSheet( title: NSLocalizedString( "PIN_CHANGE_ERROR_TITLE", comment: "Error title indicating that the attempt to change a PIN failed."), message: NSLocalizedString( "PIN_CHANGE_ERROR_MESSAGE", comment: "Error body indicating that the attempt to change a PIN failed.") ) { _ in self.completionHandler(self, error) } case .creating: OWSActionSheets.showActionSheet( title: NSLocalizedString( "PIN_RECREATION_ERROR_TITLE", comment: "Error title indicating that the attempt to recreate a PIN failed."), message: NSLocalizedString( "PIN_RECRETION_ERROR_MESSAGE", comment: "Error body indicating that the attempt to recreate a PIN failed.") ) { _ in self.completionHandler(self, error) } case .confirming: owsFailDebug("Unexpected initial mode") } } } } } } // MARK: - extension PinSetupViewController: UITextFieldDelegate { public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let hasPendingChanges: Bool if pinType == .numeric { ViewControllerUtils.ows2FAPINTextField(textField, shouldChangeCharactersIn: range, replacementString: string) hasPendingChanges = false } else { hasPendingChanges = true } // Reset the validation state to clear errors, since the user is trying again validationState = .valid // Inform our caller whether we took care of performing the change. return hasPendingChanges } } extension PinSetupViewController { public class func disablePinWithConfirmation(fromViewController: UIViewController) -> Promise { guard !OWS2FAManager.shared.isRegistrationLockV2Enabled else { return showRegistrationLockConfirmation(fromViewController: fromViewController) } let (promise, future) = Promise.pending() let actionSheet = ActionSheetController( title: NSLocalizedString("PIN_CREATION_DISABLE_CONFIRMATION_TITLE", comment: "Title of the 'pin disable' action sheet."), message: NSLocalizedString("PIN_CREATION_DISABLE_CONFIRMATION_MESSAGE", comment: "Message of the 'pin disable' action sheet.") ) let cancelAction = ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel) { _ in future.resolve(false) } actionSheet.addAction(cancelAction) let disableAction = ActionSheetAction( title: NSLocalizedString("PIN_CREATION_DISABLE_CONFIRMATION_ACTION", comment: "Action of the 'pin disable' action sheet."), style: .destructive ) { _ in ModalActivityIndicatorViewController.present( fromViewController: fromViewController, canCancel: false ) { modal in SDSDatabaseStorage.shared.asyncWrite { transaction in KeyBackupService.useDeviceLocalMasterKey(transaction: transaction) transaction.addAsyncCompletionOnMain { modal.dismiss { future.resolve(true) } } } } } actionSheet.addAction(disableAction) fromViewController.presentActionSheet(actionSheet) return promise } private class func showRegistrationLockConfirmation(fromViewController: UIViewController) -> Promise { let (promise, future) = Promise.pending() let actionSheet = ActionSheetController( title: NSLocalizedString("PIN_CREATION_REGLOCK_CONFIRMATION_TITLE", comment: "Title of the 'pin disable' reglock action sheet."), message: NSLocalizedString("PIN_CREATION_REGLOCK_CONFIRMATION_MESSAGE", comment: "Message of the 'pin disable' reglock action sheet.") ) let cancelAction = ActionSheetAction(title: CommonStrings.cancelButton, style: .cancel) { _ in future.resolve(false) } actionSheet.addAction(cancelAction) let disableAction = ActionSheetAction( title: NSLocalizedString("PIN_CREATION_REGLOCK_CONFIRMATION_ACTION", comment: "Action of the 'pin disable' reglock action sheet."), style: .destructive ) { _ in ModalActivityIndicatorViewController.present( fromViewController: fromViewController, canCancel: false ) { modal in OWS2FAManager.shared.disableRegistrationLockV2().then { Guarantee { resolve in modal.dismiss { resolve(()) } } }.then { () -> Promise in disablePinWithConfirmation(fromViewController: fromViewController) }.done { success in future.resolve(success) }.catch { error in modal.dismiss { future.reject(error) } } } } actionSheet.addAction(disableAction) fromViewController.presentActionSheet(actionSheet) return promise } }