Add "Enable Optimize Storage" toggle to "Welcome to Backups" sheet

This commit is contained in:
Sasha Weiss 2026-05-29 15:49:10 -07:00 committed by GitHub
parent 39780d4bc7
commit 0206e8c487
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 180 additions and 108 deletions

View File

@ -147,7 +147,7 @@ class BackupSettingsViewController:
),
hasBackupFailed: backupFailureStateManager.hasFailedBackup(tx: tx),
isBackgroundAppRefreshDisabled: Self.isBackgroundAppRefreshDisabled(),
isOptimizeStorageEnabled: remoteConfig.currentConfig().isOptimizeStorageEnabled,
isOptimizeStorageRemoteConfigEnabled: remoteConfig.currentConfig().isOptimizeStorageEnabled,
)
return viewModel
@ -621,28 +621,88 @@ class BackupSettingsViewController:
final class WelcomeToBackupsSheet: HeroSheetViewController {
override var canBeDismissed: Bool { false }
init(onConfirm: @escaping () -> Void) {
init(
optimizeLocalStorage: (isOn: Bool, onValueChanged: (Bool) -> Void)?,
onConfirm: @escaping (HeroSheetViewController) -> Void,
) {
let toggle: HeroSheetViewController.Body.Toggle?
if let (isOn, onValueChanged) = optimizeLocalStorage {
toggle = HeroSheetViewController.Body.Toggle(
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_TITLE",
comment: "Title for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature.",
),
footer: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_FOOTER",
comment: "Footer for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature.",
),
isOn: isOn,
onValueChanged: onValueChanged,
)
} else {
toggle = nil
}
super.init(
hero: .image(.backupsSubscribed),
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_TITLE",
comment: "Title for a sheet shown after the user enables backups.",
),
body: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE",
comment: "Message for a sheet shown after the user enables backups.",
),
primaryButton: HeroSheetViewController.Button(
title: CommonStrings.okButton,
action: { _ in onConfirm() },
body: HeroSheetViewController.Body(
textContent: .plain(OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE",
comment: "Message for a sheet shown after the user enables backups.",
)),
toggle: toggle,
),
primary: .button(HeroSheetViewController.Button(
title: OWSLocalizedString(
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_BUTTON_TITLE",
comment: "Title for a button in a sheet shown after the user enables backups.",
),
action: { onConfirm($0) },
)),
secondary: nil,
)
}
}
let welcomeToBackupsSheet = WelcomeToBackupsSheet { [self] in
viewModel.performManualBackup()
dismiss(animated: true)
let backupPlan = db.read { tx in
backupPlanManager.backupPlan(tx: tx)
}
let welcomeToBackupsSheet: WelcomeToBackupsSheet
switch backupPlan {
case _ where !viewModel.isOptimizeStorageRemoteConfigEnabled,
.disabled,
.disabling,
.free:
welcomeToBackupsSheet = WelcomeToBackupsSheet(
optimizeLocalStorage: nil,
onConfirm: { sheet in
sheet.dismiss(animated: true) { [self] in
viewModel.performManualBackup()
}
},
)
case .paid,
.paidAsTester,
.paidExpiringSoon:
var isOptimizeStorageEnabled = true
welcomeToBackupsSheet = WelcomeToBackupsSheet(
optimizeLocalStorage: (
isOn: isOptimizeStorageEnabled,
onValueChanged: { isOptimizeStorageEnabled = $0 },
),
onConfirm: { sheet in
sheet.dismiss(animated: true) { [self] in
setOptimizeLocalStorage(isOptimizeStorageEnabled)
viewModel.performManualBackup()
}
},
)
}
present(welcomeToBackupsSheet, animated: true)
@ -1021,35 +1081,33 @@ class BackupSettingsViewController:
// MARK: -
fileprivate func setOptimizeLocalStorage(_ newOptimizeLocalStorage: Bool) {
let isPaidPlanTester: Bool = db.write { tx in
let hasMadeAtLeastOneBackup: Bool = db.write { tx in
let currentBackupPlan = backupPlanManager.backupPlan(tx: tx)
let newBackupPlan: BackupPlan
let isPaidPlanTester: Bool
let lastBackupDetails = backupSettingsStore.lastBackupDetails(tx: tx)
let newBackupPlan: BackupPlan
switch currentBackupPlan {
case .disabled, .disabling, .free:
owsFailDebug("Shouldn't be setting Optimize Local Storage: \(currentBackupPlan)")
return false
owsFail("Shouldn't be setting Optimize Local Storage: \(currentBackupPlan)")
case .paid:
newBackupPlan = .paid(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = false
case .paidExpiringSoon:
newBackupPlan = .paidExpiringSoon(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = false
case .paidAsTester:
newBackupPlan = .paidAsTester(optimizeLocalStorage: newOptimizeLocalStorage)
isPaidPlanTester = true
}
backupPlanManager.setBackupPlan(newBackupPlan, tx: tx)
return isPaidPlanTester
return lastBackupDetails != nil
}
// If disabling Optimize Local Storage, offer to start downloads now.
if !newOptimizeLocalStorage {
if
hasMadeAtLeastOneBackup,
!newOptimizeLocalStorage
{
// If disabling Optimize Local Storage with media potentially
// offloaded, offer to start downloads now.
showDownloadOffloadedMediaSheet()
} else if isPaidPlanTester {
showOffloadedMediaForTestersWarningSheet(onAcknowledge: {})
}
}
@ -1088,44 +1146,15 @@ class BackupSettingsViewController:
presentActionSheet(actionSheet)
}
private func showOffloadedMediaForTestersWarningSheet(
onAcknowledge: @escaping () -> Void,
) {
let actionSheet = ActionSheetController(
title: OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_TITLE",
comment: "Title for an action sheet warning users who are testers about the Optimize Local Storage feature.",
),
message: OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_MESSAGE",
comment: "Message for an action sheet warning users who are testers about the Optimize Local Storage feature.",
),
)
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.okButton,
handler: { _ in
onAcknowledge()
},
))
presentActionSheet(actionSheet)
}
// MARK: -
fileprivate func setIsBackupDownloadQueueSuspended(_ isSuspended: Bool, backupPlan: BackupPlan) {
if isSuspended {
switch backupPlan {
case .disabled, .disabling, .free, .paid:
case .disabled, .disabling, .free, .paid, .paidAsTester:
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
case .paidAsTester:
showOffloadedMediaForTestersWarningSheet(onAcknowledge: { [self] in
db.write { tx in
backupSettingsStore.setIsBackupDownloadQueueSuspended(true, tx: tx)
}
})
case .paidExpiringSoon:
let warningSheet = ActionSheetController(
title: OWSLocalizedString(
@ -1542,7 +1571,9 @@ private class BackupSettingsViewModel: ObservableObject {
/// from running.)
@Published var isBackgroundAppRefreshDisabled: Bool
@Published var isOptimizeStorageEnabled: Bool
/// Whether the "Optimze Storage" feature is available to this user, per
/// remote config. Not to be confused with `isOptimizeLocalStorageAvailable`.
@Published var isOptimizeStorageRemoteConfigEnabled: Bool
weak var actionsDelegate: ActionsDelegate?
@ -1560,7 +1591,7 @@ private class BackupSettingsViewModel: ObservableObject {
mediaTierCapacityOverflow: UInt64?,
hasBackupFailed: Bool,
isBackgroundAppRefreshDisabled: Bool,
isOptimizeStorageEnabled: Bool,
isOptimizeStorageRemoteConfigEnabled: Bool,
) {
self.backupSubscriptionConfiguration = backupSubscriptionConfiguration
@ -1581,7 +1612,7 @@ private class BackupSettingsViewModel: ObservableObject {
self.hasBackupFailed = hasBackupFailed
self.isBackgroundAppRefreshDisabled = isBackgroundAppRefreshDisabled
self.isOptimizeStorageEnabled = isOptimizeStorageEnabled
self.isOptimizeStorageRemoteConfigEnabled = isOptimizeStorageRemoteConfigEnabled
}
// MARK: -
@ -1640,7 +1671,9 @@ private class BackupSettingsViewModel: ObservableObject {
// MARK: -
var optimizeLocalStorageAvailable: Bool {
/// Whether the "Optimze Storage" feature is available, per the current
/// `BackupPlan`. Not to be confused with `isOptimizeStorageRemoteConfigEnabled`.
var isOptimizeLocalStorageAvailable: Bool {
switch backupPlan {
case .disabled, .disabling, .free:
false
@ -1649,7 +1682,7 @@ private class BackupSettingsViewModel: ObservableObject {
}
}
var optimizeLocalStorage: Bool {
var isOptimizeLocalStorageEnabled: Bool {
switch backupPlan {
case .disabled, .disabling, .free:
false
@ -1922,29 +1955,21 @@ struct BackupSettingsView: View {
viewModel: viewModel,
)
if viewModel.isOptimizeStorageEnabled {
if viewModel.isOptimizeStorageRemoteConfigEnabled {
Toggle(
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE",
comment: "Title for a toggle allowing users to change the Optimize Local Storage setting.",
),
isOn: Binding(
get: { viewModel.optimizeLocalStorage },
get: { viewModel.isOptimizeLocalStorageEnabled },
set: { viewModel.setOptimizeLocalStorage($0) },
),
).disabled(!viewModel.optimizeLocalStorageAvailable)
).disabled(!viewModel.isOptimizeLocalStorageAvailable)
}
} footer: {
if viewModel.isOptimizeStorageEnabled {
let footerText: String = if
viewModel.optimizeLocalStorageAvailable,
viewModel.isPaidPlanTester
{
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE_FOR_TESTERS",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available and they are a tester.",
)
} else if viewModel.optimizeLocalStorageAvailable {
if viewModel.isOptimizeStorageRemoteConfigEnabled {
let footerText: String = if viewModel.isOptimizeLocalStorageAvailable {
OWSLocalizedString(
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE",
comment: "Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available.",
@ -3178,7 +3203,7 @@ private extension BackupSettingsViewModel {
mediaTierCapacityOverflow: mediaTierCapacityOverflow,
hasBackupFailed: hasBackupFailed,
isBackgroundAppRefreshDisabled: isBackgroundAppRefreshDisabled,
isOptimizeStorageEnabled: false,
isOptimizeStorageRemoteConfigEnabled: true,
)
let actionsDelegate = PreviewActionsDelegate()
viewModel.actionsDelegate = actionsDelegate

View File

@ -988,23 +988,14 @@
/* Title for an action sheet allowing users to download their offloaded media. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_DOWNLOAD_SHEET_TITLE" = "Download Offloaded Media?";
/* Message for an action sheet warning users who are testers about the Optimize Local Storage feature. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_MESSAGE" = "Media that is offloaded will be available as long as you are using TestFlight. Make sure to download all your media before leaving the test program.";
/* Title for an action sheet warning users who are testers about the Optimize Local Storage feature. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TESTER_WARNING_SHEET_TITLE" = "Testing Offloaded Media";
/* Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE" = "Unused media will be offloaded, but can be downloaded from your backup anytime.";
/* Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is available and they are a tester. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE_FOR_TESTERS" = "Unused media will be offloaded, but can be downloaded from your backup anytime while you are using TestFlight.";
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_AVAILABLE" = "Older media will be offloaded, but can be downloaded from your backup anytime.";
/* Footer for a toggle allowing users to change the Optimize Local Storage setting, if the toggle is unavailable. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_FOOTER_UNAVAILABLE" = "Storage optimization can only be used with the paid tier of Signal Backups. Upgrade your backup plan to start using this feature.";
/* Title for a toggle allowing users to change the Optimize Local Storage setting. */
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE" = "Optimize On-Device Storage";
"BACKUP_SETTINGS_OPTIMIZE_LOCAL_STORAGE_TOGGLE_TITLE" = "Optimize Signal Storage";
/* Subtitle for a notification telling the user they are out of remote storage space. */
"BACKUP_SETTINGS_OUT_OF_STORAGE_SPACE_NOTIFICATION_SUBTITLE" = "Youve reached your backup storage limit. Free up space in Signal to continue backing up chats and media.";
@ -1060,11 +1051,20 @@
/* Subtitle for a progress bar tracking active uploading. */
"BACKUP_SETTINGS_UPLOAD_PROGRESS_SUBTITLE_RUNNING_GENERIC" = "Uploading…";
/* Title for a button in a sheet shown after the user enables backups. */
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_BUTTON_TITLE" = "Back Up Now";
/* Message for a sheet shown after the user enables backups. */
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE" = "Depending on the size of your backup, this could take a long time. You can use Signal as you normally do while the backup takes place.";
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_MESSAGE" = "This could take a while. You can use Signal normally while backing up.";
/* Footer for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature. */
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_FOOTER" = "Older media will be offloaded, but can be downloaded from your backup anytime.";
/* Title for a toggle shown after the user enables backups, letting them enable the Optimize Storage feature. */
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_OPTIMIZE_MEDIA_TOGGLE_TITLE" = "Optimize Signal Storage";
/* Title for a sheet shown after the user enables backups. */
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_TITLE" = "Welcome to Signal Secure Backups. Start your backup now.";
"BACKUP_SETTINGS_WELCOME_TO_BACKUPS_SHEET_TITLE" = "You're all set. Start your backup now.";
/* Message for a sheet shown when your Backup subscription fails to renew. */
"BACKUP_SUBSCRIPTION_FAILED_TO_RENEW_SHEET_MESSAGE" = "Check to make sure your payment method is up to date. Tap Manage Subscription > Signal > Update Payment Method.";

View File

@ -114,7 +114,6 @@ class BackupPlanManagerImpl: BackupPlanManager {
let oldBackupPlan = backupPlan(tx: tx)
guard oldBackupPlan != newBackupPlan else {
logger.warn("Attempting to set BackupPlan to existing value: aborting. \(oldBackupPlan)")
return
}

View File

@ -38,16 +38,19 @@ open class HeroSheetViewController: StackSheetViewController {
}
public struct Toggle {
public let text: String
public let title: String
public let footer: String?
public let isOn: Bool
public let onValueChanged: (_ isEnabled: Bool) -> Void
public init(
text: String,
title: String,
footer: String?,
isOn: Bool,
onValueChanged: @escaping (_ isEnabled: Bool) -> Void,
) {
self.text = text
self.title = title
self.footer = footer
self.isOn = isOn
self.onValueChanged = onValueChanged
}
@ -305,14 +308,15 @@ open class HeroSheetViewController: StackSheetViewController {
}
private func viewForToggle(_ toggle: Body.Toggle) -> UIView {
let label = UILabel()
label.text = toggle.text
label.font = .dynamicTypeSubheadline
label.textAlignment = .natural
label.numberOfLines = 0
label.textColor = .Signal.label
let titleLabel = UILabel()
titleLabel.text = toggle.title
titleLabel.font = .dynamicTypeSubheadline
titleLabel.textAlignment = .natural
titleLabel.numberOfLines = 0
titleLabel.textColor = .Signal.label
let toggleSwitch = UISwitch()
toggleSwitch.setCompressionResistanceHigh()
toggleSwitch.isOn = toggle.isOn
toggleSwitch.addAction(
UIAction { [weak self] action in
@ -327,19 +331,41 @@ open class HeroSheetViewController: StackSheetViewController {
for: .valueChanged,
)
let containerView = PillView()
containerView.backgroundColor = .Signal.tertiaryBackground
containerView.layoutMargins = UIEdgeInsets(hMargin: 20, vMargin: 16)
containerView.addSubview(label)
containerView.addSubview(toggleSwitch)
let pillView = PillView()
pillView.backgroundColor = .Signal.tertiaryBackground
pillView.layoutMargins = UIEdgeInsets(hMargin: 20, vMargin: 16)
pillView.addSubview(titleLabel)
pillView.addSubview(toggleSwitch)
label.autoPinEdges(toSuperviewMarginsExcludingEdge: .trailing)
titleLabel.autoPinEdges(toSuperviewMarginsExcludingEdge: .trailing)
toggleSwitch.autoPinEdge(.leading, to: .trailing, of: label, withOffset: 16, relation: .greaterThanOrEqual)
toggleSwitch.autoPinEdge(.leading, to: .trailing, of: titleLabel, withOffset: 16, relation: .greaterThanOrEqual)
toggleSwitch.autoPinEdge(toSuperviewMargin: .trailing)
toggleSwitch.autoVCenterInSuperview()
return containerView
if let footer = toggle.footer {
let footerLabel = UILabel()
footerLabel.text = footer
footerLabel.font = .dynamicTypeFootnote
footerLabel.textAlignment = .natural
footerLabel.numberOfLines = 0
footerLabel.textColor = .Signal.secondaryLabel
let pillAndFooterContainer = UIView()
pillAndFooterContainer.addSubview(pillView)
pillAndFooterContainer.addSubview(footerLabel)
pillView.autoPinEdges(toSuperviewEdgesExcludingEdge: .bottom)
footerLabel.autoPinEdge(.top, to: .bottom, of: pillView, withOffset: 8)
footerLabel.autoPinEdgesToSuperviewEdges(
with: UIEdgeInsets(hMargin: 20, vMargin: 0),
excludingEdge: .top,
)
return pillAndFooterContainer
} else {
return pillView
}
}
private func viewForElement(_ element: Element) -> UIView {
@ -413,14 +439,36 @@ open class HeroSheetViewController: StackSheetViewController {
}
@available(iOS 17, *)
#Preview("Body w/toggle") {
#Preview("Body w/toggle-and-footer") {
SheetPreviewViewController(sheet: HeroSheetViewController(
hero: .image(UIImage(named: "toggle-32")!),
title: nil,
title: "Feeding Boots the cat",
body: HeroSheetViewController.Body(
textContent: .plain(#"Give Boots extra dinner? He'd like you to know he's "extra hungry" tonight."#),
toggle: HeroSheetViewController.Body.Toggle(
text: "Extra Food?",
title: "Extra dinner?",
footer: "Side effects may include sleepiness and increased insistence that he receive extra food in the future.",
isOn: true,
onValueChanged: { enabled in
print(enabled ? "😸" : "😾")
},
),
),
primary: .button(.dismissing(title: "Order Up")),
secondary: nil,
))
}
@available(iOS 17, *)
#Preview("Body w/long-text toggle") {
SheetPreviewViewController(sheet: HeroSheetViewController(
hero: .image(UIImage(named: "toggle-32")!),
title: "Feeding Boots the Cat",
body: HeroSheetViewController.Body(
textContent: .plain(#"Give Boots extra dinner? He'd like you to know he's "extra hungry" tonight."#),
toggle: HeroSheetViewController.Body.Toggle(
title: "Give Boots extra dinner? Side effects may include sleepiness and increased insistence that he receive extra food in the future.",
footer: nil,
isOn: true,
onValueChanged: { enabled in
print(enabled ? "😸" : "😾")