Add whoami check for receipt_credentials.

This commit is contained in:
Alex Hart
2025-01-09 10:04:15 -04:00
committed by Greyson Parrelli
parent 0dbab7ede0
commit 23f90e070e
16 changed files with 300 additions and 69 deletions

View File

@@ -202,6 +202,11 @@ object BackupRepository {
return alertAfter <= now
}
@JvmStatic
fun shouldDisplayBackupAlreadyRedeemedIndicator(): Boolean {
return !(shouldNotDisplayBackupFailedMessaging() || !SignalStore.backup.hasBackupAlreadyRedeemedError)
}
/**
* Whether the "Backup Failed" row should be displayed in settings.
* Shown when the initial backup creation has failed
@@ -226,6 +231,10 @@ object BackupRepository {
return SignalStore.backup.hasBackupBeenUploaded && SignalStore.backup.hasBackupFailure
}
fun markBackupAlreadyRedeemedIndicatorClicked() {
SignalStore.backup.hasBackupAlreadyRedeemedError = false
}
/**
* Updates the watermark for the indicator display.
*/

View File

@@ -12,11 +12,16 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -31,11 +36,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
@@ -47,6 +53,7 @@ import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
@@ -125,6 +132,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
is BackupAlert.MediaBackupsAreOff -> {
onSubscribeClick()
}
BackupAlert.MediaWillBeDeletedToday -> {
performFullMediaDownload()
}
@@ -132,6 +140,8 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
is BackupAlert.DiskFull -> Unit
is BackupAlert.BackupFailed ->
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext())
BackupAlert.CouldNotRedeemBackup -> Unit
}
dismissAllowingStateLoss()
@@ -152,6 +162,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
}
// TODO [backups] - Update support URL with backups page
BackupAlert.BackupFailed -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_support_url))
BackupAlert.CouldNotRedeemBackup -> CommunicationActions.openBrowserLink(requireContext(), requireContext().getString(R.string.backup_support_url)) // TODO [backups] final url
}
dismissAllowingStateLoss()
@@ -224,14 +235,14 @@ private fun BackupAlertSheetContent(
BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> {
Box {
Image(
painter = painterResource(id = R.drawable.image_signal_backups),
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.padding(2.dp)
)
Icon(
painter = painterResource(R.drawable.symbol_error_circle_fill_24),
imageVector = ImageVector.vectorResource(R.drawable.symbol_error_circle_fill_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.align(Alignment.TopEnd)
@@ -242,7 +253,7 @@ private fun BackupAlertSheetContent(
else -> {
val iconColors = rememberBackupsIconColors(backupAlert = backupAlert)
Icon(
painter = painterResource(id = R.drawable.symbol_backup_light),
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_backup_light),
contentDescription = null,
tint = iconColors.foreground,
modifier = Modifier
@@ -270,6 +281,7 @@ private fun BackupAlertSheetContent(
BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
BackupAlert.BackupFailed -> BackupFailedBody()
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
}
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
@@ -297,6 +309,57 @@ private fun BackupAlertSheetContent(
}
}
@Composable
private fun CouldNotRedeemBackup() {
Text(
text = stringResource(R.string.BackupAlertBottomSheet__too_many_devices_have_tried),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 16.dp)
)
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.padding(horizontal = 35.dp)
) {
Box(
modifier = Modifier
.width(4.dp)
.fillMaxHeight()
.padding(vertical = 2.dp)
.background(color = SignalTheme.colors.colorTransparentInverse2)
)
Text(
text = stringResource(R.string.BackupAlertBottomSheet__reregistered_your_signal_account),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 12.dp)
)
}
Row(
modifier = Modifier
.height(IntrinsicSize.Min)
.padding(horizontal = 35.dp)
.padding(top = 12.dp, bottom = 40.dp)
) {
Box(
modifier = Modifier
.width(4.dp)
.fillMaxHeight()
.padding(vertical = 2.dp)
.background(color = SignalTheme.colors.colorTransparentInverse2)
)
Text(
text = stringResource(R.string.BackupAlertBottomSheet__have_too_many_devices_using_the_same_subscription),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 12.dp)
)
}
}
@Composable
private fun CouldNotCompleteBackup(
daysSinceLastBackup: Int
@@ -390,7 +453,7 @@ private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColo
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> error("Not icon-based options.")
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed, is BackupAlert.DiskFull -> BackupsIconColors.Warning
is BackupAlert.CouldNotCompleteBackup, BackupAlert.BackupFailed, is BackupAlert.DiskFull, BackupAlert.CouldNotRedeemBackup -> BackupsIconColors.Warning
BackupAlert.MediaWillBeDeletedToday -> BackupsIconColors.Error
}
}
@@ -405,6 +468,7 @@ private fun titleString(backupAlert: BackupAlert): String {
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today)
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)
}
}
@@ -420,6 +484,7 @@ private fun primaryActionString(
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
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)
}
}
@@ -433,6 +498,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__learn_more
BackupAlert.CouldNotRedeemBackup -> R.string.BackupAlertBottomSheet__learn_more
}
}
}
@@ -504,6 +570,17 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.CouldNotRedeemBackup,
mediaTtl = 60.days
)
}
}
/**
* All necessary information to display the sheet should be handed in through the specific alert.
*/
@@ -547,4 +624,9 @@ sealed class BackupAlert : Parcelable {
*
*/
data class DiskFull(val requiredSpace: String) : BackupAlert()
/**
* Too many attempts to redeem the backup subscription have occurred this month.
*/
data object CouldNotRedeemBackup : BackupAlert()
}

View File

@@ -231,6 +231,22 @@ private fun AppSettingsContent(
}
}
BackupFailureState.ALREADY_REDEEMED -> {
item {
Dividers.Default()
BackupsWarningRow(
text = stringResource(R.string.AppSettingsFragment__couldnt_redeem_your_backups_subscription),
onClick = {
BackupRepository.markBackupAlreadyRedeemedIndicatorClicked()
callbacks.navigate(R.id.action_appSettingsFragment_to_remoteBackupsSettingsFragment)
}
)
Dividers.Default()
}
}
BackupFailureState.NONE -> Unit
}

View File

@@ -77,6 +77,8 @@ class AppSettingsViewModel : ViewModel() {
BackupFailureState.COULD_NOT_COMPLETE_BACKUP
} else if (SignalStore.backup.subscriptionStateMismatchDetected) {
BackupFailureState.SUBSCRIPTION_STATE_MISMATCH
} else if (SignalStore.backup.hasBackupAlreadyRedeemedError) {
BackupFailureState.ALREADY_REDEEMED
} else {
BackupFailureState.NONE
}

View File

@@ -12,5 +12,6 @@ enum class BackupFailureState {
NONE,
BACKUP_FAILED,
COULD_NOT_COMPLETE_BACKUP,
SUBSCRIPTION_STATE_MISMATCH
SUBSCRIPTION_STATE_MISMATCH,
ALREADY_REDEEMED
}

View File

@@ -36,6 +36,7 @@ import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
@@ -153,7 +154,8 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
backupProgress = backupProgress,
backupSize = state.backupSize,
backupState = state.backupState,
backupRestoreState = restoreState
backupRestoreState = restoreState,
hasRedemptionError = state.hasRedemptionError
)
}
@@ -248,6 +250,10 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
override fun onRestoreUsingCellularClick(canUseCellular: Boolean) {
viewModel.setCanRestoreUsingCellular(canUseCellular)
}
override fun onRedemptionErrorDetailsClick() {
BackupAlertBottomSheet.create(BackupAlert.CouldNotRedeemBackup).show(parentFragmentManager, null)
}
}
private fun displayBackupKey() {
@@ -331,6 +337,7 @@ private interface ContentCallbacks {
fun onContactSupport() = Unit
fun onLearnMoreAboutBackupFailure() = Unit
fun onRestoreUsingCellularClick(canUseCellular: Boolean) = Unit
fun onRedemptionErrorDetailsClick() = Unit
}
@Composable
@@ -346,7 +353,8 @@ private fun RemoteBackupsSettingsContent(
requestedSnackbar: RemoteBackupsSettingsState.Snackbar,
contentCallbacks: ContentCallbacks,
backupProgress: ArchiveUploadProgressState?,
backupSize: Long
backupSize: Long,
hasRedemptionError: Boolean
) {
val snackbarHostState = remember {
SnackbarHostState()
@@ -364,6 +372,12 @@ private fun RemoteBackupsSettingsContent(
modifier = Modifier
.padding(it)
) {
if (hasRedemptionError) {
item {
RedemptionErrorAlert(onDetailsClick = contentCallbacks::onRedemptionErrorDetailsClick)
}
}
item {
AnimatedContent(backupState, label = "backup-state-block") { state ->
when (state) {
@@ -771,6 +785,42 @@ private fun BackupCard(
}
}
@Composable
private fun RedemptionErrorAlert(
onDetailsClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 8.dp, bottom = 4.dp)
.border(
width = 1.dp,
color = colorResource(R.color.signal_colorOutline_38),
shape = RoundedCornerShape(12.dp)
)
.padding(vertical = 16.dp)
.padding(start = 16.dp, end = 12.dp)
) {
Icon(
painter = painterResource(R.drawable.symbol_backup_error_24),
tint = Color(0xFFFF9500),
contentDescription = null
)
Text(
text = stringResource(R.string.AppSettingsFragment__couldnt_redeem_your_backups_subscription),
modifier = Modifier.padding(start = 16.dp, end = 4.dp).weight(1f)
)
Buttons.Small(onClick = onDetailsClick) {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__details)
)
}
}
}
@Composable
private fun BoxCard(content: @Composable () -> Unit) {
Box(
@@ -988,6 +1038,7 @@ private fun getBackupPhaseMessage(state: ArchiveUploadProgressState): String {
(progress.progress * 100).toInt()
)
}
else -> stringResource(R.string.RemoteBackupsSettingsFragment__preparing_backup)
}
}
@@ -1272,11 +1323,20 @@ private fun RemoteBackupsSettingsContentPreview() {
backupState = RemoteBackupsSettingsState.BackupState.ActiveFree(
messageBackupsType = MessageBackupsType.Free(mediaRetentionDays = 30)
),
backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup)
backupRestoreState = BackupRestoreState.FromBackupStatusData(BackupStatusData.CouldNotCompleteBackup),
hasRedemptionError = true
)
}
}
@SignalPreview
@Composable
private fun RedemptionErrorAlertPreview() {
Previews.Preview {
RedemptionErrorAlert { }
}
}
@SignalPreview
@Composable
private fun LoadingCardPreview() {

View File

@@ -15,6 +15,7 @@ data class RemoteBackupsSettingsState(
val backupsEnabled: Boolean,
val canBackUpUsingCellular: Boolean = false,
val canRestoreUsingCellular: Boolean = false,
val hasRedemptionError: Boolean = false,
val backupState: BackupState = BackupState.Loading,
val backupSize: Long = 0,
val backupsFrequency: BackupFrequency = BackupFrequency.DAILY,

View File

@@ -240,6 +240,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
Log.d(TAG, "Subscription found. Updating UI state with subscription details.")
_state.update {
it.copy(
hasRedemptionError = lastPurchase?.data?.error?.data_ == "409",
backupState = when {
subscription.isActive -> RemoteBackupsSettingsState.BackupState.ActivePaid(
messageBackupsType = type,

View File

@@ -23,18 +23,17 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.Th
import org.thoughtcrime.securesms.components.settings.app.subscription.thanks.ThanksForYourSupportBottomSheetDialogFragmentArgs
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.DonationErrorValue
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* Handles displaying bottom sheets for in-app payments. The current policy is to "fire and forget".
*/
class InAppPaymentsBottomSheetDelegate(
private val fragmentManager: FragmentManager,
private val lifecycleOwner: LifecycleOwner,
private vararg val supportedTypes: InAppPaymentSubscriberRecord.Type = arrayOf(InAppPaymentSubscriberRecord.Type.DONATION)
private val lifecycleOwner: LifecycleOwner
) : DefaultLifecycleObserver {
companion object {
@@ -56,13 +55,11 @@ class InAppPaymentsBottomSheetDelegate(
private val badgeRepository = TerminalDonationRepository()
override fun onResume(owner: LifecycleOwner) {
if (InAppPaymentSubscriberRecord.Type.DONATION in supportedTypes) {
handleLegacyTerminalDonationSheets()
handleLegacyVerifiedMonthlyDonationSheets()
handleInAppPaymentDonationSheets()
}
handleLegacyTerminalDonationSheets()
handleLegacyVerifiedMonthlyDonationSheets()
handleInAppPaymentDonationSheets()
if (InAppPaymentSubscriberRecord.Type.BACKUP in supportedTypes) {
if (RemoteConfig.messageBackups) {
handleInAppPaymentBackupsSheets()
}
}

View File

@@ -173,7 +173,47 @@ class InAppPaymentRecurringContextJob private constructor(
inAppPayment
}
submitAndValidateCredentials(updatedInAppPayment, subscription, requestContext)
if (hasEntitlementAlready(inAppPayment, subscription.endOfCurrentPeriod)) {
info("Already have entitlement for this badge. Marking complete.")
markInAppPaymentCompleted(inAppPayment)
} else {
submitAndValidateCredentials(updatedInAppPayment, subscription, requestContext)
}
}
private fun hasEntitlementAlready(
inAppPayment: InAppPaymentTable.InAppPayment,
endOfCurrentSubscriptionPeriod: Long
): Boolean {
@Suppress("UsePropertyAccessSyntax")
val whoAmIResponse = AppDependencies.signalServiceAccountManager.getWhoAmI()
return when (inAppPayment.type) {
InAppPaymentType.RECURRING_BACKUP -> {
val backupExpirationSeconds = whoAmIResponse.entitlements?.backup?.expirationSeconds ?: return false
backupExpirationSeconds >= endOfCurrentSubscriptionPeriod
}
InAppPaymentType.RECURRING_DONATION -> {
val donationExpirationSeconds = whoAmIResponse.entitlements?.badges?.firstOrNull { it.id == inAppPayment.data.badge?.id }?.expirationSeconds ?: return false
donationExpirationSeconds >= endOfCurrentSubscriptionPeriod
}
else -> error("Unsupported IAP type ${inAppPayment.type}")
}
}
private fun markInAppPaymentCompleted(inAppPayment: InAppPaymentTable.InAppPayment) {
SignalDatabase.inAppPayments.update(
inAppPayment = inAppPayment.copy(
state = InAppPaymentTable.State.END,
data = inAppPayment.data.copy(
redemption = InAppPaymentData.RedemptionState(stage = InAppPaymentData.RedemptionState.Stage.REDEEMED)
)
)
)
}
private fun getAndValidateInAppPayment(): Pair<InAppPaymentTable.InAppPayment, ReceiptCredentialRequestContext> {
@@ -435,18 +475,13 @@ class InAppPaymentRecurringContextJob private constructor(
}
409 -> {
if (isForKeepAlive) {
warning("Already redeemed this token during keep-alive, ignoring.", applicationError)
SignalDatabase.inAppPayments.update(
inAppPayment.copy(
state = InAppPaymentTable.State.END,
data = inAppPayment.data.copy(redemption = inAppPayment.data.redemption.copy(stage = InAppPaymentData.RedemptionState.Stage.REDEEMED))
)
)
} else {
warning("Already redeemed this token during new subscription. Failing.", applicationError)
updateInAppPaymentWithGenericRedemptionError(inAppPayment)
warning("Already redeemed this token during new subscription. Failing.", applicationError)
if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) {
SignalStore.backup.hasBackupAlreadyRedeemedError = true
}
updateInAppPaymentWithTokenAlreadyRedeemedError(inAppPayment)
}
else -> {
@@ -520,6 +555,20 @@ class InAppPaymentRecurringContextJob private constructor(
return isSameLevel && isExpirationAfterSub && isExpiration86400 && isExpirationInTheFuture && isExpirationWithinMax
}
private fun updateInAppPaymentWithTokenAlreadyRedeemedError(inAppPayment: InAppPaymentTable.InAppPayment) {
SignalDatabase.inAppPayments.update(
inAppPayment = inAppPayment.copy(
state = InAppPaymentTable.State.END,
data = inAppPayment.data.copy(
error = InAppPaymentData.Error(
type = InAppPaymentData.Error.Type.REDEMPTION,
data_ = "409"
)
)
)
)
}
private fun updateInAppPaymentWithGenericRedemptionError(inAppPayment: InAppPaymentTable.InAppPayment) {
SignalDatabase.inAppPayments.update(
inAppPayment = inAppPayment.copy(

View File

@@ -63,6 +63,7 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_COUNT = "backup.failed.acknowledged.snooze.count"
private const val KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME = "backup.failed.sheet.snooze"
private const val KEY_BACKUP_FAIL_SPACE_REMAINING = "backup.failed.space.remaining"
private const val KEY_BACKUP_ALREADY_REDEEMED = "backup.already.redeemed"
private const val KEY_USER_MANUALLY_SKIPPED_MEDIA_RESTORE = "backup.user.manually.skipped.media.restore"
@@ -209,6 +210,8 @@ class BackupValues(store: KeyValueStore) : SignalStoreValues(store) {
val nextBackupFailureSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_ACKNOWLEDGED_SNOOZE_TIME, 0L).milliseconds
val nextBackupFailureSheetSnoozeTime: Duration get() = getLong(KEY_BACKUP_FAIL_SHEET_SNOOZE_TIME, getNextBackupFailureSheetSnoozeTime(lastBackupTime.milliseconds).inWholeMilliseconds).milliseconds
var hasBackupAlreadyRedeemedError: Boolean by booleanValue(KEY_BACKUP_ALREADY_REDEEMED, false)
/**
* Denotes how many bytes are still available on the disk for writing. Used to display
* the disk full error and sheet. Set when we believe there might be an "out of space"

View File

@@ -166,7 +166,7 @@ class MainActivityListHostFragment : Fragment(R.layout.main_activity_list_host_f
super.onResume()
SimpleTask.run(viewLifecycleOwner.lifecycle, { Recipient.self() }, ::initializeProfileIcon)
_backupsFailedDot.alpha = if (BackupRepository.shouldDisplayBackupFailedIndicator()) {
_backupsFailedDot.alpha = if (BackupRepository.shouldDisplayBackupFailedIndicator() || BackupRepository.shouldDisplayBackupAlreadyRedeemedIndicator()) {
1f
} else {
0f