Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/AppSettings/Payments/PaymentsBiometryLockPromptViewController.swift
1 views
//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

public import SignalServiceKit
public import SignalUI

public protocol PaymentsBiometryLockPromptDelegate: AnyObject {
    func didEnablePaymentsLock()
    func didNotEnablePaymentsLock()
}

// MARK: -

private enum KnownDeviceOwnerAuthenticationType {
    case passcode
    case touchId
    case faceId
    case opticId

    static func from(_ deviceOwnerAuthenticationType: DeviceOwnerAuthenticationType) -> KnownDeviceOwnerAuthenticationType? {
        switch deviceOwnerAuthenticationType {
        case .unknown:
            return nil
        case .passcode:
            return .passcode
        case .faceId:
            return .faceId
        case .touchId:
            return .touchId
        case .opticId:
            return .opticId
        }
    }
}

// MARK: -

public class PaymentsBiometryLockPromptViewController: OWSViewController {

    private var hasBeenDoubleReminded: Bool = false

    private let knownDeviceOwnerAuthenticationType: KnownDeviceOwnerAuthenticationType

    private weak var delegate: PaymentsBiometryLockPromptDelegate?

    private let rootView = UIStackView()

    public init?(deviceOwnerAuthenticationType: DeviceOwnerAuthenticationType, delegate: PaymentsBiometryLockPromptDelegate?) {
        guard let knownDeviceOwnerAuthenticationType = KnownDeviceOwnerAuthenticationType.from(deviceOwnerAuthenticationType) else {
            return nil
        }
        self.knownDeviceOwnerAuthenticationType = knownDeviceOwnerAuthenticationType
        self.delegate = delegate

        super.init()
    }

    override public func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString(
            "SETTINGS_PAYMENTS_ENABLE_PAYMENTS_LOCK_PROMPT",
            comment: "Title for the 'enable payments lock' view of the payments activation flow.",
        )

        OWSTableViewController2.removeBackButtonText(viewController: self)

        rootView.axis = .vertical
        rootView.alignment = .fill
        view.addSubview(rootView)
        rootView.autoPin(toTopLayoutGuideOf: self, withInset: 0)
        rootView.autoPin(toBottomLayoutGuideOf: self, withInset: 0)
        rootView.autoPinWidthToSuperviewMargins()

        updateContents()
        updateNavbar()
    }

    override public func themeDidChange() {
        super.themeDidChange()

        self.applyTheme()
    }

    public func applyTheme() {
        updateContents()
    }

    private func updateNavbar() {
        navigationItem.leftBarButtonItem = UIBarButtonItem(
            image: Theme.iconImage(.buttonX),
            style: .plain,
            target: self,
            action: #selector(didTapClose),
            accessibilityIdentifier: "close",
        )
    }

    override public func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        updateContents()
        updateNavbar()
    }

    private func updateContents() {
        AssertIsOnMainThread()

        view.backgroundColor = OWSTableViewController2.tableBackgroundColor(isUsingPresentedStyle: true)

        let heroImage = UIImageView(image: UIImage(named: "payments-lock"))

        let titleLabel = UILabel()
        titleLabel.text = OWSLocalizedString(
            "SETTINGS_PAYMENTS_VIEW_PAYMENTS_LOCK_PROMPT_TITLE",
            comment: "Title for the content section of the  'payments lock prompt' view shown after payemts activation.",
        )
        titleLabel.font = UIFont.dynamicTypeTitle2Clamped.semibold()
        titleLabel.textColor = Theme.primaryTextColor
        titleLabel.textAlignment = .center

        let explanationLabel = UILabel()
        explanationLabel.text = localizedExplanationLabelText()
        explanationLabel.font = .dynamicTypeSubheadlineClamped
        explanationLabel.textColor = Theme.primaryTextColor
        explanationLabel.textAlignment = .center
        explanationLabel.numberOfLines = 0

        let topStack = UIStackView(arrangedSubviews: [
            heroImage,
            UIView.spacer(withHeight: 20),
            titleLabel,
            UIView.spacer(withHeight: 10),
            explanationLabel,
        ])
        topStack.axis = .vertical
        topStack.alignment = .center
        topStack.isLayoutMarginsRelativeArrangement = true
        topStack.layoutMargins = UIEdgeInsets(hMargin: 20, vMargin: 0)

        let enableButton = OWSFlatButton.insetButton(
            title: enableButtonTitle(),
            font: UIFont.dynamicTypeHeadline,
            titleColor: .white,
            backgroundColor: .ows_accentBlue,
            target: self,
            selector: #selector(didTapEnableButton),
        )
        enableButton.autoSetHeightUsingFont()

        let notNowButton = OWSFlatButton.insetButton(
            title: CommonStrings.notNowButton,
            font: UIFont.dynamicTypeHeadline,
            titleColor: .ows_accentBlue,
            backgroundColor: .clear,
            target: self,
            selector: #selector(didTapNotNowButton),
        )
        notNowButton.autoSetHeightUsingFont()

        let spacerFactory = SpacerFactory()

        rootView.removeAllSubviews()
        rootView.addArrangedSubviews([
            spacerFactory.buildVSpacer(),
            topStack,
            spacerFactory.buildVSpacer(),
            enableButton,
            UIView.spacer(withHeight: 16),
            notNowButton,
            UIView.spacer(withHeight: 8),
        ])

        spacerFactory.finalizeSpacers()
    }

    // MARK: - Events

    @objc
    private func didTapClose() {
        guard hasBeenDoubleReminded == false else {
            dismiss(animated: true, completion: nil)
            return
        }

        showDoubleReminder()
    }

    @objc
    private func didTapEnableButton() {
        SSKEnvironment.shared.databaseStorageRef.write { transaction in
            SSKEnvironment.shared.owsPaymentsLockRef.setIsPaymentsLockEnabled(true, transaction: transaction)
        }
        dismiss(animated: true, completion: nil)
    }

    @objc
    private func didTapNotNowButton() {
        AssertIsOnMainThread()

        guard hasBeenDoubleReminded == false else {
            dismiss(animated: true, completion: nil)
            return
        }

        showDoubleReminder()
    }

    func showDoubleReminder() {
        AssertIsOnMainThread()

        self.hasBeenDoubleReminded = true

        let actionSheet = ActionSheetController(
            title: doubleReminderActionSheetTitle(),
            message: OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_MESSAGE",
                comment: "Description for the 'double reminder' action sheet in the 'payments lock prompt' view in the payment settings.",
            ),
        )

        actionSheet.addAction(
            ActionSheetAction(
                title: CommonStrings.skipButton,
                style: .destructive,
            ) { [weak self] _ in
                Logger.debug("User is explicitly skipping the double reminder, so dismiss the 'payments lock prompt' view entirely.")
                self?.dismiss(animated: true, completion: nil)
            },
        )

        actionSheet.addAction(
            ActionSheetAction(
                title: CommonStrings.cancelButton,
                style: .cancel,
            ) { _ in
                Logger.debug("User cancelled the payments lock dismissal, dismiss the action sheet so user can reconsider payments lock decision")
            },
        )

        presentActionSheet(actionSheet)
    }

    private func localizedExplanationLabelText() -> String {
        switch knownDeviceOwnerAuthenticationType {
        case .faceId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_FACEID",
                comment: "Explanation of 'payments lock' with Face ID in the 'payments lock prompt' view shown after payments activation.",
            )
        case .touchId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_TOUCHID",
                comment: "Explanation of 'payments lock' with Touch ID in the 'payments lock prompt' view shown after payments activation.",
            )
        case .opticId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_OPTICID",
                comment: "Explanation of 'payments lock' with Optic ID in the 'payments lock prompt' view shown after payments activation.",
            )
        case .passcode:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_EXPLANATION_PASSCODE",
                comment: "Explanation of 'payments lock' with passcode in the 'payments lock prompt' view shown after payments activation.",
            )
        }
    }

    private func enableButtonTitle() -> String {
        switch knownDeviceOwnerAuthenticationType {
        case .faceId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_FACEID",
                comment: "Enable Button title in Payments Lock Prompt view for Face ID.",
            )
        case .touchId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_TOUCHID",
                comment: "Enable Button title in Payments Lock Prompt view for Touch ID.",
            )
        case .opticId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_OPTICID",
                comment: "Enable Button title in Payments Lock Prompt view for Optic ID.",
            )
        case .passcode:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_ENABLE_BUTTON_PASSCODE",
                comment: "Enable Button title in Payments Lock Prompt view for Passcode.",
            )
        }
    }

    private func doubleReminderActionSheetTitle() -> String {
        switch knownDeviceOwnerAuthenticationType {
        case .faceId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_FACEID",
                comment: "Double reminder action sheet title in Payments Lock Prompt view for Face ID.",
            )
        case .touchId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_TOUCHID",
                comment: "Double reminder action sheet title in Payments Lock Prompt view for Touch ID.",
            )
        case .opticId:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_OPTICID",
                comment: "Double reminder action sheet title in Payments Lock Prompt view for Optic ID.",
            )
        case .passcode:
            return OWSLocalizedString(
                "SETTINGS_PAYMENTS_PAYMENTS_LOCK_PROMPT_DOUBLE_REMINDER_TITLE_PASSCODE",
                comment: "Double reminder action sheet title in Payments Lock Prompt view for Passcode.",
            )
        }
    }
}