Signal-iOS/Signal/Provisioning/UserInterface/ProvisioningQRCodeViewController.swift
2024-12-10 14:18:09 -08:00

281 lines
10 KiB
Swift

//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import SafariServices
import SignalServiceKit
import SignalUI
class ProvisioningQRCodeViewController: ProvisioningBaseViewController {
private enum QRCodeDisplayMode {
case qrCodeImage(UIImage?)
case refreshButton
case loadingSpinner
}
private var qrCodeImageBackgroundView: UIView!
private var qrCodeImageView: UIImageView!
private var qrCodeRefreshButton: OWSButton!
private var qrCodeLoadingSpinner: UIActivityIndicatorView!
private var rotateQRCodeTask: Task<Void, Never>?
private func setDisplayMode(_ displayMode: QRCodeDisplayMode) {
switch displayMode {
case .qrCodeImage(let image):
qrCodeLoadingSpinner.isHidden = true
qrCodeImageBackgroundView.isHidden = false
qrCodeRefreshButton.isHidden = true
qrCodeImageView.setTemplateImage(image, tintColor: .qrCodeBlue)
case .refreshButton:
qrCodeLoadingSpinner.isHidden = true
qrCodeImageBackgroundView.isHidden = true
qrCodeRefreshButton.isHidden = false
case .loadingSpinner:
qrCodeLoadingSpinner.isHidden = false
qrCodeImageBackgroundView.isHidden = true
qrCodeRefreshButton.isHidden = true
}
}
override func loadView() {
// MARK: Views
view = UIView()
view.addSubview(primaryView)
primaryView.autoPinEdgesToSuperviewEdges()
view.backgroundColor = Theme.backgroundColor
let titleLabel = self.createTitleLabel(text: OWSLocalizedString(
"SECONDARY_ONBOARDING_SCAN_CODE_TITLE",
comment: "header text while displaying a QR code which, when scanned, will link this device."
))
let bodyLabel = self.createTitleLabel(text: OWSLocalizedString(
"SECONDARY_ONBOARDING_SCAN_CODE_BODY",
comment: "body text while displaying a QR code which, when scanned, will link this device."
))
bodyLabel.font = UIFont.dynamicTypeBody
bodyLabel.numberOfLines = 0
let qrCodeWrapperView = UIView()
qrCodeWrapperView.backgroundColor = .ows_gray02
qrCodeWrapperView.layoutMargins = UIEdgeInsets(margin: 48)
qrCodeWrapperView.layer.cornerRadius = 24
qrCodeImageBackgroundView = UIView()
qrCodeImageBackgroundView.backgroundColor = .white
qrCodeImageBackgroundView.layoutMargins = UIEdgeInsets(margin: 20)
qrCodeImageBackgroundView.layer.cornerRadius = 12
qrCodeImageView = UIImageView()
qrCodeImageView.backgroundColor = .white
qrCodeRefreshButton = OWSRoundedButton()
qrCodeRefreshButton.setAttributedTitle(
{
let icon = NSAttributedString.with(
image: UIImage(named: "refresh")!,
font: .dynamicTypeBody,
centerVerticallyRelativeTo: .dynamicTypeBody
)
let text = OWSLocalizedString(
"SECONDARY_ONBOARDING_SCAN_CODE_REFRESH_CODE_BUTTON",
comment: "Text for a button offering to refresh the QR code to link an iPad."
)
let string = NSMutableAttributedString()
string.append(icon)
string.append(" ")
string.append(text)
return string
}(),
for: .normal
)
qrCodeRefreshButton.setTitleColor(.black, for: .normal)
qrCodeRefreshButton.titleLabel!.font = .dynamicTypeBody.bold()
qrCodeRefreshButton.backgroundColor = .white
qrCodeRefreshButton.ows_contentEdgeInsets = UIEdgeInsets(hMargin: 24, vMargin: 0)
qrCodeRefreshButton.autoSetDimension(.height, toSize: 40)
qrCodeRefreshButton.block = { [weak self] in
Task { [weak self] in
guard let self else { return }
setDisplayMode(.loadingSpinner)
do {
let qrCodeImage = try await fetchNewProvisioningQRCode()
setDisplayMode(.qrCodeImage(qrCodeImage))
} catch {
setDisplayMode(.refreshButton)
}
}
}
qrCodeLoadingSpinner = UIActivityIndicatorView(style: .large)
qrCodeLoadingSpinner.color = Theme.lightThemePrimaryColor
qrCodeLoadingSpinner.autoSetDimensions(to: .init(width: 40, height: 40))
qrCodeLoadingSpinner.startAnimating()
let getHelpLabel = UILabel()
getHelpLabel.text = OWSLocalizedString(
"SECONDARY_ONBOARDING_SCAN_CODE_HELP_TEXT",
comment: "Link text for page with troubleshooting info shown on the QR scanning screen"
)
getHelpLabel.textColor = Theme.accentBlueColor
getHelpLabel.font = UIFont.dynamicTypeSubheadlineClamped
getHelpLabel.numberOfLines = 0
getHelpLabel.textAlignment = .center
getHelpLabel.lineBreakMode = .byWordWrapping
getHelpLabel.isUserInteractionEnabled = true
getHelpLabel.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapExplanationLabel)))
getHelpLabel.setContentHuggingHigh()
// MARK: Layout
qrCodeImageBackgroundView.addSubview(qrCodeImageView)
qrCodeImageView.autoPinEdgesToSuperviewMargins()
qrCodeWrapperView.addSubview(qrCodeLoadingSpinner)
qrCodeLoadingSpinner.autoCenterInSuperviewMargins()
qrCodeWrapperView.addSubview(qrCodeRefreshButton)
qrCodeRefreshButton.autoCenterInSuperviewMargins()
qrCodeWrapperView.addSubview(qrCodeImageBackgroundView)
qrCodeImageBackgroundView.autoPinEdgesToSuperviewMargins()
primaryView.addSubview(titleLabel)
primaryView.addSubview(bodyLabel)
primaryView.addSubview(qrCodeWrapperView)
primaryView.addSubview(getHelpLabel)
titleLabel.setContentHuggingHigh()
titleLabel.autoPinTopToSuperviewMargin()
titleLabel.autoPinWidthToSuperviewMargins()
titleLabel.autoPinEdge(.bottom, to: .top, of: bodyLabel, withOffset: -12)
bodyLabel.setContentHuggingHigh()
bodyLabel.autoPinWidthToSuperviewMargins()
bodyLabel.autoPinEdge(.bottom, to: .top, of: qrCodeWrapperView, withOffset: -64)
qrCodeWrapperView.autoHCenterInSuperview()
qrCodeWrapperView.autoSetDimensions(to: .square(352))
#if TESTABLE_BUILD
let shareURLButton = UIButton(type: .system)
shareURLButton.setTitle(LocalizationNotNeeded("Debug only: Share URL"), for: .normal)
shareURLButton.addTarget(self, action: #selector(didTapShareURL), for: .touchUpInside)
primaryView.addSubview(shareURLButton)
getHelpLabel.autoPinWidthToSuperviewMargins()
getHelpLabel.autoPinEdge(.bottom, to: .top, of: shareURLButton, withOffset: -12)
shareURLButton.autoPinWidthToSuperviewMargins()
shareURLButton.autoPinBottomToSuperviewMargin()
#else
getHelpLabel.autoPinWidthToSuperviewMargins()
getHelpLabel.autoPinBottomToSuperviewMargin()
#endif
}
override func viewDidLoad() {
super.viewDidLoad()
setDisplayMode(.loadingSpinner)
rotateQRCodeTask = Task {
/// Refresh the QR code every 45s, five times. If we fail, or once
/// we've exhausted the five refreshes, fall back to showing an
/// error state with a manual retry.
do {
for _ in 0..<5 {
let qrCodeImage = try await fetchNewProvisioningQRCode()
try Task.checkCancellation()
setDisplayMode(.qrCodeImage(qrCodeImage))
try await Task.sleep(nanoseconds: 45 * NSEC_PER_SEC)
try Task.checkCancellation()
}
} catch is CancellationError {
// Bail!
return
} catch {
// Fall through as if we'd exhausted our rotations.
}
setDisplayMode(.refreshButton)
}
}
override public func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
rotateQRCodeTask?.cancel()
rotateQRCodeTask = nil
setDisplayMode(.refreshButton)
}
// MARK: - Events
override func shouldShowBackButton() -> Bool {
// Never show the back button here
return false
}
@objc
func didTapExplanationLabel(sender: UIGestureRecognizer) {
UIApplication.shared.open(URL(string: "https://support.signal.org/hc/articles/360007320451")!)
}
#if TESTABLE_BUILD
private let currentProvisioningUrl: AtomicValue<URL?> = AtomicValue(nil, lock: .init())
@IBAction func didTapShareURL(_ sender: UIButton) {
if let provisioningUrl = currentProvisioningUrl.get() {
UIPasteboard.general.url = provisioningUrl
// If we share the plain url and airdrop it to a mac, it will just open the url,
// and fail because signal desktop can't open it.
// Share some text instead so we can open it on mac and copy paste into
// a primary device simulator.
let activityVC = UIActivityViewController(
activityItems: ["Provisioning URL: " + provisioningUrl.absoluteString],
applicationActivities: nil
)
activityVC.popoverPresentationController?.sourceView = sender
self.present(activityVC, animated: true)
} else {
UIPasteboard.general.string = LocalizationNotNeeded("URL NOT READY YET")
}
}
#endif
// MARK: -
private nonisolated func fetchNewProvisioningQRCode() async throws -> UIImage? {
let provisioningUrl = try await provisioningController.getProvisioningURL()
#if TESTABLE_BUILD
currentProvisioningUrl.set(provisioningUrl)
#endif
return SignalBrandedQRCodeGenerator(
foregroundColor: .qrCodeBlue,
backgroundColor: .clear
).generateQRCode(url: provisioningUrl)
}
}
private extension UIColor {
static var qrCodeBlue: UIColor {
return SignalBrandedQRCodes.QRCodeColor.blue.foreground
}
}