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

public import SignalServiceKit
public import SignalUI

public class PaymentsRestoreWalletWordViewController: OWSViewController {

    private weak var restoreWalletDelegate: PaymentsRestoreWalletDelegate?

    private let partialPassphrase: PartialPaymentsPassphrase

    private let wordIndex: Int

    private let textfield = UITextField()

    private var wordText: String? {
        textfield.text?.strippedOrNil?.lowercased()
    }

    private var hasValidWord: Bool {
        isValidWord(wordText)
    }

    private func isValidWord(_ wordText: String?) -> Bool {
        guard
            let wordText,
            !wordText.isEmpty
        else {
            return false
        }
        return SUIEnvironment.shared.paymentsSwiftRef.isValidPassphraseWord(wordText)
    }

    private let warningLabel = UILabel()

    public init(
        restoreWalletDelegate: PaymentsRestoreWalletDelegate,
        partialPassphrase: PartialPaymentsPassphrase,
        wordIndex: Int,
    ) {
        self.restoreWalletDelegate = restoreWalletDelegate
        self.partialPassphrase = partialPassphrase
        self.wordIndex = wordIndex

        super.init()

        textfield.text = partialPassphrase.getWord(atIndex: wordIndex)
    }

    public func clearInput() {
        partialPassphrase.reset()
        textfield.text = nil
    }

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

        title = OWSLocalizedString(
            "SETTINGS_PAYMENTS_RESTORE_WALLET_TITLE",
            comment: "Title for the 'restore payments wallet' view of the app settings.",
        )

        OWSTableViewController2.removeBackButtonText(viewController: self)

        rootView.axis = .vertical
        rootView.alignment = .fill
        rootView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(rootView)
        NSLayoutConstraint.activate([
            rootView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
            rootView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
            rootView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
            rootView.bottomAnchor.constraint(equalTo: keyboardLayoutGuide.topAnchor),
        ])

        updateContents()
    }

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

        textfield.becomeFirstResponder()
    }

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

        textfield.becomeFirstResponder()
    }

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

        updateContents()
    }

    private let rootView = UIStackView()

    private func updateContents() {

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

        let titleLabel = UILabel()
        titleLabel.text = OWSLocalizedString(
            "SETTINGS_PAYMENTS_RESTORE_WALLET_WORD_TITLE",
            comment: "Title for the 'enter word' step of the 'restore payments wallet' views.",
        )
        titleLabel.font = UIFont.dynamicTypeTitle2Clamped.semibold()
        titleLabel.textColor = Theme.primaryTextColor
        titleLabel.textAlignment = .center

        let instructionsFormat = OWSLocalizedString(
            "SETTINGS_PAYMENTS_RESTORE_WALLET_WORD_INSTRUCTIONS_FORMAT",
            comment: "Format for the instructions for the 'enter word' step of the 'restore payments wallet' views. Embeds {{ the index of the current word }}.",
        )
        let instructions = String.nonPluralLocalizedStringWithFormat(instructionsFormat, OWSFormat.formatInt(wordIndex + 1))

        let instructionsLabel = UILabel()
        instructionsLabel.text = instructions
        instructionsLabel.textAlignment = .center
        instructionsLabel.numberOfLines = 0
        instructionsLabel.lineBreakMode = .byWordWrapping

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

        textfield.textColor = Theme.primaryTextColor
        textfield.font = .dynamicTypeBodyClamped
        textfield.keyboardAppearance = Theme.keyboardAppearance
        textfield.autocapitalizationType = .none
        textfield.autocorrectionType = .no
        textfield.spellCheckingType = .no
        textfield.smartQuotesType = .no
        textfield.smartDashesType = .no
        textfield.returnKeyType = .done
        textfield.accessibilityIdentifier = "payments.passphrase.restore.\(wordIndex)"
        textfield.addTarget(self, action: #selector(textfieldDidChange), for: .editingChanged)
        textfield.delegate = self

        let placeholderFormat = OWSLocalizedString(
            "SETTINGS_PAYMENTS_VIEW_PASSPHRASE_CONFIRM_PLACEHOLDER_FORMAT",
            comment: "Format for the placeholder text in the 'confirm payments passphrase' view of the app settings. Embeds: {{ the index of the word }}.",
        )
        let placeholder = NSAttributedString(
            string: String.nonPluralLocalizedStringWithFormat(
                placeholderFormat,
                OWSFormat.formatInt(wordIndex + 1),
            ),
            attributes: [
                .foregroundColor: Theme.secondaryTextAndIconColor,
            ],
        )
        textfield.attributedPlaceholder = placeholder

        let textfieldStack = UIStackView(arrangedSubviews: [textfield])
        textfieldStack.axis = .vertical
        textfieldStack.alignment = .fill
        textfieldStack.isLayoutMarginsRelativeArrangement = true
        textfieldStack.layoutMargins = UIEdgeInsets(
            hMargin: OWSTableViewController2.cellHInnerMargin,
            vMargin: OWSTableViewController2.cellVInnerMargin,
        )
        let backgroundColor = OWSTableViewController2.cellBackgroundColor(isUsingPresentedStyle: true)
        textfieldStack.addBackgroundView(
            withBackgroundColor: backgroundColor,
            cornerRadius: 10,
        )

        warningLabel.text = " "
        warningLabel.font = .dynamicTypeCaption1
        warningLabel.textColor = .ows_accentRed

        let warningStack = UIStackView(arrangedSubviews: [warningLabel])
        warningStack.axis = .vertical
        warningStack.alignment = .fill
        warningStack.isLayoutMarginsRelativeArrangement = true
        warningStack.layoutMargins = UIEdgeInsets(
            hMargin: OWSTableViewController2.cellHInnerMargin,
            vMargin: 0,
        )

        let nextButton = OWSFlatButton.button(
            title: CommonStrings.nextButton,
            font: UIFont.dynamicTypeHeadline,
            titleColor: .white,
            backgroundColor: .ows_accentBlue,
            target: self,
            selector: #selector(didTapNextButton),
        )
        nextButton.autoSetHeightUsingFont()

        rootView.removeAllSubviews()
        rootView.addArrangedSubviews([
            UIView.spacer(withHeight: 20),
            topStack,
            UIView.spacer(withHeight: 60),
            textfieldStack,
            UIView.spacer(withHeight: 8),
            warningStack,
            UIView.vStretchingSpacer(),
            nextButton,
            UIView.spacer(withHeight: 8),
        ])
    }

    // MARK: - Events

    @objc
    private func didTapNextButton() {
        guard let restoreWalletDelegate else {
            owsFailDebug("Missing restoreWalletDelegate.")
            dismiss(animated: true, completion: nil)
            return
        }
        guard
            let wordText = self.wordText,
            isValidWord(wordText)
        else {
            warningLabel.text = OWSLocalizedString(
                "SETTINGS_PAYMENTS_RESTORE_WALLET_WORD_INVALID_WORD",
                comment: "Error indicating that the user has entered an invalid word in the 'enter word' step of the 'restore payments wallet' views.",
            )
            return
        }
        partialPassphrase.set(word: wordText, index: wordIndex)
        if partialPassphrase.isComplete {
            guard let paymentsPassphrase = partialPassphrase.asPaymentsPassphrase() else {
                OWSActionSheets.showErrorAlert(message: OWSLocalizedString(
                    "SETTINGS_PAYMENTS_RESTORE_WALLET_WORD_INVALID_PASSPHRASE",
                    comment: "Error indicating that the user has entered an invalid payments passphrase in the 'restore payments wallet' views.",
                ))
                return
            }
            let view = PaymentsRestoreWalletCompleteViewController(
                restoreWalletDelegate: restoreWalletDelegate,
                passphrase: paymentsPassphrase,
            )
            navigationController?.pushViewController(view, animated: true)
        } else {
            let view = PaymentsRestoreWalletWordViewController(
                restoreWalletDelegate: restoreWalletDelegate,
                partialPassphrase: partialPassphrase,
                wordIndex: wordIndex + 1,
            )
            navigationController?.pushViewController(view, animated: true)
        }
    }

    @objc
    private func textfieldDidChange() {
        // Clear any warning.
        warningLabel.text = " "
    }
}

// MARK: -

public class PartialPaymentsPassphrase {

    private var wordMap = [Int: String]()

    public var isComplete: Bool {
        wordMap.count == PaymentsConstants.passphraseWordCount
    }

    public init() {}

    public static var empty: PartialPaymentsPassphrase { PartialPaymentsPassphrase() }

    public func set(word: String, index: Int) {
        let word = word.stripped
        guard !word.isEmpty else {
            owsFailDebug("Invalid word.")
            return
        }
        wordMap[index] = word
    }

    public func getWord(atIndex index: Int) -> String? {
        wordMap[index]
    }

    public func asPaymentsPassphrase() -> PaymentsPassphrase? {
        var words = [String]()

        for index in 0..<PaymentsConstants.passphraseWordCount {
            guard let word = wordMap[index] else {
                owsFailDebug("Missing word: \(index)")
                return nil
            }
            words.append(word)
        }

        do {
            return try PaymentsPassphrase(words: words)
        } catch {
            owsFailDebug("Error: \(error)")
            return nil
        }
    }

    public func reset() {
        wordMap.removeAll()
    }
}

// MARK: -

extension PaymentsRestoreWalletWordViewController: UITextFieldDelegate {
    public func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        didTapNextButton()
        return false
    }
}