Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Provisioning/UserInterface/BaseQuickRestoreQRCodeViewController.swift
1 views
//
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI
import SwiftUI

class BaseQuickRestoreQRCodeViewController:
    OWSViewController,
    OWSNavigationChildController,
    ProvisioningSocketManagerUIDelegate
{
    private var provisioningSocketManager: ProvisioningSocketManager
    private var model: RotatingQRCodeView.Model

    override init() {
        self.provisioningSocketManager = ProvisioningSocketManager(linkType: .quickRestore)
        self.model = RotatingQRCodeView.Model(
            urlDisplayMode: .loading,
            onRefreshButtonPressed: { [weak provisioningSocketManager] in
                provisioningSocketManager?.reset()
            },
        )
        super.init()

        self.provisioningSocketManager.delegate = self
        self.navigationItem.hidesBackButton = true
    }

    private lazy var hostingController = UIHostingController(rootView: ContentStack(
        model: model,
        cancelAction: { [weak self] in
            self?.cancel()
        },
    ))

    func cancel() {
        provisioningSocketManager.stop()
    }

    func reset() {
        provisioningSocketManager.reset()
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .Signal.background

        addChild(hostingController)
        view.addSubview(hostingController.view)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.topAnchor.constraint(equalTo: contentLayoutGuide.topAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: contentLayoutGuide.leadingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: contentLayoutGuide.bottomAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: contentLayoutGuide.trailingAnchor),
        ])
        hostingController.didMove(toParent: self)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        provisioningSocketManager.reset()
    }

    func waitForMessage() async throws -> RegistrationProvisioningMessage {
        return try await provisioningSocketManager.waitForMessage()
    }

    // MARK: ProvisioningSocketManagerUIDelegate

    func provisioningSocketManager(
        _ provisioningSocketManager: ProvisioningSocketManager,
        didUpdateProvisioningURL url: URL,
    ) {
        self.model.updateURLDisplayMode(.loaded(url))
    }

    func provisioningSocketManagerDidPauseQRRotation(_ provisioningSocketManager: ProvisioningSocketManager) {
        self.model.updateURLDisplayMode(.refreshButton)
    }

    // MARK: OWSNavigationChildController

    var preferredNavigationBarStyle: OWSNavigationBarStyle { .solid }

    var navbarBackgroundColorOverride: UIColor? { .clear }
}

// MARK: - SwiftUI

private struct ContentStack: View {
    @ObservedObject var model: RotatingQRCodeView.Model

    let cancelAction: () -> Void

    var body: some View {
        ScrollView {
            VStack(spacing: 36) {
                Text(OWSLocalizedString(
                    "REGISTRATION_SCAN_QR_CODE_TITLE",
                    comment: "Title for screen containing QR code that users scan with their old phone when they want to transfer/restore their message history to a new device.",
                ))
                .font(.title.weight(.semibold))
                .multilineTextAlignment(.center)
                .fixedSize(horizontal: false, vertical: true)

                RotatingQRCodeView(model: model)
                    .padding(.horizontal, 40)

                TutorialStack()

                Button(CommonStrings.cancelButton) {
                    self.cancelAction()
                }
                .buttonStyle(Registration.UI.MediumSecondaryButtonStyle())
                .dynamicTypeSize(...DynamicTypeSize.accessibility2)
                .padding(.bottom, NSDirectionalEdgeInsets.buttonContainerLayoutMargins.bottom)
            }
        }
    }
}

private struct TutorialStack: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 24) {
            Label(
                OWSLocalizedString(
                    "REGISTRATION_SCAN_QR_CODE_TUTORIAL_OPEN_SIGNAL",
                    comment: "Tutorial text describing the first step to scanning the restore/transfer QR code with your old phone: opening Signal",
                ),
                image: "device-phone",
            )
            .fixedSize(horizontal: false, vertical: true)
            Label(
                OWSLocalizedString(
                    "REGISTRATION_SCAN_QR_CODE_TUTORIAL_TAP_CAMERA",
                    comment: "Tutorial text describing the second step to scanning the restore/transfer QR code with your old phone: tap the camera icon",
                ),
                image: "camera",
            )
            .fixedSize(horizontal: false, vertical: true)
            Label(
                OWSLocalizedString(
                    "REGISTRATION_SCAN_QR_CODE_TUTORIAL_SCAN",
                    comment: "Tutorial text describing the third step to scanning the restore/transfer QR code with your old phone: scan the code",
                ),
                image: "qr_code",
            )
            .fixedSize(horizontal: false, vertical: true)
        }
        .foregroundStyle(.secondary)
        .padding(.horizontal, 8)
    }
}

#if DEBUG
@available(iOS 17, *)
#Preview {
    @Previewable @State var displayMode: RotatingQRCodeView.Model.URLDisplayMode = .loading

    let url1 = URL(string: "https://signal.org")!
    let url2 = URL(string: "https://support.signal.org")!
    let cycle: () async -> Void = { @MainActor in
        displayMode = .loading
        try? await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
        displayMode = .loaded(url1)
        try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 3)
        displayMode = .loaded(url2)
        try? await Task.sleep(nanoseconds: NSEC_PER_SEC * 3)
        displayMode = .refreshButton
    }

    ContentStack(
        model: .init(
            urlDisplayMode: displayMode,
            onRefreshButtonPressed: { Task { await cycle() } },
        ),
        cancelAction: {},
    )
    .task {
        await cycle()
    }
}
#endif