From 60d7270f807cbe2f7cf04ca7c100fb3d2e20d12b Mon Sep 17 00:00:00 2001 From: Pete Walters Date: Thu, 8 Jan 2026 09:59:27 -0600 Subject: [PATCH] Add outgoing device prompt if backup too old --- Signal.xcodeproj/project.pbxproj | 22 +++- Signal/AppLaunch/AppEnvironment.swift | 3 + .../BackupSettingsViewController.swift | 12 +- .../BackupOnboardingCoordinator.swift | 5 +- .../BackupEnablementMegaphone.swift | 2 +- .../BackupsEnabledNotificationMegaphone.swift | 2 +- .../NotificationActionHandler.swift | 2 +- ...iceRestoreBackupPromptViewController.swift | 124 ++++++++++++++++++ ...ngDeviceRestoreInitialViewController.swift | 0 .../OutgoingDeviceRestorePresenter.swift | 105 +++++++++++++++ ...gDeviceRestoreProgressViewController.swift | 0 .../OutgoingDeviceRestoreViewModel.swift | 0 .../QuickRestoreManager.swift | 0 .../ChatListFYISheetCoordinator.swift | 4 +- ...ontroller+BackupDownloadProgressView.swift | 2 +- .../Chat List/ChatListViewController.swift | 17 ++- .../translations/en.lproj/Localizable.strings | 24 ++++ 17 files changed, 303 insertions(+), 21 deletions(-) create mode 100644 Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift rename Signal/{DeviceTransfer => QuickRestore}/OutgoingDeviceRestoreInitialViewController.swift (100%) rename Signal/{DeviceTransfer => QuickRestore}/OutgoingDeviceRestorePresenter.swift (73%) rename Signal/{DeviceTransfer => QuickRestore}/OutgoingDeviceRestoreProgressViewController.swift (100%) rename Signal/{DeviceTransfer => QuickRestore}/OutgoingDeviceRestoreViewModel.swift (100%) rename Signal/{Provisioning => QuickRestore}/QuickRestoreManager.swift (100%) diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8ffd939eaa..45382ad0fc 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -1789,6 +1789,7 @@ C190F8F52C1B47E100D1EAC9 /* OWSOutgoingArchivedPaymentMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C190F8F22C1B47E100D1EAC9 /* OWSOutgoingArchivedPaymentMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; C190F8F72C1B48BE00D1EAC9 /* OWSArchivedPaymentMessage.h in Headers */ = {isa = PBXBuildFile; fileRef = C190F8F62C1B484A00D1EAC9 /* OWSArchivedPaymentMessage.h */; settings = {ATTRIBUTES = (Public, ); }; }; C1939F6F2A844E4D003BAEF0 /* SignalProtocolStoreMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1ED5C9E2A72DFC9009AD3FC /* SignalProtocolStoreMocks.swift */; }; + C197187D2EF9D28C002E4198 /* OutgoingDeviceRestoreBackupPromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C197187C2EF9D27E002E4198 /* OutgoingDeviceRestoreBackupPromptViewController.swift */; }; C198FDD62A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C198FDD52A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift */; }; C1A0F79D2B9F57340009DC0D /* MessageRootBackupKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A0F79C2B9F57340009DC0D /* MessageRootBackupKey.swift */; }; C1A136C32DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1A136C22DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift */; }; @@ -5943,6 +5944,7 @@ C190F8F22C1B47E100D1EAC9 /* OWSOutgoingArchivedPaymentMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSOutgoingArchivedPaymentMessage.h; sourceTree = ""; }; C190F8F32C1B47E100D1EAC9 /* OWSOutgoingArchivedPaymentMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingArchivedPaymentMessage.m; sourceTree = ""; }; C190F8F62C1B484A00D1EAC9 /* OWSArchivedPaymentMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSArchivedPaymentMessage.h; sourceTree = ""; }; + C197187C2EF9D27E002E4198 /* OutgoingDeviceRestoreBackupPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingDeviceRestoreBackupPromptViewController.swift; sourceTree = ""; }; C198FDD52A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KyberPreKeyStoreImpl.swift; sourceTree = ""; }; C1A0F79C2B9F57340009DC0D /* MessageRootBackupKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRootBackupKey.swift; sourceTree = ""; }; C1A136C22DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingDeviceRestorePresenter.swift; sourceTree = ""; }; @@ -10569,7 +10571,6 @@ C17B31582D710DD80060664D /* ProvisioningManager+Shims.swift */, C17B31542D710DBD0060664D /* ProvisioningManager.swift */, C18087892D76023400B16D1E /* ProvisioningSocketManager.swift */, - C1AE7C592D7B4451007A618D /* QuickRestoreManager.swift */, ); path = Provisioning; sourceTree = ""; @@ -11335,10 +11336,6 @@ 887CD47A247304B600FDD265 /* DeviceTransferService+URL.swift */, 88C4E37F24635337009C9B97 /* DeviceTransferService.swift */, C147C1712D9C58D60026952D /* DeviceTransferStatusViewController.swift */, - C14D475F2DAEC45C006178AC /* OutgoingDeviceRestoreInitialViewController.swift */, - C1A136C22DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift */, - C14D47632DAEE351006178AC /* OutgoingDeviceRestoreProgressViewController.swift */, - C1A136C42DB044A20049CD05 /* OutgoingDeviceRestoreViewModel.swift */, 88C659AF24688335002AC115 /* SelfSignedIdentity.swift */, C1868F812DBAD04400DA512A /* TransferStatusState.swift */, ); @@ -11691,6 +11688,19 @@ path = Upload; sourceTree = ""; }; + C1F9F7D62F0FFFCD00FF3688 /* QuickRestore */ = { + isa = PBXGroup; + children = ( + C197187C2EF9D27E002E4198 /* OutgoingDeviceRestoreBackupPromptViewController.swift */, + C14D475F2DAEC45C006178AC /* OutgoingDeviceRestoreInitialViewController.swift */, + C1A136C22DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift */, + C14D47632DAEE351006178AC /* OutgoingDeviceRestoreProgressViewController.swift */, + C1A136C42DB044A20049CD05 /* OutgoingDeviceRestoreViewModel.swift */, + C1AE7C592D7B4451007A618D /* QuickRestoreManager.swift */, + ); + path = QuickRestore; + sourceTree = ""; + }; D221A07E169C9E5E00537ABF = { isa = PBXGroup; children = ( @@ -11793,6 +11803,7 @@ 50423CA22BBF426700DCB8F5 /* Profiles */, 66CDB7532AFC3EFB009A36EC /* Provisioning */, D9DCFDAC2A3BB22800C73C0B /* QRCodes */, + C1F9F7D62F0FFFCD00FF3688 /* QuickRestore */, 6600F38C29918A5100B1EDB7 /* Registration */, 50BF51062BB201AE00C2C309 /* Sharing */, 34074F54203D0722004596AE /* Sounds */, @@ -18045,6 +18056,7 @@ 887B380A25F0427F00685845 /* NotificationSettingsViewController.swift in Sources */, F9CA468828FF0CA600C074F6 /* OneTimeDonationCustomAmountTextField.swift in Sources */, F9952B2F29F1E59F00EA989E /* OsExpiry.swift in Sources */, + C197187D2EF9D28C002E4198 /* OutgoingDeviceRestoreBackupPromptViewController.swift in Sources */, C14D47602DAEC45C006178AC /* OutgoingDeviceRestoreInitialViewController.swift in Sources */, C1A136C32DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift in Sources */, C14D47642DAEE351006178AC /* OutgoingDeviceRestoreProgressViewController.swift in Sources */, diff --git a/Signal/AppLaunch/AppEnvironment.swift b/Signal/AppLaunch/AppEnvironment.swift index 17d346a4d2..cb939cbead 100644 --- a/Signal/AppLaunch/AppEnvironment.swift +++ b/Signal/AppLaunch/AppEnvironment.swift @@ -122,6 +122,9 @@ public class AppEnvironment: NSObject { ) self.outgoingDeviceRestorePresenter = OutgoingDeviceRestorePresenter( + dateProvider: Date.provider, + db: DependenciesBridge.shared.db, + backupSettingsStore: BackupSettingsStore(), deviceTransferService: deviceTransferServiceRef, quickRestoreManager: quickRestoreManager, ) diff --git a/Signal/Backups/BackupSettingsViewController.swift b/Signal/Backups/BackupSettingsViewController.swift index 81ddcee25e..90e1f81cf7 100644 --- a/Signal/Backups/BackupSettingsViewController.swift +++ b/Signal/Backups/BackupSettingsViewController.swift @@ -15,6 +15,7 @@ class BackupSettingsViewController: { enum OnAppearAction { case presentWelcomeToBackupsSheet + case automaticallyStartBackup(completion: ((UIViewController) -> Void)?) } private let accountEntropyPoolManager: AccountEntropyPoolManager @@ -173,11 +174,13 @@ class BackupSettingsViewController: override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - switch onAppearAction.take() { + switch onAppearAction { case nil: break case .presentWelcomeToBackupsSheet: presentWelcomeToBackupsSheet() + case .automaticallyStartBackup: + performManualBackup() } } @@ -244,7 +247,12 @@ class BackupSettingsViewController: switch result { case .success: - break + switch onAppearAction { + case .presentWelcomeToBackupsSheet, nil: + break + case .automaticallyStartBackup(let completion): + completion?(self) + } case .failure(let error): showSheetForBackupExportJobError(error) } diff --git a/Signal/Backups/Onboarding/BackupOnboardingCoordinator.swift b/Signal/Backups/Onboarding/BackupOnboardingCoordinator.swift index ab6022bd32..c513dc3d09 100644 --- a/Signal/Backups/Onboarding/BackupOnboardingCoordinator.swift +++ b/Signal/Backups/Onboarding/BackupOnboardingCoordinator.swift @@ -46,15 +46,18 @@ class BackupOnboardingCoordinator { self.db = db } + /// - Parameter onAppearAction + /// An on-appear action for Backup Settings, if onboarding is not necessary. func prepareForPresentation( inNavController navController: UINavigationController, + onAppearAction: BackupSettingsViewController.OnAppearAction? = nil, ) -> UIViewController { let haveBackupsEverBeenEnabled = db.read { tx in backupSettingsStore.haveBackupsEverBeenEnabled(tx: tx) } if haveBackupsEverBeenEnabled { - return BackupSettingsViewController(onAppearAction: nil) + return BackupSettingsViewController(onAppearAction: onAppearAction) } else { // Weakly retain the nav controller, so we can use it throughout // onboarding. diff --git a/Signal/Megaphones/UserInterface/BackupEnablementMegaphone.swift b/Signal/Megaphones/UserInterface/BackupEnablementMegaphone.swift index 9a78e7c15c..f017ac0474 100644 --- a/Signal/Megaphones/UserInterface/BackupEnablementMegaphone.swift +++ b/Signal/Megaphones/UserInterface/BackupEnablementMegaphone.swift @@ -33,7 +33,7 @@ class BackupEnablementMegaphone: MegaphoneView { ) let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) self?.markAsSnoozedWithSneakyTransaction() self?.dismiss(animated: true) } diff --git a/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift b/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift index 0dd521ec9f..a8b14d0367 100644 --- a/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift +++ b/Signal/Megaphones/UserInterface/BackupsEnabledNotificationMegaphone.swift @@ -40,7 +40,7 @@ class BackupsEnabledNotificationMegaphone: MegaphoneView { comment: "Action text for backups enabled megaphone taking user to backup settings", ) let primaryButton = MegaphoneView.Button(title: primaryButtonTitle) { [weak self] in - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) self?.markAsViewed() self?.dismiss(animated: true) } diff --git a/Signal/Notifications/NotificationActionHandler.swift b/Signal/Notifications/NotificationActionHandler.swift index b2fdf18944..c434ce9d05 100644 --- a/Signal/Notifications/NotificationActionHandler.swift +++ b/Signal/Notifications/NotificationActionHandler.swift @@ -376,7 +376,7 @@ public class NotificationActionHandler { @MainActor private class func showBackupsSettings() { - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) } private struct NotificationMessage { diff --git a/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift b/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift new file mode 100644 index 0000000000..6623eafe99 --- /dev/null +++ b/Signal/QuickRestore/OutgoingDeviceRestoreBackupPromptViewController.swift @@ -0,0 +1,124 @@ +// +// Copyright 2025 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +// + +import Foundation +import SignalServiceKit +import SignalUI +import SwiftUI + +class OutgoingDeviceRestoreBackupPromptViewController: HostingController { + init( + lastBackupDetails: BackupSettingsStore.LastBackupDetails, + makeBackupCallback: @escaping (Bool) -> Void, + ) { + super.init(wrappedView: OutgoingDeviceRestoreBackupPromptView( + lastBackupDetails: lastBackupDetails, + makeBackupCallback: makeBackupCallback, + )) + self.modalPresentationStyle = .overFullScreen + self.title = OWSLocalizedString( + "OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_INITIAL_VIEW_TITLE", + comment: "Title text describing the outgoing transfer.", + ) + self.navigationItem.leftBarButtonItem = .cancelButton(dismissingFrom: self) + view.backgroundColor = UIColor.Signal.secondaryBackground + OWSTableViewController2.removeBackButtonText(viewController: self) + } +} + +struct OutgoingDeviceRestoreBackupPromptView: View { + private let lastBackupDetails: BackupSettingsStore.LastBackupDetails + private let makeBackupCallback: (Bool) -> Void + init( + lastBackupDetails: BackupSettingsStore.LastBackupDetails, + makeBackupCallback: @escaping (Bool) -> Void, + ) { + self.lastBackupDetails = lastBackupDetails + self.makeBackupCallback = makeBackupCallback + } + + var body: some View { + SignalList { + SignalSection { + VStack(alignment: .center, spacing: 24) { + Text(OWSLocalizedString( + "OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_INITIAL_VIEW_BODY", + comment: "Body text describing the outgoing transfer.", + )) + .font(.subheadline) + .foregroundStyle(Color.Signal.secondaryLabel) + .tint(Color.Signal.label) + + Image(.transferAccount) + + Text(lastBackupDetailsString()) + .font(.subheadline) + .foregroundStyle(Color.Signal.secondaryLabel) + .tint(Color.Signal.label) + + Button(OWSLocalizedString( + "OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_BACKUP_ACTION", + comment: "Action button to backup before continuing.", + )) { + self.makeBackupCallback(true) + } + .buttonStyle(Registration.UI.LargePrimaryButtonStyle()) + + Button(OWSLocalizedString( + "OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_SKIP_ACTION", + comment: "Action button to skip backup and continue.", + )) { + self.makeBackupCallback(false) + } + .buttonStyle(Registration.UI.LargeSecondaryButtonStyle()) + }.padding([.top, .bottom], 12) + } + footer: { + let footerString = OWSLocalizedString( + "OUTGOING_DEVICE_RESTORE_INITIAL_VIEW_FOOTER", + comment: "Body text describing the outgoing transfer.", + ) + Text("\(SignalSymbol.lock.text(dynamicTypeBaseSize: 14)) \(footerString)") + .font(.footnote) + .foregroundStyle(Color.Signal.secondaryLabel) + .padding([.top, .bottom], 12) + } + } + .scrollBounceBehaviorIfAvailable(.basedOnSize) + .multilineTextAlignment(.center) + } + + private func lastBackupDetailsString() -> String { + let date = lastBackupDetails.date + return String( + format: OWSLocalizedString( + "OUTGOING_DEVICE_RESTORE_BACKUP_RESTORE_DESCRIPTION", + comment: "Description for form confirming restore from backup without size detail.", + ), + DateUtil.dateFormatter.string(for: date) ?? "", + DateUtil.timeFormatter.string(for: date) ?? "", + ) + } +} + +// MARK: Previews + +#if DEBUG +@available(iOS 17, *) +#Preview { + OWSNavigationController( + rootViewController: OutgoingDeviceRestoreBackupPromptViewController( + lastBackupDetails: .init( + date: Date(), + backupFileSizeBytes: 1024, + backupTotalSizeBytes: 4096, + ), + makeBackupCallback: { + print("Should do backup? \($0)") + }, + ), + ) +} +#endif diff --git a/Signal/DeviceTransfer/OutgoingDeviceRestoreInitialViewController.swift b/Signal/QuickRestore/OutgoingDeviceRestoreInitialViewController.swift similarity index 100% rename from Signal/DeviceTransfer/OutgoingDeviceRestoreInitialViewController.swift rename to Signal/QuickRestore/OutgoingDeviceRestoreInitialViewController.swift diff --git a/Signal/DeviceTransfer/OutgoingDeviceRestorePresenter.swift b/Signal/QuickRestore/OutgoingDeviceRestorePresenter.swift similarity index 73% rename from Signal/DeviceTransfer/OutgoingDeviceRestorePresenter.swift rename to Signal/QuickRestore/OutgoingDeviceRestorePresenter.swift index 32b8867419..716c8970fe 100644 --- a/Signal/DeviceTransfer/OutgoingDeviceRestorePresenter.swift +++ b/Signal/QuickRestore/OutgoingDeviceRestorePresenter.swift @@ -17,7 +17,14 @@ extension Notification.Name { class OutgoingDeviceRestorePresenter: OutgoingDeviceRestoreInitialPresenter { + private enum Constants { + static let lastBackupAgeThreshold: TimeInterval = 30 * .minute + } + private let internalNavigationController = OWSNavigationController() + private let dateProvider: DateProvider + private let db: DB + private let backupSettingsStore: BackupSettingsStore private let deviceTransferService: DeviceTransferService private let quickRestoreManager: QuickRestoreManager @@ -25,9 +32,15 @@ class OutgoingDeviceRestorePresenter: OutgoingDeviceRestoreInitialPresenter { private var presentingViewController: UIViewController? init( + dateProvider: @escaping DateProvider, + db: DB, + backupSettingsStore: BackupSettingsStore, deviceTransferService: DeviceTransferService, quickRestoreManager: QuickRestoreManager, ) { + self.dateProvider = dateProvider + self.db = db + self.backupSettingsStore = backupSettingsStore self.deviceTransferService = deviceTransferService self.quickRestoreManager = quickRestoreManager } @@ -82,6 +95,44 @@ class OutgoingDeviceRestorePresenter: OutgoingDeviceRestoreInitialPresenter { ) } + @MainActor + private func pushBackupPropmtViewController(presentingViewController: UIViewController) async -> Bool { + + let ( + backupPlan, + lastBackupDetails, + ) = db.read { ( + backupSettingsStore.backupPlan(tx: $0), + backupSettingsStore.lastBackupDetails(tx: $0), + ) } + + switch backupPlan { + case .disabled, .disabling: return false + case .free, .paid, .paidAsTester, .paidExpiringSoon: break + } + + guard let lastBackupDetails else { + owsFailDebug("Failed to load last backup details") + return false + } + + if dateProvider().timeIntervalSince(lastBackupDetails.date) < Constants.lastBackupAgeThreshold { + return false + } + + return await withCheckedContinuation { continuation in + Task { + await internalNavigationController.awaitablePush( + OutgoingDeviceRestoreBackupPromptViewController( + lastBackupDetails: lastBackupDetails, + makeBackupCallback: { continuation.resume(returning: $0) }, + ), + animated: true, + ) + } + } + } + @MainActor private func displayTransferComplete(presentingViewController: UIViewController) async { let sheet = HeroSheetViewController( @@ -158,6 +209,25 @@ class OutgoingDeviceRestorePresenter: OutgoingDeviceRestoreInitialPresenter { return } + if await pushBackupPropmtViewController(presentingViewController: presentingViewController) { + await internalNavigationController.dismiss(animated: true) + Task { @MainActor in + SignalApp.shared.showAppSettings( + mode: .backups( + onAppearAction: .automaticallyStartBackup( + completion: { [weak self] backupSettingsVC in + guard let self else { return } + showRestoreReturnSheetAfterBackup( + presentingViewController: backupSettingsVC, + ) + }, + ), + ), + ) + } + return + } + // Show a sheet while fetching the transfer data await presentSheet() let restoreMethodData = try await viewModel.waitForRestoreMethodResponse() @@ -266,4 +336,39 @@ class OutgoingDeviceRestorePresenter: OutgoingDeviceRestoreInitialPresenter { await presentingViewController.awaitableDismiss(animated: true) await presentingViewController.awaitablePresent(sheet, animated: true) } + + private func showRestoreReturnSheetAfterBackup( + presentingViewController: UIViewController?, + ) { + let returnSheet = HeroSheetViewController( + hero: .image(.transferAccount), + title: OWSLocalizedString( + "BACKUP_SETTINGS_BACKUP_EXPORT_SUCCEEDED_READY_TO_RESTORE_TITLE", + comment: "Title for an action sheet explaining the backup succeeded and a restore can continue.", + ), + body: OWSLocalizedString( + "BACKUP_SETTINGS_BACKUP_EXPORT_SUCCEEDED_READY_TO_RESTORE_BODY", + comment: "Body for an action sheet explaining the backup succeeded and a restore can continue.", + ), + primary: .button(HeroSheetViewController.Button( + title: OWSLocalizedString( + "BACKUP_SETTINGS_BACKUP_EXPORT_SUCCEEDED_READY_TO_RESTORE_ACTION_TITLE", + comment: "Title for an action sheet action explaining the user can now scan a QR code to continue the restore.", + ), + action: { sheet in + sheet.dismiss(animated: true) { + presentingViewController?.dismiss(animated: true) { + SignalApp.shared.showCameraCaptureView() + } + } + }, + )), + secondary: .button(.dismissing( + title: CommonStrings.cancelButton, + style: .secondary, + )), + ) + + presentingViewController?.present(returnSheet, animated: true) + } } diff --git a/Signal/DeviceTransfer/OutgoingDeviceRestoreProgressViewController.swift b/Signal/QuickRestore/OutgoingDeviceRestoreProgressViewController.swift similarity index 100% rename from Signal/DeviceTransfer/OutgoingDeviceRestoreProgressViewController.swift rename to Signal/QuickRestore/OutgoingDeviceRestoreProgressViewController.swift diff --git a/Signal/DeviceTransfer/OutgoingDeviceRestoreViewModel.swift b/Signal/QuickRestore/OutgoingDeviceRestoreViewModel.swift similarity index 100% rename from Signal/DeviceTransfer/OutgoingDeviceRestoreViewModel.swift rename to Signal/QuickRestore/OutgoingDeviceRestoreViewModel.swift diff --git a/Signal/Provisioning/QuickRestoreManager.swift b/Signal/QuickRestore/QuickRestoreManager.swift similarity index 100% rename from Signal/Provisioning/QuickRestoreManager.swift rename to Signal/QuickRestore/QuickRestoreManager.swift diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift index 256cc5e0c4..3b80bac7c9 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListFYISheetCoordinator.swift @@ -395,7 +395,7 @@ class ChatListFYISheetCoordinator { let sheet = BackupSubscriptionExpiredHeroSheet( subscriptionType: backupSubscriptionExpired.subscriptionType, onManageBackups: { - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) }, ) chatListViewController.present(sheet, animated: true) { [self] in @@ -423,7 +423,7 @@ class ChatListFYISheetCoordinator { let sheet = BackupSubscriptionFailedToRenewHeroSheet( onManageSubscription: { - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) }, ) chatListViewController.present(sheet, animated: true) { [self] in diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift index d8a2848b33..d389d17363 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController+BackupDownloadProgressView.swift @@ -225,7 +225,7 @@ extension ChatListViewController { } if isPrimaryDevice { - showAppSettings(mode: .backups) + showAppSettings(mode: .backups()) } else { showCancelBackupDownloadsHeroSheet() } diff --git a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift index 76505e9e95..20b5a9afed 100644 --- a/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift +++ b/Signal/src/ViewControllers/HomeView/Chat List/ChatListViewController.swift @@ -522,7 +522,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { ), image: image, handler: { [weak self] _ in - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) db.write { tx in backupSettingsStore.setErrorBadgeMuted(target: .chatListMenuItem, tx: tx) } @@ -545,7 +545,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { ), image: image, handler: { [weak self] _ in - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) db.write { tx in backupSubscriptionIssueStore.setDidAckIAPSubscriptionAlreadyRedeemedChatListMenuItem(tx: tx) } @@ -571,7 +571,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { ), image: image, handler: { [weak self] _ in - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) db.write { tx in backupSubscriptionIssueStore.setDidAckIAPSubscriptionNotFoundLocallyChatListMenuItem(tx: tx) } @@ -594,7 +594,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController { ), image: image, handler: { _ in - SignalApp.shared.showAppSettings(mode: .backups) + SignalApp.shared.showAppSettings(mode: .backups()) }, ), ]), @@ -1355,7 +1355,7 @@ extension ChatListViewController { case paymentsTransferIn case appearance case avatarBuilder - case backups + case backups(onAppearAction: BackupSettingsViewController.OnAppearAction? = nil) case corruptedUsernameResolution case corruptedUsernameLinkResolution case donate(donateMode: DonateViewController.DonateMode) @@ -1408,10 +1408,13 @@ extension ChatListViewController { viewControllers += [profile] internalCompletion = { profile.presentAvatarSettingsView() } - case .backups: + case .backups(let onAppearAction): viewControllers += [ BackupOnboardingCoordinator() - .prepareForPresentation(inNavController: navigationController), + .prepareForPresentation( + inNavController: navigationController, + onAppearAction: onAppearAction, + ), ] case .corruptedUsernameResolution: diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index af84cfd41e..b60cd64e9c 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -700,6 +700,15 @@ /* Description for a progress bar tracking the processing of Backup media. */ "BACKUP_SETTINGS_BACKUP_EXPORT_PROGRESS_DESCRIPTION_PROCESSING_MEDIA" = "Processing media..."; +/* Title for an action sheet action explaining the user can now scan a QR code to continue the restore. */ +"BACKUP_SETTINGS_BACKUP_EXPORT_SUCCEEDED_READY_TO_RESTORE_ACTION_TITLE" = "Scan QR Code"; + +/* Body for an action sheet explaining the backup succeeded and a restore can continue. */ +"BACKUP_SETTINGS_BACKUP_EXPORT_SUCCEEDED_READY_TO_RESTORE_BODY" = "Use this device to scan the QR code on the device you want to transfer to"; + +/* Title for an action sheet explaining the backup succeeded and a restore can continue. */ +"BACKUP_SETTINGS_BACKUP_EXPORT_SUCCEEDED_READY_TO_RESTORE_TITLE" = "Ready to Transfer"; + /* Message describing to the user that the last backup failed. */ "BACKUP_SETTINGS_BACKUP_FAILED_MESSAGE" = "Your last backup couldn't be completed. Tap \"Back Up Now\" to try again."; @@ -5914,6 +5923,21 @@ /* Title of prompt notifying restore failed for unknown reasons. */ "OUTGOING_DEVICE_REGISTRATION_UNKNOWN_ERROR_TITLE" = "Unknown error"; +/* Action button to backup before continuing. */ +"OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_BACKUP_ACTION" = "Backup Up Now"; + +/* Body text describing the outgoing transfer. */ +"OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_INITIAL_VIEW_BODY" = "Back up now before transferring. You may have received messages that haven’t been backed up yet."; + +/* Title text describing the outgoing transfer. */ +"OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_INITIAL_VIEW_TITLE" = "Getting Your Device Ready"; + +/* Action button to skip backup and continue. */ +"OUTGOING_DEVICE_RESTORE_BACKUP_PROMPT_SKIP_ACTION" = "Skip and Continue"; + +/* Description for form confirming restore from backup without size detail. */ +"OUTGOING_DEVICE_RESTORE_BACKUP_RESTORE_DESCRIPTION" = "Your last backup was made on %1$@ at %2$@."; + /* Body of prompt notifying device restore started on the new device. */ "OUTGOING_DEVICE_RESTORE_COMPLETE_BODY" = "Your Signal account and messages have started transferring to your other device. Signal is now inactive on this device.";