mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 12:17:22 +00:00
Add UI for when the user's grace period expires or while they are in the grace period.
This commit is contained in:
committed by
Cody Henthorne
parent
5e9824a180
commit
a6bfeebb24
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user