diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt
index bd77ff0ba1..e289a2bd13 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt
@@ -142,7 +142,7 @@ object BackupRepository {
Log.w(TAG, "Received status 403. The user is not in the media tier. Updating local state.", error.exception)
SignalStore.backup.backupTier = MessageBackupTier.FREE
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
- // TODO [backup] If the user thought they were in media tier but aren't, feels like we should have a special UX flow for this?
+ SignalStore.backup.backupExpiredAndDowngraded = true
}
}
}
@@ -264,6 +264,18 @@ object BackupRepository {
return SignalStore.backup.hasBackupBeenUploaded && SignalStore.backup.hasBackupFailure
}
+ /**
+ * Displayed when the user falls out of the grace period for backups after their subscription
+ * expires.
+ */
+ fun shouldDisplayBackupExpiredAndDowngradedSheet(): Boolean {
+ if (shouldNotDisplayBackupFailedMessaging()) {
+ return false
+ }
+
+ return SignalStore.backup.backupExpiredAndDowngraded
+ }
+
fun markBackupAlreadyRedeemedIndicatorClicked() {
SignalStore.backup.hasBackupAlreadyRedeemedError = false
}
@@ -283,6 +295,13 @@ object BackupRepository {
SignalStore.backup.updateMessageBackupFailureSheetWatermark()
}
+ /**
+ * User closed backup expiration alert sheet
+ */
+ fun markBackupExpiredAndDowngradedSheetDismissed() {
+ SignalStore.backup.backupExpiredAndDowngraded = false
+ }
+
/**
* Whether or not the "Backup failed" sheet should be displayed.
* Should only be displayed if this is the failure of the initial backup creation.
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt
index 5d1d7833f4..d8dc25167f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertBottomSheet.kt
@@ -119,6 +119,9 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
BackupAlert.CouldNotRedeemBackup -> Unit
+ BackupAlert.ExpiredAndDowngraded -> {
+ startActivity(AppSettingsActivity.remoteBackups(requireContext()))
+ }
}
dismissAllowingStateLoss()
@@ -127,6 +130,7 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
@Stable
private fun performSecondaryAction() {
when (backupAlert) {
+ BackupAlert.ExpiredAndDowngraded -> Unit
is BackupAlert.CouldNotCompleteBackup -> Unit
BackupAlert.FailedToRenew -> Unit
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
@@ -150,6 +154,7 @@ class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed -> BackupRepository.markBackupFailedSheetDismissed()
is BackupAlert.MediaWillBeDeletedToday -> BackupRepository.snoozeYourMediaWillBeDeletedTodaySheet()
+ is BackupAlert.ExpiredAndDowngraded -> BackupRepository.markBackupExpiredAndDowngradedSheetDismissed()
else -> Unit
}
}
@@ -205,7 +210,7 @@ fun BackupAlertSheetContent(
when (backupAlert) {
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
- BackupAlert.FailedToRenew -> {
+ BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> {
Box {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
@@ -255,6 +260,7 @@ fun BackupAlertSheetContent(
BackupAlert.BackupFailed -> BackupFailedBody()
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
+ BackupAlert.ExpiredAndDowngraded -> SubscriptionExpired()
}
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
@@ -331,6 +337,23 @@ private fun CouldNotRedeemBackup() {
}
}
+@Composable
+private fun SubscriptionExpired() {
+ Text(
+ text = stringResource(id = R.string.BackupAlertBottomSheet__your_subscription_couldnt_be_renewed),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = 24.dp)
+ )
+
+ Text(
+ text = stringResource(id = R.string.BackupAlertBottomSheet__youll_continue_to_have_access_to_the_free),
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.padding(bottom = 36.dp)
+ )
+}
+
@Composable
private fun CouldNotCompleteBackup(
daysSinceLastBackup: Int
@@ -401,7 +424,7 @@ private fun BackupFailedBody() {
private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors {
return remember(backupAlert) {
when (backupAlert) {
- BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.")
+ BackupAlert.ExpiredAndDowngraded, BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.")
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed, is BackupAlert.DiskFull, BackupAlert.CouldNotRedeemBackup -> BackupsIconColors.Warning
BackupAlert.MediaWillBeDeletedToday -> BackupsIconColors.Error
}
@@ -418,6 +441,7 @@ private fun titleString(backupAlert: BackupAlert): String {
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__backup_failed)
BackupAlert.CouldNotRedeemBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_redeem_your_backups_subscription)
+ BackupAlert.ExpiredAndDowngraded -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_has_expired)
}
}
@@ -433,6 +457,7 @@ private fun primaryActionString(
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it)
is BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__check_for_update)
BackupAlert.CouldNotRedeemBackup -> stringResource(R.string.BackupAlertBottomSheet__got_it)
+ BackupAlert.ExpiredAndDowngraded -> stringResource(R.string.BackupAlertBottomSheet__manage_backups)
}
}
@@ -441,7 +466,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
- BackupAlert.FailedToRenew -> R.string.BackupAlertBottomSheet__not_now
+ BackupAlert.FailedToRenew, BackupAlert.ExpiredAndDowngraded -> R.string.BackupAlertBottomSheet__not_now
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
@@ -511,6 +536,16 @@ private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
}
}
+@SignalPreview
+@Composable
+private fun BackupAlertSheetContentPreviewSubscriptionExpired() {
+ Previews.BottomSheetPreview {
+ BackupAlertSheetContent(
+ backupAlert = BackupAlert.ExpiredAndDowngraded
+ )
+ }
+}
+
/**
* All necessary information to display the sheet should be handed in through the specific alert.
*/
@@ -559,4 +594,10 @@ sealed class BackupAlert : Parcelable {
* Too many attempts to redeem the backup subscription have occurred this month.
*/
data object CouldNotRedeemBackup : BackupAlert()
+
+ /**
+ * Displayed after the user falls out of the grace period and their backups subscription is downgraded
+ * to the free tier.
+ */
+ data object ExpiredAndDowngraded : BackupAlert()
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt
index b0b785140b..a596f7d86d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/BackupAlertDelegate.kt
@@ -29,6 +29,8 @@ object BackupAlertDelegate {
BackupAlertBottomSheet.create(BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = SignalStore.backup.daysSinceLastBackup)).show(fragmentManager, null)
} else if (withContext(Dispatchers.IO) { BackupRepository.shouldDisplayYourMediaWillBeDeletedTodaySheet() }) {
BackupAlertBottomSheet.create(BackupAlert.MediaWillBeDeletedToday).show(fragmentManager, null)
+ } else if (BackupRepository.shouldDisplayBackupExpiredAndDowngradedSheet()) {
+ BackupAlertBottomSheet.create(BackupAlert.ExpiredAndDowngraded).show(fragmentManager, null)
}
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt
index 73bb7d3059..e856b4fad5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsFragment.kt
@@ -165,7 +165,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
@Stable
private inner class Callbacks : ContentCallbacks {
override fun onNavigationClick() {
- findNavController().popBackStack()
+ requireActivity().onBackPressedDispatcher.onBackPressed()
}
override fun onBackupTypeActionClick(tier: MessageBackupTier) {
@@ -433,6 +433,14 @@ private fun RemoteBackupsSettingsContent(
onBackupTypeActionButtonClicked = contentCallbacks::onBackupTypeActionClick
)
}
+
+ RemoteBackupsSettingsState.BackupState.NotFound -> {
+ SubscriptionNotFoundCard(
+ title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found),
+ onRenewClick = contentCallbacks::onRenewLostSubscription,
+ onLearnMoreClick = contentCallbacks::onLearnMoreAboutLostSubscription
+ )
+ }
}
}
@@ -849,7 +857,9 @@ private fun RedemptionErrorAlert(
Text(
text = stringResource(R.string.AppSettingsFragment__couldnt_redeem_your_backups_subscription),
- modifier = Modifier.padding(start = 16.dp, end = 4.dp).weight(1f)
+ modifier = Modifier
+ .padding(start = 16.dp, end = 4.dp)
+ .weight(1f)
)
Buttons.Small(onClick = onDetailsClick) {
@@ -939,8 +949,8 @@ private fun PendingCard(
}
@Composable
-private fun SubscriptionMismatchMissingGooglePlayCard(
- state: RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay,
+private fun SubscriptionNotFoundCard(
+ title: String,
onRenewClick: () -> Unit = {},
onLearnMoreClick: () -> Unit = {}
) {
@@ -951,11 +961,9 @@ private fun SubscriptionMismatchMissingGooglePlayCard(
.background(color = SignalTheme.colors.colorSurface2, shape = RoundedCornerShape(12.dp))
.padding(24.dp)
) {
- val days by rememberUpdatedState((state.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays)
-
Row {
Text(
- text = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__your_subscription_on_this_device_is_valid, days.toInt(), days),
+ text = title,
modifier = Modifier
.weight(1f)
.padding(end = 13.dp)
@@ -1017,6 +1025,21 @@ private fun SubscriptionMismatchMissingGooglePlayCard(
}
}
+@Composable
+private fun SubscriptionMismatchMissingGooglePlayCard(
+ state: RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay,
+ onRenewClick: () -> Unit = {},
+ onLearnMoreClick: () -> Unit = {}
+) {
+ val days by rememberUpdatedState((state.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays)
+
+ SubscriptionNotFoundCard(
+ title = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__your_subscription_on_this_device_is_valid, days.toInt(), days),
+ onRenewClick = onRenewClick,
+ onLearnMoreClick = onLearnMoreClick
+ )
+}
+
@Composable
private fun InProgressBackupRow(
archiveUploadProgressState: ArchiveUploadProgressState
@@ -1430,6 +1453,16 @@ private fun PendingCardPreview() {
}
}
+@SignalPreview
+@Composable
+private fun SubscriptionNotFoundCardPreview() {
+ Previews.Preview {
+ SubscriptionNotFoundCard(
+ title = stringResource(R.string.RemoteBackupsSettingsFragment__your_subscription_was_not_found)
+ )
+ }
+}
+
@SignalPreview
@Composable
private fun SubscriptionMismatchMissingGooglePlayCardPreview() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt
index d93e4d17e9..fde3c8961a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsState.kt
@@ -85,6 +85,11 @@ data class RemoteBackupsSettingsState(
override val renewalTime: Duration = 0.seconds
) : WithTypeAndRenewalTime
+ /**
+ * User has an active backup but no active subscription
+ */
+ data object NotFound : BackupState
+
/**
* User has a canceled paid tier backup
*/
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt
index cfaf1ecaea..e0685e4a19 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt
@@ -345,11 +345,19 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
)
}
} else {
- Log.d(TAG, "ActiveSubscription had null subscription object. Updating UI state with INACTIVE subscription.")
- _state.update {
- it.copy(
- backupState = RemoteBackupsSettingsState.BackupState.Inactive(type)
- )
+ Log.d(TAG, "ActiveSubscription had null subscription object.")
+ if (SignalStore.backup.areBackupsEnabled) {
+ _state.update {
+ it.copy(
+ backupState = RemoteBackupsSettingsState.BackupState.NotFound
+ )
+ }
+ } else {
+ _state.update {
+ it.copy(
+ backupState = RemoteBackupsSettingsState.BackupState.Inactive(type)
+ )
+ }
}
}
} else {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt
index 39ae5a0249..b5ebfc9a53 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt
@@ -522,6 +522,22 @@ fun Screen(
Dividers.Default()
+ Rows.TextRow(
+ text = "Mark backup failure",
+ label = "This will display the error sheet when returning to the chats list.",
+ onClick = {
+ SignalStore.backup.internalSetBackupFailedErrorState()
+ }
+ )
+
+ Rows.TextRow(
+ text = "Mark backup expired and downgraded",
+ label = "This will not actually downgrade the user.",
+ onClick = {
+ SignalStore.backup.backupExpiredAndDowngraded = true
+ }
+ )
+
Spacer(modifier = Modifier.height(8.dp))
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt
index 938eb91351..5eec3d57cf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/BackupValues.kt
@@ -70,6 +70,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_INVALID_BACKUP_VERSION = "backup.invalid.version"
private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore"
+ private const val KEY_BACKUP_EXPIRED_AND_DOWNGRADED = "backup.expired.and.downgraded"
private const val KEY_MEDIA_ROOT_BACKUP_KEY = "backup.mediaRootBackupKey"
@@ -111,6 +112,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
var userManuallySkippedMediaRestore: Boolean by booleanValue(KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE, false)
+ var backupExpiredAndDowngraded: Boolean by booleanValue(KEY_BACKUP_EXPIRED_AND_DOWNGRADED, false)
+
/**
* The last time the device notified the server that the archive is still in use.
*/
@@ -242,6 +245,11 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
*/
var spaceAvailableOnDiskBytes: Long by longValue(KEY_BACKUP_FAIL_SPACE_REMAINING, -1L)
+ fun internalSetBackupFailedErrorState() {
+ markMessageBackupFailure()
+ putLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, 0)
+ }
+
/**
* Call when the user disables backups. Clears/resets all relevant fields.
*/
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fdf16ab037..d823ed251d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -7837,6 +7837,12 @@
Your backups subscription failed to renew
Check to make sure your payment method is up to date. Tap Manage subscription and under Payment methods tap Update.
+
+ Your backups subscription has expired
+
+ Your subscription couldn\'t be renewed with your payment method. To continue backing up media you need an active subscription.
+
+ You\'ll continue to have access to the free Signal Backups plan.
Couldn\'t complete backup
@@ -7848,6 +7854,8 @@
Learn more
Skip restore
+
+ Manage backups
Back up now
@@ -8140,6 +8148,8 @@
Downloading your backup
Depending on the size of your backup, this could take a long time. You can use your phone as you normally do while the download takes place.
+
+ Your subscription was not found on this device. Renew to continue using Signal Backups.
- Your subscription on this device is valid for the next %1$d day. Renew to continue using Signal Backups