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
}
}