• add dynamic color support • tweak fonts and colors to better match other reg screens • migrate away from PureLayout.
432 lines
17 KiB
Swift
432 lines
17 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import MultipeerConnectivity
|
|
import SignalServiceKit
|
|
public import SignalUI
|
|
|
|
public class RegistrationTransferQRCodeViewController: OWSViewController, OWSNavigationChildController {
|
|
|
|
public var preferredNavigationBarStyle: OWSNavigationBarStyle { .solid }
|
|
|
|
public var navbarBackgroundColorOverride: UIColor? { .clear }
|
|
|
|
public var prefersNavigationBarHidden: Bool { true }
|
|
|
|
public override var preferredStatusBarStyle: UIStatusBarStyle {
|
|
return isQRCodeExpanded ? .lightContent : super.preferredStatusBarStyle
|
|
}
|
|
|
|
private lazy var qrCodeView = QRCodeView(contentInset: 8)
|
|
|
|
private lazy var expansionButton: UIButton = {
|
|
let button = UIButton(configuration: .filled(), primaryAction: UIAction { [weak self] _ in
|
|
self?.toggleQRCodeExpansion()
|
|
})
|
|
button.configuration?.cornerStyle = .capsule
|
|
button.configuration?.imagePadding = 4
|
|
button.configuration?.titleTextAttributesTransformer = .defaultFont(.dynamicTypeHeadlineClamped)
|
|
button.configuration?.contentInsets = .init(hMargin: 16, vMargin: 8)
|
|
return button
|
|
}()
|
|
|
|
private lazy var compactQRCodeContainer: UIView = {
|
|
let view = UIView()
|
|
view.layer.cornerRadius = 12
|
|
view.directionalLayoutMargins = .init(margin: 16)
|
|
view.backgroundColor = .Signal.secondaryBackground
|
|
|
|
view.addSubview(qrCodeView)
|
|
qrCodeView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
view.addSubview(expansionButton)
|
|
expansionButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
NSLayoutConstraint.activate([
|
|
qrCodeView.widthAnchor.constraint(equalTo: qrCodeView.heightAnchor),
|
|
|
|
qrCodeView.topAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
|
qrCodeView.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
|
qrCodeView.trailingAnchor.constraint(equalTo: view.layoutMarginsGuide.trailingAnchor),
|
|
|
|
expansionButton.topAnchor.constraint(equalTo: qrCodeView.bottomAnchor, constant: 12),
|
|
expansionButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
|
expansionButton.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.bottomAnchor),
|
|
])
|
|
|
|
return view
|
|
}()
|
|
|
|
private lazy var closeButton: UIButton = {
|
|
let closeButton = OWSButton()
|
|
closeButton.setImage(
|
|
Theme.iconImage(.buttonX).withTintColor(.ows_white, renderingMode: .alwaysOriginal),
|
|
for: .normal
|
|
)
|
|
closeButton.contentMode = .center
|
|
closeButton.addTarget(self, action: #selector(compactQRCode), for: .touchUpInside)
|
|
return closeButton
|
|
}()
|
|
|
|
private let titleLabel: UILabel = {
|
|
let label = UILabel.titleLabelForRegistration(
|
|
text: OWSLocalizedString(
|
|
"DEVICE_TRANSFER_QRCODE_TITLE",
|
|
comment: "The title for the device transfer qr code view"
|
|
)
|
|
)
|
|
label.accessibilityIdentifier = "onboarding.transferQRCode.titleLabel"
|
|
return label
|
|
}()
|
|
|
|
private let explanationLabel: UILabel = {
|
|
let label = UILabel.explanationLabelForRegistration(
|
|
text: OWSLocalizedString(
|
|
"DEVICE_TRANSFER_QRCODE_EXPLANATION",
|
|
comment: "The explanation for the device transfer qr code view"
|
|
)
|
|
)
|
|
label.accessibilityIdentifier = "onboarding.transferQRCode.bodyLabel"
|
|
return label
|
|
}()
|
|
|
|
private lazy var explanationLabel2: UILabel = {
|
|
let explanationLabel2 = UILabel.explanationLabelForRegistration(
|
|
text: OWSLocalizedString(
|
|
"DEVICE_TRANSFER_QRCODE_EXPLANATION2",
|
|
comment: "The second explanation for the device transfer qr code view"
|
|
)
|
|
)
|
|
return explanationLabel2
|
|
}()
|
|
|
|
private lazy var helpButton: UIButton = {
|
|
let button = UIButton(
|
|
configuration: .mediumBorderless(title: OWSLocalizedString(
|
|
"DEVICE_TRANSFER_QRCODE_NOT_SEEING",
|
|
comment: "A prompt to provide further explanation if the user is not seeing the transfer on both devices."
|
|
)),
|
|
primaryAction: UIAction { [weak self] _ in
|
|
self?.didTapHelp()
|
|
}
|
|
)
|
|
button.enableMultilineLabel()
|
|
return button
|
|
}()
|
|
|
|
private lazy var cancelButton = UIButton(
|
|
configuration: .mediumSecondary(title: CommonStrings.cancelButton),
|
|
primaryAction: UIAction { [weak self] _ in
|
|
self?.didTapCancel()
|
|
}
|
|
)
|
|
|
|
private let url: URL
|
|
|
|
public init(url: URL) {
|
|
self.url = url
|
|
|
|
super.init()
|
|
|
|
navigationItem.hidesBackButton = true
|
|
}
|
|
|
|
override public func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
view.backgroundColor = .Signal.background
|
|
|
|
// Content view.
|
|
let stackView = UIStackView(arrangedSubviews: [
|
|
titleLabel,
|
|
explanationLabel,
|
|
compactQRCodeContainer,
|
|
explanationLabel2,
|
|
UIView.vStretchingSpacer(),
|
|
helpButton,
|
|
cancelButton
|
|
])
|
|
stackView.axis = .vertical
|
|
stackView.alignment = .center
|
|
stackView.spacing = 12
|
|
stackView.setCustomSpacing(24, after: explanationLabel)
|
|
stackView.setCustomSpacing(24, after: compactQRCodeContainer)
|
|
stackView.preservesSuperviewLayoutMargins = true
|
|
stackView.isLayoutMarginsRelativeArrangement = true
|
|
view.addSubview(stackView)
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
stackView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
|
|
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
stackView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -8),
|
|
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
])
|
|
|
|
// QR code view.
|
|
qrCodeView.translatesAutoresizingMaskIntoConstraints = false
|
|
qrCodeView.setContentHuggingVerticalLow()
|
|
NSLayoutConstraint.activate([
|
|
{
|
|
let constraint = qrCodeView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: Constants.compactQRCodeWidthMultiple)
|
|
constraint.priority = .defaultHigh
|
|
return constraint
|
|
}(),
|
|
qrCodeView.widthAnchor.constraint(greaterThanOrEqualToConstant: Constants.compactQRCodeMinSize),
|
|
])
|
|
|
|
view.addSubview(closeButton)
|
|
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
|
NSLayoutConstraint.activate([
|
|
closeButton.leadingAnchor.constraint(equalTo: view.layoutMarginsGuide.leadingAnchor),
|
|
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
|
|
])
|
|
|
|
updateExpansionState(animated: false)
|
|
|
|
view.addGestureRecognizer(UITapGestureRecognizer(
|
|
target: self,
|
|
action: #selector(compactQRCode)
|
|
))
|
|
}
|
|
|
|
override public func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
|
|
AppEnvironment.shared.deviceTransferServiceRef.addObserver(self)
|
|
|
|
Task { @MainActor in
|
|
qrCodeView.setQRCode(url: url, stylingMode: .brandedWithoutLogo)
|
|
}
|
|
}
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
super.viewWillDisappear(animated)
|
|
|
|
AppEnvironment.shared.deviceTransferServiceRef.removeObserver(self)
|
|
AppEnvironment.shared.deviceTransferServiceRef.stopAcceptingTransfersFromOldDevices()
|
|
}
|
|
|
|
// MARK: - QR Code expansion
|
|
|
|
private var isQRCodeExpanded = false
|
|
|
|
private func updateExpansionState(animated: Bool) {
|
|
view.isUserInteractionEnabled = false
|
|
|
|
let otherViews = [
|
|
titleLabel,
|
|
explanationLabel,
|
|
helpButton,
|
|
cancelButton
|
|
]
|
|
|
|
let animations: () -> Void = {
|
|
if self.isQRCodeExpanded {
|
|
let darkTraitCollection = UITraitCollection(userInterfaceStyle: .dark)
|
|
|
|
let desiredSize = UIScreen.main.bounds.width - (Constants.expandedQRCodeMargin * 2)
|
|
let qrExpandScale = desiredSize / self.qrCodeView.frame.width
|
|
let currentQRCenterY = self.view.convert(
|
|
self.qrCodeView.center,
|
|
from: self.qrCodeView.superview
|
|
).y
|
|
let desiredQRCenterY = self.view.frame.height * 0.4
|
|
let qrExpandYOffset = desiredQRCenterY - currentQRCenterY
|
|
self.qrCodeView.layer.anchorPoint = .init(x: 0.5, y: 0.5)
|
|
self.qrCodeView.transform = .scale(qrExpandScale)
|
|
.concatenating(.translate(CGPoint(x: 0, y: qrExpandYOffset)))
|
|
|
|
let expandedQRCodeBottom = desiredQRCenterY + (desiredSize / 2)
|
|
|
|
let desiredButtonTop = expandedQRCodeBottom + 16
|
|
let currentButtonTop = self.view.convert(
|
|
self.expansionButton.frame.origin,
|
|
from: self.expansionButton.superview
|
|
).y
|
|
let buttonYOffset = max(0, desiredButtonTop - currentButtonTop)
|
|
self.expansionButton.transform = .translate(CGPoint(x: 0, y: buttonYOffset))
|
|
|
|
let desiredLabelTop = desiredButtonTop + self.expansionButton.frame.height + 16
|
|
let currentLabelTop = self.view.convert(
|
|
self.explanationLabel2.frame.origin,
|
|
from: self.explanationLabel2.superview
|
|
).y
|
|
let labelYOffset = max(0, desiredLabelTop - currentLabelTop)
|
|
self.explanationLabel2.transform = .translate(CGPoint(x: 0, y: labelYOffset))
|
|
|
|
self.explanationLabel2.textColor = .Signal.secondaryLabel.resolvedColor(with: darkTraitCollection)
|
|
self.compactQRCodeContainer.backgroundColor = .clear
|
|
self.view.backgroundColor = .Signal.background.resolvedColor(with: darkTraitCollection)
|
|
|
|
self.closeButton.alpha = 1
|
|
otherViews.forEach { $0.alpha = 0 }
|
|
|
|
// Expand/collapse button
|
|
self.expansionButton.configuration?.title = OWSLocalizedString(
|
|
"DEVICE_TRANSFER_CONTRACT_QR_CODE_BUTTON",
|
|
comment: "Button shown to contract a QR code and exit the fullscreen view."
|
|
)
|
|
self.expansionButton.configuration?.image = Theme.iconImage(.minimize16)
|
|
self.expansionButton.configuration?.baseForegroundColor = Theme.darkThemePrimaryColor
|
|
self.expansionButton.configuration?.baseBackgroundColor = Theme.darkThemeSecondaryBackgroundColor
|
|
} else {
|
|
self.qrCodeView.transform = .identity
|
|
self.expansionButton.transform = .identity
|
|
self.explanationLabel2.transform = .identity
|
|
|
|
self.explanationLabel2.textColor = .Signal.secondaryLabel
|
|
self.compactQRCodeContainer.backgroundColor = .Signal.secondaryBackground
|
|
self.view.backgroundColor = .Signal.background
|
|
|
|
self.closeButton.alpha = 0
|
|
otherViews.forEach { $0.alpha = 1 }
|
|
|
|
// Expand/collapse button
|
|
self.expansionButton.configuration?.title = OWSLocalizedString(
|
|
"DEVICE_TRANSFER_EXPAND_QR_CODE_BUTTON",
|
|
comment: "Button shown to expand a QR code and view it fullscreen."
|
|
)
|
|
self.expansionButton.configuration?.image = Theme.iconImage(.maximize16)
|
|
self.expansionButton.configuration?.baseForegroundColor = .Signal.label
|
|
self.expansionButton.configuration?.baseBackgroundColor = .clear
|
|
}
|
|
}
|
|
|
|
guard animated else {
|
|
animations()
|
|
view.isUserInteractionEnabled = true
|
|
return
|
|
}
|
|
UIView.animate(withDuration: 0.2, animations: animations) { _ in
|
|
self.view.isUserInteractionEnabled = true
|
|
}
|
|
}
|
|
|
|
@objc
|
|
private func toggleQRCodeExpansion() {
|
|
isQRCodeExpanded = !isQRCodeExpanded
|
|
updateExpansionState(animated: true)
|
|
setNeedsStatusBarAppearanceUpdate()
|
|
}
|
|
|
|
@objc
|
|
private func compactQRCode() {
|
|
guard isQRCodeExpanded else { return }
|
|
toggleQRCodeExpansion()
|
|
}
|
|
|
|
// MARK: - Events
|
|
|
|
weak var permissionActionSheetController: ActionSheetController?
|
|
|
|
func didTapHelp() {
|
|
guard !isQRCodeExpanded else { return }
|
|
let turnOnView = TurnOnPermissionView(
|
|
title: OWSLocalizedString(
|
|
"LOCAL_NETWORK_PERMISSION_ACTION_SHEET_TITLE",
|
|
comment: "Title for local network permission action sheet"
|
|
),
|
|
message: OWSLocalizedString(
|
|
"LOCAL_NETWORK_PERMISSION_ACTION_SHEET_BODY",
|
|
comment: "Body for local network permission action sheet"
|
|
),
|
|
steps: [
|
|
.init(
|
|
icon: #imageLiteral(resourceName: "settings-app-icon-32"),
|
|
text: OWSLocalizedString(
|
|
"LOCAL_NETWORK_PERMISSION_ACTION_SHEET_STEP_ONE",
|
|
comment: "First step for local network permission action sheet"
|
|
)
|
|
),
|
|
.init(
|
|
icon: UIImage(resource: UIApplication.shared.currentAppIcon.previewImageResource),
|
|
text: OWSLocalizedString(
|
|
"LOCAL_NETWORK_PERMISSION_ACTION_SHEET_STEP_TWO",
|
|
comment: "Second step for local network permission action sheet"
|
|
)
|
|
),
|
|
.init(
|
|
icon: UIImage(imageLiteralResourceName: "toggle-32"),
|
|
text: OWSLocalizedString(
|
|
"LOCAL_NETWORK_PERMISSION_ACTION_SHEET_STEP_THREE",
|
|
comment: "Third step for local network permission action sheet"
|
|
)
|
|
)
|
|
],
|
|
button: UIButton(
|
|
configuration: .largePrimary(title: OWSLocalizedString(
|
|
"LOCAL_NETWORK_PERMISSION_ACTION_SHEET_NEED_HELP",
|
|
comment: "A button asking the user if they need further help getting their transfer working."
|
|
)),
|
|
primaryAction: UIAction { [weak self] _ in
|
|
self?.didTapContactSupport()
|
|
}
|
|
)
|
|
)
|
|
|
|
let actionSheetController = ActionSheetController()
|
|
permissionActionSheetController = actionSheetController
|
|
actionSheetController.customHeader = turnOnView
|
|
actionSheetController.isCancelable = true
|
|
presentActionSheet(actionSheetController)
|
|
}
|
|
|
|
func didTapCancel() {
|
|
guard !isQRCodeExpanded else { return }
|
|
Logger.info("")
|
|
|
|
guard let navigationController else {
|
|
return owsFailBeta("unexpectedly missing nav controller")
|
|
}
|
|
|
|
navigationController.popViewController(animated: true)
|
|
}
|
|
|
|
func didTapContactSupport() {
|
|
guard !isQRCodeExpanded else { return }
|
|
Logger.info("")
|
|
|
|
permissionActionSheetController?.dismiss(animated: true)
|
|
permissionActionSheetController = nil
|
|
|
|
ContactSupportActionSheet.present(
|
|
emailFilter: .deviceTransfer,
|
|
logDumper: .fromGlobals(),
|
|
fromViewController: self
|
|
)
|
|
}
|
|
|
|
// MARK: - Constants
|
|
|
|
private enum Constants {
|
|
static let compactQRCodeWidthMultiple: CGFloat = 0.6
|
|
static let compactQRCodeMinSize: CGFloat = 182
|
|
|
|
static let expandedQRCodeMargin: CGFloat = 8
|
|
}
|
|
}
|
|
|
|
extension RegistrationTransferQRCodeViewController: DeviceTransferServiceObserver {
|
|
func deviceTransferServiceDiscoveredNewDevice(peerId: MCPeerID, discoveryInfo: [String: String]?) {}
|
|
|
|
func deviceTransferServiceDidStartTransfer(progress: Progress) {
|
|
let view = RegistrationTransferProgressViewController(progress: progress)
|
|
navigationController?.pushViewController(view, animated: true)
|
|
}
|
|
|
|
func deviceTransferServiceDidEndTransfer(error: DeviceTransferService.Error?) {
|
|
if let error = error {
|
|
owsFailDebug("unexpected error while rendering QR code \(error)")
|
|
}
|
|
}
|
|
|
|
func deviceTransferServiceDidRequestAppRelaunch() {
|
|
if CurrentAppContext().frontmostViewController() == self {
|
|
owsFail("Relaunch not supported from QR screen")
|
|
}
|
|
}
|
|
}
|