767 lines
32 KiB
Swift
767 lines
32 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
|
|
class ProvisioningNavigationController: OWSNavigationController {
|
|
private(set) var provisioningController: ProvisioningController
|
|
|
|
init(provisioningController: ProvisioningController) {
|
|
self.provisioningController = provisioningController
|
|
super.init()
|
|
}
|
|
|
|
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
let superOrientations = super.supportedInterfaceOrientations
|
|
let provisioningOrientations: UIInterfaceOrientationMask = UIDevice.current.isIPad ? .all : .portrait
|
|
|
|
return superOrientations.intersection(provisioningOrientations)
|
|
}
|
|
}
|
|
|
|
class ProvisioningController: NSObject {
|
|
|
|
private let appReadiness: AppReadinessSetter
|
|
|
|
private lazy var registrationWebSocketManager = RegistrationWebSocketManagerImpl(
|
|
chatConnectionManager: DependenciesBridge.shared.chatConnectionManager,
|
|
messagePipelineSupervisor: SSKEnvironment.shared.messagePipelineSupervisorRef,
|
|
messageProcessor: SSKEnvironment.shared.messageProcessorRef,
|
|
)
|
|
|
|
private lazy var provisioningCoordinator: ProvisioningCoordinator = {
|
|
return ProvisioningCoordinatorImpl(
|
|
chatConnectionManager: DependenciesBridge.shared.chatConnectionManager,
|
|
db: DependenciesBridge.shared.db,
|
|
identityManager: DependenciesBridge.shared.identityManager,
|
|
linkAndSyncManager: DependenciesBridge.shared.linkAndSyncManager,
|
|
accountKeyStore: DependenciesBridge.shared.accountKeyStore,
|
|
networkManager: SSKEnvironment.shared.networkManagerRef,
|
|
preKeyManager: DependenciesBridge.shared.preKeyManager,
|
|
profileManager: SSKEnvironment.shared.profileManagerImplRef,
|
|
pushRegistrationManager: ProvisioningCoordinatorImpl.Wrappers.PushRegistrationManager(AppEnvironment.shared.pushRegistrationManagerRef),
|
|
receiptManager: ProvisioningCoordinatorImpl.Wrappers.ReceiptManager(SSKEnvironment.shared.receiptManagerRef),
|
|
registrationStateChangeManager: DependenciesBridge.shared.registrationStateChangeManager,
|
|
registrationWebSocketManager: registrationWebSocketManager,
|
|
signalProtocolStoreManager: DependenciesBridge.shared.signalProtocolStoreManager,
|
|
signalService: SSKEnvironment.shared.signalServiceRef,
|
|
storageServiceManager: SSKEnvironment.shared.storageServiceManagerRef,
|
|
svr: DependenciesBridge.shared.svr,
|
|
svrLocalStorage: DependenciesBridge.shared.svrLocalStorage,
|
|
syncManager: SSKEnvironment.shared.syncManagerRef,
|
|
threadStore: ThreadStoreImpl(),
|
|
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
|
|
udManager: SSKEnvironment.shared.udManagerRef,
|
|
)
|
|
}()
|
|
|
|
private let provisioningSocketManager: ProvisioningSocketManager
|
|
|
|
private init(
|
|
appReadiness: AppReadinessSetter,
|
|
provisioningSocketManager: ProvisioningSocketManager,
|
|
) {
|
|
self.appReadiness = appReadiness
|
|
self.provisioningSocketManager = provisioningSocketManager
|
|
|
|
super.init()
|
|
}
|
|
|
|
@MainActor
|
|
static func presentProvisioningFlow(appReadiness: AppReadinessSetter) {
|
|
let provisioningSocketManager = ProvisioningSocketManager(linkType: .linkDevice)
|
|
let provisioningController = ProvisioningController(
|
|
appReadiness: appReadiness,
|
|
provisioningSocketManager: provisioningSocketManager,
|
|
)
|
|
let navController = ProvisioningNavigationController(provisioningController: provisioningController)
|
|
provisioningController.setUpDebugLogsGesture(on: navController)
|
|
|
|
let (backupRestoreState, registrationState) = DependenciesBridge.shared.db.read { tx in
|
|
(
|
|
DependenciesBridge.shared.backupArchiveManager.backupRestoreState(tx: tx),
|
|
DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx),
|
|
)
|
|
}
|
|
|
|
switch (backupRestoreState, registrationState) {
|
|
case (.unfinalized, .unregistered), (.finalized, .unregistered):
|
|
// If we started a link'n'sync and terminated after committing
|
|
// the restored backup but before finishing, reset the app data
|
|
// and start over.
|
|
SignalApp.shared.resetAppDataAndExit(
|
|
keyFetcher: SSKEnvironment.shared.databaseStorageRef.keyFetcher,
|
|
)
|
|
default:
|
|
break
|
|
}
|
|
|
|
let vc = ProvisioningSplashViewController(provisioningController: provisioningController)
|
|
navController.setViewControllers([vc], animated: false)
|
|
|
|
CurrentAppContext().mainWindow?.rootViewController = navController
|
|
}
|
|
|
|
static func presentRelinkingFlow(appReadiness: AppReadinessSetter) {
|
|
let provisioningSocketManager = ProvisioningSocketManager(linkType: .linkDevice)
|
|
let provisioningController = ProvisioningController(
|
|
appReadiness: appReadiness,
|
|
provisioningSocketManager: provisioningSocketManager,
|
|
)
|
|
let navController = ProvisioningNavigationController(provisioningController: provisioningController)
|
|
provisioningController.setUpDebugLogsGesture(on: navController)
|
|
|
|
let vc = ProvisioningQRCodeViewController(
|
|
provisioningController: provisioningController,
|
|
provisioningSocketManager: provisioningSocketManager,
|
|
)
|
|
navController.setViewControllers([vc], animated: false)
|
|
CurrentAppContext().mainWindow?.rootViewController = navController
|
|
|
|
Task {
|
|
await provisioningController.awaitProvisioning(
|
|
from: vc,
|
|
navigationController: navController,
|
|
)
|
|
}
|
|
}
|
|
|
|
#if DEBUG
|
|
static func preview() -> ProvisioningController {
|
|
ProvisioningController(appReadiness: AppReadinessMock(), provisioningSocketManager: ProvisioningSocketManager(linkType: .linkDevice))
|
|
}
|
|
#endif
|
|
|
|
private func setUpDebugLogsGesture(
|
|
on navigationController: UINavigationController,
|
|
) {
|
|
let submitLogsGesture = UITapGestureRecognizer(target: self, action: #selector(submitLogs))
|
|
submitLogsGesture.numberOfTapsRequired = 8
|
|
submitLogsGesture.delaysTouchesEnded = false
|
|
navigationController.view.addGestureRecognizer(submitLogsGesture)
|
|
}
|
|
|
|
@objc
|
|
@MainActor
|
|
private func submitLogs() {
|
|
guard let viewController = CurrentAppContext().frontmostViewController() else {
|
|
return
|
|
}
|
|
let logs = DebugLogs(dumper: .fromGlobals())
|
|
logs.promptToSubmitLogs(from: viewController, supportTag: "Onboarding")
|
|
}
|
|
|
|
// MARK: - Transitions
|
|
|
|
func provisioningSplashRequestedModeSwitch(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
let view = ProvisioningModeSwitchConfirmationViewController(provisioningController: self)
|
|
viewController.navigationController?.pushViewController(view, animated: true)
|
|
}
|
|
|
|
func switchToPrimaryRegistration(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
let loader = RegistrationCoordinatorLoaderImpl(dependencies: .from(self))
|
|
SignalApp.shared.showRegistration(loader: loader, desiredMode: .registering, appReadiness: appReadiness)
|
|
}
|
|
|
|
@MainActor
|
|
func provisioningSplashDidComplete(viewController: UIViewController) async {
|
|
Logger.info("")
|
|
await pushPermissionsViewOrSkipToRegistration(onto: viewController)
|
|
}
|
|
|
|
@MainActor
|
|
private func pushPermissionsViewOrSkipToRegistration(onto oldViewController: UIViewController) async {
|
|
// Disable interaction during the asynchronous operation.
|
|
oldViewController.view.isUserInteractionEnabled = false
|
|
|
|
let newViewController = ProvisioningPermissionsViewController(provisioningController: self)
|
|
let needsToAskForAnyPermissions = await newViewController.needsToAskForAnyPermissions()
|
|
|
|
// Always re-enable interaction in case the user restart registration.
|
|
oldViewController.view.isUserInteractionEnabled = true
|
|
|
|
if needsToAskForAnyPermissions {
|
|
oldViewController.navigationController?.pushViewController(newViewController, animated: true)
|
|
} else {
|
|
self.provisioningPermissionsDidComplete(viewController: oldViewController)
|
|
}
|
|
}
|
|
|
|
func provisioningPermissionsDidComplete(viewController: UIViewController) {
|
|
AssertIsOnMainThread()
|
|
|
|
Logger.info("")
|
|
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
pushTransferChoiceView(onto: navigationController)
|
|
}
|
|
|
|
func pushTransferChoiceView(onto navigationController: UINavigationController) {
|
|
AssertIsOnMainThread()
|
|
|
|
let view = ProvisioningTransferChoiceViewController(provisioningController: self)
|
|
navigationController.pushViewController(view, animated: true)
|
|
}
|
|
|
|
// MARK: - Transfer
|
|
|
|
@MainActor
|
|
func transferAccount(fromViewController: UIViewController) async {
|
|
Logger.info("")
|
|
guard let navigationController = fromViewController.navigationController else {
|
|
owsFailDebug("Missing navigationController")
|
|
return
|
|
}
|
|
|
|
if navigationController.topViewController is BaseQuickRestoreQRCodeViewController {
|
|
// qr code view is already presented, we don't need to push it again.
|
|
return
|
|
}
|
|
|
|
let view = BaseQuickRestoreQRCodeViewController()
|
|
await navigationController.awaitablePush(view, animated: true)
|
|
do {
|
|
let message = try await view.waitForMessage()
|
|
guard let restoreToken = message.restoreMethodToken else {
|
|
throw OWSAssertionError("Missing restore token")
|
|
}
|
|
|
|
let transferState = DeviceTransferCoordinator(
|
|
deviceTransferService: AppEnvironment.shared.deviceTransferServiceRef,
|
|
quickRestoreManager: AppEnvironment.shared.quickRestoreManager,
|
|
restoreMethodToken: restoreToken,
|
|
restoreMode: .linked,
|
|
)
|
|
|
|
transferState.cancelTransferBlock = { [weak self] in
|
|
self?.pushTransferChoiceView(onto: navigationController)
|
|
}
|
|
transferState.onFailure = { [weak self] _ in
|
|
self?.pushTransferChoiceView(onto: navigationController)
|
|
}
|
|
|
|
await navigationController.awaitablePush(
|
|
DeviceTransferStatusViewController(coordinator: transferState),
|
|
animated: true,
|
|
)
|
|
} catch {
|
|
// Display error to the user
|
|
Logger.error("Failed to start transfer")
|
|
}
|
|
}
|
|
|
|
// MARK: - Linking
|
|
|
|
@MainActor
|
|
func didConfirmSecondaryDevice(from viewController: ProvisioningPrepViewController) async {
|
|
guard let navigationController = viewController.navigationController else {
|
|
owsFailDebug("navigationController was unexpectedly nil")
|
|
return
|
|
}
|
|
|
|
let qrCodeViewController = ProvisioningQRCodeViewController(
|
|
provisioningController: self,
|
|
provisioningSocketManager: provisioningSocketManager,
|
|
)
|
|
|
|
await navigationController.awaitablePush(qrCodeViewController, animated: true)
|
|
|
|
await awaitProvisioning(
|
|
from: qrCodeViewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
|
|
@MainActor
|
|
private func awaitProvisioning(
|
|
from viewController: ProvisioningQRCodeViewController,
|
|
navigationController: UINavigationController,
|
|
) async {
|
|
|
|
let provisioningMessage = await waitForProvisioningMessage(navigationController: navigationController)
|
|
|
|
provisioningSocketManager.stop()
|
|
|
|
guard let provisioningMessage else {
|
|
return
|
|
}
|
|
|
|
/// Ensure the primary is new enough to link us.
|
|
guard provisioningMessage.provisioningVersion >= LinkingProvisioningMessage.Constants.provisioningVersion else {
|
|
OWSActionSheets.showActionSheet(
|
|
title: OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_OLD_VERSION_TITLE",
|
|
comment: "alert title for outdated linking device",
|
|
),
|
|
message: OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_OLD_VERSION_MESSAGE",
|
|
comment: "alert message for outdated linking device",
|
|
),
|
|
) { _ in
|
|
navigationController.popViewController(animated: true)
|
|
}
|
|
return
|
|
}
|
|
|
|
let progressViewModel = LinkAndSyncSecondaryProgressViewModel()
|
|
|
|
performCoordinatorTaskWithModal(
|
|
task: Task {
|
|
try await self.provisioningCoordinator.completeProvisioning(
|
|
provisionMessage: provisioningMessage,
|
|
deviceName: UIDevice.current.name,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
},
|
|
viewController: viewController,
|
|
navigationController: navigationController,
|
|
willLinkAndSync: provisioningMessage.ephemeralBackupKey != nil,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
}
|
|
|
|
@MainActor
|
|
private func waitForProvisioningMessage(
|
|
navigationController: UINavigationController,
|
|
) async -> LinkingProvisioningMessage? {
|
|
do {
|
|
return try await provisioningSocketManager.waitForMessage()
|
|
} catch let error {
|
|
Logger.error("Failed to decrypt provision envelope: \(error)")
|
|
let alert = ActionSheetController(
|
|
title: OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_WAITING_FOR_SCAN",
|
|
comment: "alert title",
|
|
),
|
|
message: error.userErrorDescription,
|
|
)
|
|
alert.addAction(ActionSheetAction(
|
|
title: CommonStrings.cancelButton,
|
|
style: .cancel,
|
|
handler: { _ in
|
|
navigationController.popViewController(animated: true)
|
|
},
|
|
))
|
|
|
|
navigationController.presentActionSheet(alert)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func provisioningDidComplete(from viewController: UIViewController) {
|
|
if viewController.presentedViewController != nil {
|
|
viewController.dismiss(animated: true) {
|
|
self.provisioningDidComplete(from: viewController)
|
|
}
|
|
return
|
|
}
|
|
SignalApp.shared.showConversationSplitView(appReadiness: appReadiness)
|
|
}
|
|
|
|
@MainActor
|
|
private func resetBackToQrCodeController(
|
|
from viewController: ProvisioningQRCodeViewController,
|
|
navigationController: UINavigationController,
|
|
) async {
|
|
Logger.warn("")
|
|
|
|
// Reset at the start so it goes while other stuff animates.
|
|
viewController.reset()
|
|
await registrationWebSocketManager.releaseRestrictedWebSocket(isRegistered: false)
|
|
|
|
if navigationController.presentedViewController != nil {
|
|
await navigationController.awaitableDismiss(animated: true)
|
|
}
|
|
if viewController.presentedViewController != nil {
|
|
await viewController.awaitableDismiss(animated: true)
|
|
}
|
|
navigationController.popToViewController(viewController, animated: true)
|
|
|
|
Task {
|
|
await awaitProvisioning(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func performCoordinatorTaskWithModal(
|
|
task: Task<Void, Error>,
|
|
viewController: ProvisioningQRCodeViewController,
|
|
navigationController: UINavigationController,
|
|
willLinkAndSync: Bool,
|
|
progressViewModel: LinkAndSyncSecondaryProgressViewModel,
|
|
) {
|
|
if willLinkAndSync {
|
|
Task { @MainActor in
|
|
let progressViewController: LinkAndSyncProvisioningProgressViewController
|
|
if let vc = viewController.presentedViewController {
|
|
if let vc = vc as? LinkAndSyncProvisioningProgressViewController {
|
|
progressViewController = vc
|
|
} else {
|
|
vc.dismiss(animated: true, completion: {
|
|
self.performCoordinatorTaskWithModal(
|
|
task: task,
|
|
viewController: viewController,
|
|
navigationController: navigationController,
|
|
willLinkAndSync: willLinkAndSync,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
})
|
|
return
|
|
}
|
|
} else {
|
|
progressViewController = LinkAndSyncProvisioningProgressViewController(
|
|
provisioningController: self,
|
|
viewModel: progressViewModel,
|
|
)
|
|
}
|
|
progressViewController.linkNSyncTask = task
|
|
viewController.present(progressViewController, animated: false)
|
|
do {
|
|
try await task.value
|
|
// Don't dismiss the progress view or it will quickly jump
|
|
// to that before jumping again to the chat list.
|
|
self.provisioningDidComplete(from: viewController)
|
|
} catch var error as CompleteProvisioningError {
|
|
if case let .linkAndSyncError(linkAndSyncError) = error {
|
|
switch linkAndSyncError.error {
|
|
case SecondaryLinkNSyncError.primaryFailedBackupExport(let continueWithoutSyncing):
|
|
if continueWithoutSyncing {
|
|
do {
|
|
try await linkAndSyncError.continueWithoutSyncing()
|
|
self.provisioningDidComplete(from: viewController)
|
|
return
|
|
} catch let innerError as CompleteProvisioningError {
|
|
error = innerError
|
|
}
|
|
} else {
|
|
// Crash if this fails; things have gone horribly wrong.
|
|
try! await linkAndSyncError.restartProvisioning()
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
return
|
|
}
|
|
case is CancellationError:
|
|
// Exit provisioning if we cancelled
|
|
do {
|
|
try await linkAndSyncError.continueWithoutSyncing()
|
|
self.provisioningDidComplete(from: viewController)
|
|
return
|
|
} catch let innerError as CompleteProvisioningError {
|
|
error = innerError
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
let errorActionSheet = self.errorActionSheet(
|
|
error: error,
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
if progressViewController.presentedViewController == nil {
|
|
progressViewController.presentActionSheet(errorActionSheet)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
let presentingController = viewController.presentedViewController ?? viewController
|
|
ModalActivityIndicatorViewController.present(
|
|
fromViewController: presentingController,
|
|
canCancel: false,
|
|
) { modal async -> Void in
|
|
let result: CompleteProvisioningError?
|
|
do {
|
|
try await task.value
|
|
result = nil
|
|
} catch let error {
|
|
result = error as? CompleteProvisioningError
|
|
}
|
|
|
|
let errorActionSheet = result.map {
|
|
self.errorActionSheet(
|
|
error: $0,
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
}
|
|
modal.dismiss {
|
|
if let errorActionSheet {
|
|
presentingController.presentActionSheet(errorActionSheet)
|
|
} else {
|
|
self.provisioningDidComplete(from: viewController)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func errorActionSheet(
|
|
error: CompleteProvisioningError,
|
|
from viewController: ProvisioningQRCodeViewController,
|
|
navigationController: UINavigationController,
|
|
progressViewModel: LinkAndSyncSecondaryProgressViewModel,
|
|
) -> ActionSheetController {
|
|
let alert: ActionSheetController
|
|
switch error {
|
|
case .previouslyLinkedWithDifferentAccount:
|
|
Logger.warn("was previously linked/registered on different account!")
|
|
let title = OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_DIFFERENT_ACCOUNT_TITLE",
|
|
comment: "Title for error alert indicating that re-linking failed because the account did not match.",
|
|
)
|
|
let message = OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_DIFFERENT_ACCOUNT_MESSAGE",
|
|
comment: "Message for error alert indicating that re-linking failed because the account did not match.",
|
|
)
|
|
alert = ActionSheetController(title: title, message: message)
|
|
alert.addAction(ActionSheetAction(
|
|
title: OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_DIFFERENT_ACCOUNT_RESET_DEVICE",
|
|
comment: "Label for the 'reset device' action in the 're-linking failed because the account did not match' alert.",
|
|
),
|
|
style: .default,
|
|
handler: { _ in
|
|
Task { @MainActor in
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
},
|
|
))
|
|
case .deviceLimitExceededError(let error):
|
|
alert = ActionSheetController(title: error.errorDescription, message: error.recoverySuggestion)
|
|
alert.addAction(ActionSheetAction(
|
|
title: CommonStrings.okButton,
|
|
handler: { _ in
|
|
Task { @MainActor in
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
},
|
|
))
|
|
case .obsoleteLinkedDeviceError:
|
|
Logger.warn("obsolete device error")
|
|
let title = OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_OBSOLETE_LINKED_DEVICE_TITLE",
|
|
comment: "Title for error alert indicating that a linked device must be upgraded before it can be linked.",
|
|
)
|
|
let message = OWSLocalizedString(
|
|
"SECONDARY_LINKING_ERROR_OBSOLETE_LINKED_DEVICE_MESSAGE",
|
|
comment: "Message for error alert indicating that a linked device must be upgraded before it can be linked.",
|
|
)
|
|
alert = ActionSheetController(title: title, message: message)
|
|
|
|
let updateButtonText = OWSLocalizedString(
|
|
"APP_UPDATE_NAG_ALERT_UPDATE_BUTTON",
|
|
comment: "Label for the 'update' button in the 'new app version available' alert.",
|
|
)
|
|
let updateAction = ActionSheetAction(
|
|
title: updateButtonText,
|
|
style: .default,
|
|
) { _ in
|
|
let url = TSConstants.appStoreUrl
|
|
UIApplication.shared.open(url, options: [:])
|
|
}
|
|
alert.addAction(updateAction)
|
|
case .genericError(let error):
|
|
let title = OWSLocalizedString("SECONDARY_LINKING_ERROR_WAITING_FOR_SCAN", comment: "alert title")
|
|
let message = error.userErrorDescription
|
|
alert = ActionSheetController(title: title, message: message)
|
|
alert.addAction(ActionSheetAction(
|
|
title: CommonStrings.retryButton,
|
|
style: .default,
|
|
handler: { _ in
|
|
let isProvisioned = DependenciesBridge.shared.db.read { tx in
|
|
DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx).isRegistered
|
|
}
|
|
if isProvisioned {
|
|
self.provisioningDidComplete(from: viewController)
|
|
} else {
|
|
Task { @MainActor in
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
}
|
|
},
|
|
))
|
|
case .linkAndSyncError(let error):
|
|
return self.linkAndSyncRetryActionSheet(
|
|
error: error,
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
}
|
|
return alert
|
|
}
|
|
|
|
private func linkAndSyncRetryActionSheet(
|
|
error: ProvisioningCoordinatorImpl.LinkAndSyncError,
|
|
from viewController: ProvisioningQRCodeViewController,
|
|
navigationController: UINavigationController,
|
|
progressViewModel: LinkAndSyncSecondaryProgressViewModel,
|
|
) -> ActionSheetController {
|
|
enum ErrorPromptMode {
|
|
case contactSupport
|
|
case networkErrorRetry
|
|
case restartProvisioning
|
|
}
|
|
|
|
let errorPromptMode: ErrorPromptMode
|
|
let errorMessage: String?
|
|
if case SecondaryLinkNSyncError.errorRestoringBackup = error.error {
|
|
errorPromptMode = .contactSupport
|
|
errorMessage = nil
|
|
} else if error.error.isNetworkFailureOrTimeout {
|
|
errorPromptMode = .networkErrorRetry
|
|
errorMessage = OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_NETWORK_ERROR_MESSAGE",
|
|
comment: "Message for action sheet when secondary device fails to sync messages due to network error.",
|
|
)
|
|
} else if case BackupImportError.unsupportedVersion = error.error {
|
|
let actionSheet = ActionSheetController(
|
|
title: OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_UPDATE_REQUIRED_ERROR_TITLE",
|
|
comment: "Title for action sheet when the secondary device fails to sync messages due to an app update being required.",
|
|
),
|
|
message: OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_UPDATE_REQUIRED_ERROR_MESSAGE",
|
|
comment: "Message for action sheet when the secondary device fails to sync messages due to an app update being required.",
|
|
),
|
|
)
|
|
|
|
actionSheet.addAction(ActionSheetAction(
|
|
title: OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_UPDATE_REQUIRED_CHECK_FOR_UPDATE_BUTTON",
|
|
comment: "Button on an action sheet to open Signal on the App Store.",
|
|
),
|
|
style: .default,
|
|
) { _ in
|
|
UIApplication.shared.open(TSConstants.appStoreUrl)
|
|
Task { @MainActor in
|
|
// Crash if this fails; things have gone horribly wrong.
|
|
try! await error.restartProvisioning()
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
})
|
|
return actionSheet
|
|
} else {
|
|
errorPromptMode = .restartProvisioning
|
|
errorMessage = OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_OTHER_ERROR_MESSAGE",
|
|
comment: "Message for action sheet when secondary device fails to sync messages due to an unspecified error.",
|
|
)
|
|
}
|
|
|
|
let retryActionSheet = ActionSheetController(
|
|
title: OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_ERROR_TITLE",
|
|
comment: "Title for action sheet when secondary device fails to sync messages.",
|
|
),
|
|
message: errorMessage,
|
|
)
|
|
retryActionSheet.isCancelable = false
|
|
|
|
switch errorPromptMode {
|
|
case .contactSupport:
|
|
retryActionSheet.addAction(ActionSheetAction(title: CommonStrings.contactSupport) { _ in
|
|
Task { @MainActor in
|
|
// Crash if this fails; things have gone horribly wrong.
|
|
try! await error.restartProvisioning()
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
|
|
// Wait to present until we've reset back to the QR code
|
|
// view controller.
|
|
ContactSupportActionSheet.present(
|
|
emailFilter: .backupImportFailed,
|
|
logDumper: .fromGlobals(),
|
|
fromViewController: viewController,
|
|
)
|
|
}
|
|
})
|
|
case .networkErrorRetry:
|
|
retryActionSheet.addAction(ActionSheetAction(title: CommonStrings.retryButton) { _ in
|
|
self.performCoordinatorTaskWithModal(
|
|
task: Task {
|
|
try await error.retryLinkAndSync()
|
|
},
|
|
viewController: viewController,
|
|
navigationController: navigationController,
|
|
willLinkAndSync: true,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
})
|
|
case .restartProvisioning:
|
|
retryActionSheet.addAction(ActionSheetAction(title: CommonStrings.retryButton) { _ in
|
|
Task { @MainActor in
|
|
// Crash if this fails; things have gone horribly wrong.
|
|
try! await error.restartProvisioning()
|
|
await self.resetBackToQrCodeController(
|
|
from: viewController,
|
|
navigationController: navigationController,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
|
|
retryActionSheet.addAction(ActionSheetAction(
|
|
title: CommonStrings.cancelButton,
|
|
style: .cancel,
|
|
) { _ in
|
|
self.performCoordinatorTaskWithModal(
|
|
task: Task {
|
|
try await error.continueWithoutSyncing()
|
|
},
|
|
viewController: viewController,
|
|
navigationController: navigationController,
|
|
willLinkAndSync: false,
|
|
progressViewModel: progressViewModel,
|
|
)
|
|
})
|
|
|
|
return retryActionSheet
|
|
}
|
|
}
|
|
|
|
private extension CommonStrings {
|
|
static var linkNSyncImportErrorTitle: String {
|
|
OWSLocalizedString(
|
|
"SECONDARY_LINKING_SYNCING_ERROR_TITLE",
|
|
comment: "Title for action sheet when secondary device fails to sync messages.",
|
|
)
|
|
}
|
|
}
|