// // 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