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

@ -61,11 +61,11 @@ object MockProvider {
}
fun createWhoAmIResponse(aci: ServiceId, pni: ServiceId, e164: String): WhoAmIResponse {
return WhoAmIResponse().apply {
this.uuid = aci.toString()
this.pni = pni.toString()
this.number = e164
}
return WhoAmIResponse(
aci = aci.toString(),
pni = pni.toString(),
number = e164
)
}
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account.aciIdentityKey, deviceId: Int): PreKeyResponse {

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()
}
if (InAppPaymentSubscriberRecord.Type.BACKUP in supportedTypes) {
if (RemoteConfig.messageBackups) {
handleInAppPaymentBackupsSheets()
}
}

View file

@ -173,8 +173,48 @@ class InAppPaymentRecurringContextJob private constructor(
inAppPayment
}
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> {
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)
@ -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)
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

View file

@ -5015,6 +5015,8 @@
<string name="AppSettingsFragment__renew_your_signal_backups_subscription">Renew your Signal Backups subscription</string>
<!-- String alerting user that backup failed -->
<string name="AppSettingsFragment__couldnt_complete_backup">Couldn\'t complete backup</string>
<!-- String alerting user that backup redemption -->
<string name="AppSettingsFragment__couldnt_redeem_your_backups_subscription">Couldn\'t redeem your backups subscription</string>
<!-- String displayed telling user to invite their friends to Signal -->
<string name="AppSettingsFragment__invite_your_friends">Invite your friends</string>
<!-- String displayed in a toast when we successfully copy the donations subscriber id to the clipboard -->
@ -7539,10 +7541,18 @@
<string name="BackupAlertBottomSheet__if_you_skip_restore_the">If you skip restore the remaining media and attachments in your backup can be downloaded at a later time when storage space becomes available.</string>
<!-- Dialog title when a backup fails to be created -->
<string name="BackupAlertBottomSheet__backup_failed">Backup failed</string>
<!-- Dialog title when a backup redemption fails -->
<string name="BackupAlertBottomSheet__couldnt_redeem_your_backups_subscription">Couldn\'t redeem your backups subscription</string>
<!-- Dialog text for when a backup fails to be created and ways to fix it -->
<string name="BackupAlertBottomSheet__an_error_occurred">An error occurred and your backup could not be completed. Make sure you\'re on the latest version of Signal and try again. If this problem persists, contact support.</string>
<!-- Dialog action button that will allow you to check for any Signal version updates -->
<string name="BackupAlertBottomSheet__check_for_update">Check for update</string>
<!-- Backup redemption error sheet text line 1 -->
<string name="BackupAlertBottomSheet__too_many_devices_have_tried">Too many devices have tried to redeem your subscription this month. You may have:</string>
<!-- Backup redemption error sheet bullet point 1 -->
<string name="BackupAlertBottomSheet__reregistered_your_signal_account">Re-registered your Signal account too many times.</string>
<!-- Backup redemption error sheet bullet point 2 -->
<string name="BackupAlertBottomSheet__have_too_many_devices_using_the_same_subscription">Have too many devices using the same subscription.</string>
<!-- BackupStatus -->
<!-- Status title when user does not have enough free space to download their media. Placeholder is required disk space. -->
@ -7824,6 +7834,8 @@
<string name="RemoteBackupsSettingsFragment__a_network_error_occurred">A network error occurred. Please check your internet connection and try again.</string>
<!-- Progress message when backup file is being uploaded. First placeholder and second placeholder are formatted byte sizes (2 MB) and third is percent completion. -->
<string name="RemoteBackupsSettingsFragment__uploading_s_of_s_d">Uploading: %1$s of %2$s (%3$d%%)</string>
<!-- Button label to see more details about redemption error -->
<string name="RemoteBackupsSettingsFragment__details">Details</string>
<!-- SubscriptionNotFoundBottomSheet -->
<!-- Displayed as a bottom sheet title -->

View file

@ -1,33 +0,0 @@
package org.whispersystems.signalservice.internal.push;
import com.fasterxml.jackson.annotation.JsonProperty;
public class WhoAmIResponse {
@JsonProperty
public String uuid;
@JsonProperty
public String pni;
@JsonProperty
public String number;
@JsonProperty
public String usernameHash;
public String getAci() {
return uuid;
}
public String getPni() {
return pni;
}
public String getNumber() {
return number;
}
public String getUsernameHash() {
return usernameHash;
}
}

View file

@ -0,0 +1,31 @@
package org.whispersystems.signalservice.internal.push
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
/**
* Response object for /v1/accounts/whoami
*/
data class WhoAmIResponse @JsonCreator constructor(
@JsonProperty("uuid") val aci: String? = null,
@JsonProperty val pni: String? = null,
@JsonProperty val number: String,
@JsonProperty val usernameHash: String? = null,
@JsonProperty val entitlements: Entitlements? = null
) {
data class Entitlements @JsonCreator constructor(
@JsonProperty val badges: List<BadgeEntitlement>? = null,
@JsonProperty val backup: BackupEntitlement? = null
)
data class BadgeEntitlement @JsonCreator constructor(
@JsonProperty val id: String?,
@JsonProperty val visible: Boolean?,
@JsonProperty val expirationSeconds: Long?
)
data class BackupEntitlement @JsonCreator constructor(
@JsonProperty val backupLevel: Long?,
@JsonProperty val expirationSeconds: Long?
)
}