Add outgoing device prompt if backup too old

This commit is contained in:
Pete Walters 2026-01-08 09:59:27 -06:00 committed by GitHub
parent 4171b2fd07
commit 60d7270f80
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 303 additions and 21 deletions

View File

@ -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 = "<group>"; };
C190F8F32C1B47E100D1EAC9 /* OWSOutgoingArchivedPaymentMessage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OWSOutgoingArchivedPaymentMessage.m; sourceTree = "<group>"; };
C190F8F62C1B484A00D1EAC9 /* OWSArchivedPaymentMessage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSArchivedPaymentMessage.h; sourceTree = "<group>"; };
C197187C2EF9D27E002E4198 /* OutgoingDeviceRestoreBackupPromptViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingDeviceRestoreBackupPromptViewController.swift; sourceTree = "<group>"; };
C198FDD52A37C905000BCAC9 /* KyberPreKeyStoreImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KyberPreKeyStoreImpl.swift; sourceTree = "<group>"; };
C1A0F79C2B9F57340009DC0D /* MessageRootBackupKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRootBackupKey.swift; sourceTree = "<group>"; };
C1A136C22DB044950049CD05 /* OutgoingDeviceRestorePresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutgoingDeviceRestorePresenter.swift; sourceTree = "<group>"; };
@ -10569,7 +10571,6 @@
C17B31582D710DD80060664D /* ProvisioningManager+Shims.swift */,
C17B31542D710DBD0060664D /* ProvisioningManager.swift */,
C18087892D76023400B16D1E /* ProvisioningSocketManager.swift */,
C1AE7C592D7B4451007A618D /* QuickRestoreManager.swift */,
);
path = Provisioning;
sourceTree = "<group>";
@ -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 = "<group>";
};
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 = "<group>";
};
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 */,

View File

@ -122,6 +122,9 @@ public class AppEnvironment: NSObject {
)
self.outgoingDeviceRestorePresenter = OutgoingDeviceRestorePresenter(
dateProvider: Date.provider,
db: DependenciesBridge.shared.db,
backupSettingsStore: BackupSettingsStore(),
deviceTransferService: deviceTransferServiceRef,
quickRestoreManager: quickRestoreManager,
)

View File

@ -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)
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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<OutgoingDeviceRestoreBackupPromptView> {
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

View File

@ -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)
}
}

View File

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

View File

@ -225,7 +225,7 @@ extension ChatListViewController {
}
if isPrimaryDevice {
showAppSettings(mode: .backups)
showAppSettings(mode: .backups())
} else {
showCancelBackupDownloadsHeroSheet()
}

View File

@ -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:

View File

@ -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 havent 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.";