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

import MultipeerConnectivity
import SignalServiceKit
public import SignalUI
import UIKit

public class RegistrationTransferProgressViewController: OWSViewController {

    let progressView: TransferProgressView

    public init(progress: Progress) {
        self.progressView = TransferProgressView(progress: progress)

        super.init()

        navigationItem.hidesBackButton = true
    }

    override public func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .Signal.background

        let titleLabel = UILabel.titleLabelForRegistration(
            text: OWSLocalizedString(
                "DEVICE_TRANSFER_RECEIVING_TITLE",
                comment: "The title on the view that shows receiving progress",
            ),
        )
        titleLabel.accessibilityIdentifier = "onboarding.transferProgress.titleLabel"

        let explanationLabel = UILabel.explanationLabelForRegistration(
            text: OWSLocalizedString(
                "DEVICE_TRANSFER_RECEIVING_EXPLANATION",
                comment: "The explanation on the view that shows receiving progress",
            ),
        )
        explanationLabel.accessibilityIdentifier = "onboarding.transferProgress.bodyLabel"

        let cancelButton = UIButton(
            configuration: .mediumSecondary(title: CommonStrings.cancelButton),
            primaryAction: UIAction { [weak self] _ in
                self?.didTapCancel()
            },
        )

        let topSpacer = UIView.vStretchingSpacer()
        let bottomSpacer = UIView.vStretchingSpacer()

        addStaticContentStackView(arrangedSubviews: [
            titleLabel,
            explanationLabel,
            topSpacer,
            progressView,
            bottomSpacer,
            cancelButton.enclosedInVerticalStackView(isFullWidthButton: false),
        ])
        topSpacer.translatesAutoresizingMaskIntoConstraints = false
        bottomSpacer.translatesAutoresizingMaskIntoConstraints = false
        topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true
    }

    override public func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        progressView.startUpdatingProgress()

        AppEnvironment.shared.deviceTransferServiceRef.addObserver(self)
    }

    override public func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        progressView.stopUpdatingProgress()

        AppEnvironment.shared.deviceTransferServiceRef.removeObserver(self)
        AppEnvironment.shared.deviceTransferServiceRef.cancelTransferFromOldDevice()
    }

    // MARK: - Events

    func didTapCancel() {
        Logger.info("")

        let actionSheet = ActionSheetController(
            title: OWSLocalizedString(
                "DEVICE_TRANSFER_CANCEL_CONFIRMATION_TITLE",
                comment: "The title of the dialog asking the user if they want to cancel a device transfer",
            ),
            message: OWSLocalizedString(
                "DEVICE_TRANSFER_CANCEL_CONFIRMATION_MESSAGE",
                comment: "The message of the dialog asking the user if they want to cancel a device transfer",
            ),
        )
        actionSheet.addAction(OWSActionSheets.cancelAction)

        let okAction = ActionSheetAction(
            title: OWSLocalizedString(
                "DEVICE_TRANSFER_CANCEL_CONFIRMATION_ACTION",
                comment: "The stop action of the dialog asking the user if they want to cancel a device transfer",
            ),
            style: .destructive,
        ) { [weak self] _ in
            // viewWillDissapear will cancel the transfer
            self?.navigationController?.popViewController(animated: true)
        }
        actionSheet.addAction(okAction)

        present(actionSheet, animated: true)
    }
}

extension RegistrationTransferProgressViewController: DeviceTransferServiceObserver {
    func deviceTransferServiceDiscoveredNewDevice(peerId: MCPeerID, discoveryInfo: [String: String]?) {}

    func deviceTransferServiceDidStartTransfer(progress: Progress) {}

    func deviceTransferServiceDidEndTransfer(error: DeviceTransferService.Error?) {
        guard let error else { return }

        switch error {
        case .assertion:
            progressView.renderError(
                text: OWSLocalizedString(
                    "DEVICE_TRANSFER_ERROR_GENERIC",
                    comment: "An error indicating that something went wrong with the transfer and it could not complete",
                ),
            )
        case .backgroundedDevice:
            progressView.renderError(
                text: OWSLocalizedString(
                    "DEVICE_TRANSFER_ERROR_BACKGROUNDED",
                    comment: "An error indicating that the other device closed signal mid-transfer and it could not complete",
                ),
            )
        case .cancel:
            // User initiated, nothing to do
            break
        case .certificateMismatch:
            owsFailDebug("This should never happen on the new device")
        case .notEnoughSpace:
            progressView.renderError(
                text: OWSLocalizedString(
                    "DEVICE_TRANSFER_ERROR_NOT_ENOUGH_SPACE",
                    comment: "An error indicating that the user does not have enough free space on their device to complete the transfer",
                ),
            )
        case .unsupportedVersion:
            progressView.renderError(
                text: OWSLocalizedString(
                    "DEVICE_TRANSFER_ERROR_UNSUPPORTED_VERSION",
                    comment: "An error indicating the user must update their device before trying to transfer.",
                ),
            )
        case .modeMismatch:
            owsFailDebug("This should never happen on the new device")
        }
    }

    func deviceTransferServiceDidRequestAppRelaunch() {
        self.present(TransferRelaunchSheet(), animated: true)
    }
}

private class TransferRelaunchSheet: HeroSheetViewController {
    override var canBeDismissed: Bool { false }

    init() {
        super.init(
            hero: .image(UIImage(named: "transfer_complete")!),
            title: OWSLocalizedString(
                "TRANSFER_COMPLETE_SHEET_TITLE",
                comment: "Title for bottom sheet shown when device transfer completes on the receiving device.",
            ),
            body: OWSLocalizedString(
                "TRANSFER_COMPLETE_SHEET_SUBTITLE",
                comment: "Subtitle for bottom sheet shown when device transfer completes on the receiving device.",
            ),
            primaryButton: .init(title: OWSLocalizedString(
                "TRANSFER_COMPLETE_SHEET_BUTTON",
                comment: "Button for bottom sheet shown when device transfer completes on the receiving device. Tapping will terminate the Signal app and trigger a notification to relaunch.",
            )) { _ in
                Self.didTapExitButton()
            },
        )
    }

    private static func didTapExitButton() {
        Logger.info("")
        SSKEnvironment.shared.notificationPresenterRef.notifyUserToRelaunchAfterTransfer {
            Logger.info("Deliberately terminating app post-transfer.")
            exit(0)
        }
    }
}

// MARK: -

#if DEBUG

@available(iOS 17, *)
#Preview("Transfer Progress") {
    return UINavigationController(
        rootViewController: RegistrationTransferProgressViewController(
            progress: .discreteProgress(totalUnitCount: 1024),
        ),
    )
}

@available(iOS 17, *)
#Preview("Relaunch Sheet") {
    return TransferRelaunchSheet()
}

#endif