TM-SGNL-iOS/Signal/Registration/UserInterface/RegistrationEnterBackupKeyViewController.swift
TeleMessage developers dde0620daf initial commit
2025-05-03 12:28:28 -07:00

222 lines
8.3 KiB
Swift

//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SignalServiceKit
import SignalUI
import SwiftUI
protocol RegistrationEnterBackupKeyPresenter: AnyObject {
func next()
}
class RegistrationEnterBackupKeyViewController: OWSViewController, OWSNavigationChildController {
private weak var presenter: RegistrationEnterBackupKeyPresenter?
init(presenter: RegistrationEnterBackupKeyPresenter) {
self.presenter = presenter
super.init()
navigationItem.rightBarButtonItem = canSubmit ? nextBarButton : nil
self.view.backgroundColor = Theme.backgroundColor
self.view.addSubview(titleLabel)
self.view.addSubview(descriptionLabel)
self.view.addSubview(textView)
self.view.addSubview(noKeyButton)
NSLayoutConstraint.activate([
self.titleLabel.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor),
self.descriptionLabel.topAnchor.constraint(equalTo: self.titleLabel.bottomAnchor, constant: 12),
self.textView.topAnchor.constraint(equalTo: self.descriptionLabel.bottomAnchor, constant: 24),
self.noKeyButton.topAnchor.constraint(equalTo: self.textView.bottomAnchor, constant: 24),
self.titleLabel.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor, constant: 12),
self.titleLabel.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
self.descriptionLabel.leadingAnchor.constraint(equalTo: self.titleLabel.leadingAnchor),
self.descriptionLabel.trailingAnchor.constraint(equalTo: self.titleLabel.trailingAnchor),
self.textView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
self.noKeyButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor)
])
}
// MARK: OWSNavigationChildController
public var preferredNavigationBarStyle: OWSNavigationBarStyle { .solid }
public var navbarBackgroundColorOverride: UIColor? { .clear }
public var prefersNavigationBarHidden: Bool { true }
// MARK: UI
private lazy var textView: BackupCodeTextView = {
let textView = BackupCodeTextView()
textView.autocorrectionType = .no
return textView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.text = OWSLocalizedString(
"REGISTRATION_ENTER_BACKUP_KEY_TITLE",
comment: "Title for the screen that allows users to enter their backup key."
)
label.textAlignment = .center
label.font = .dynamicTypeTitle1.semibold()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private lazy var descriptionLabel: UILabel = {
let label = UILabel()
label.text = OWSLocalizedString(
"REGISTRATION_ENTER_BACKUP_KEY_DESCRIPTION",
comment: "Description for the screen that allows users to enter their backup key."
)
label.textColor = .secondaryLabel
label.textAlignment = .center
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private lazy var noKeyButton: UIButton = {
let button = UIButton()
button.setTitle(
OWSLocalizedString(
"REGISTRATION_NO_BACKUP_KEY_BUTTON_TITLE",
comment: "Title of button to tap if you do not have a backup key during registration."
),
for: .normal
)
button.titleLabel?.font = .dynamicTypeBody.bold()
button.setTitleColor(UIColor.Signal.ultramarine, for: .normal)
button.sizeToFit()
button.addTarget(self, action: #selector(didTapNoKeyButton), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
@objc
private func didTapNoKeyButton() {
// TODO [Reg UI]: IOS-5448.
}
private lazy var nextBarButton = UIBarButtonItem(
title: CommonStrings.nextButton,
style: .done,
target: self,
action: #selector(didTapNext)
)
@objc
private func didTapNext() {
guard canSubmit else { return }
textView.resignFirstResponder()
self.presenter?.next()
}
private var canSubmit: Bool {
let codeCharCount = textView.text.filter {
$0.isNumber || $0.isLetter
}.count
let expectedCodeCharCount = BackupCodeTextView.Constants.totalChunks * BackupCodeTextView.Constants.chunkSize
return codeCharCount == expectedCodeCharCount
}
}
private class BackupCodeTextView: UITextView, UITextViewDelegate {
fileprivate enum Constants {
static let chunkSize = 4
static let chunksPerRow = 4
static let spacesBetweenChunks = 4
static let totalChunks = 16
static let insets = UIEdgeInsets(top: 14, left: 20, bottom: 14, right: 20)
static let font = UIFont.monospacedSystemFont(ofSize: 17, weight: .regular)
static let lineSpacing = 10.0
static let backgroundColor = UIColor.ows_gray02
}
convenience init() {
self.init(frame: .zero, textContainer: nil)
}
override init(frame: CGRect, textContainer: NSTextContainer?) {
super.init(frame: frame, textContainer: textContainer)
self.backgroundColor = Constants.backgroundColor
self.textContainerInset = Constants.insets
self.textContainer.lineFragmentPadding = 0
self.delegate = self
self.isEditable = true
self.isSelectable = true
self.layer.cornerRadius = 10
// TODO [Reg UI]: Add placeholder text. IOS-5451.
// The font is taken care of by the attributed string, but setting
// this here makes the cursor the right size to start with.
let font = Constants.font
self.font = font
self.translatesAutoresizingMaskIntoConstraints = false
// Any character will do because font is monospaced.
let charSize = " ".size(withAttributes: [.font: Constants.font])
let totalChars = (Constants.chunksPerRow * Constants.chunkSize) + (Constants.spacesBetweenChunks * (Constants.chunksPerRow-1))
let horizontalEdgeInsetSpace = Constants.insets.left + Constants.insets.right
let desiredWidth = (charSize.width * CGFloat(totalChars)).rounded(.up) + horizontalEdgeInsetSpace
let heightPerChar = charSize.height
let rows = Constants.totalChunks / Constants.chunksPerRow
let heightFromRows = CGFloat(rows) * heightPerChar
let heightFromLineSpacing = Constants.lineSpacing * (Double(rows) - 1)
let height = (heightFromRows + Constants.insets.top + Constants.insets.bottom + heightFromLineSpacing).rounded(.up)
NSLayoutConstraint.activate([
self.widthAnchor.constraint(equalToConstant: desiredWidth),
self.heightAnchor.constraint(equalToConstant: height)
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: UITextViewDelegate
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let currentText = textView.text ?? ""
let updatedText = (currentText as NSString).replacingCharacters(in: range, with: text)
let filtered = updatedText.filter { $0.isNumber || $0.isLetter }
let prefixed = filtered.prefix(Constants.totalChunks * Constants.chunkSize)
let lowercased = prefixed.lowercased()
let formatted = String(lowercased.formattedWithSpaces(every: Constants.chunkSize))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = Constants.lineSpacing
let attributes: [NSAttributedString.Key: Any] = [
.paragraphStyle: paragraphStyle,
.font: Constants.font
]
let attributedStr = NSAttributedString(string: formatted, attributes: attributes)
textView.attributedText = attributedStr
return false
}
}
private extension String {
func formattedWithSpaces(every n: Int, separator: String = " ") -> String {
guard n > 0 else { return self }
return self.enumerated().map { $0.offset % n == 0 && $0.offset != 0 ? "\(separator)\($0.element)" : "\($0.element)" }.joined()
}
}