Prevent "Free" tier from being upgraded in the background.

This commit is contained in:
Alex Hart
2025-06-24 11:11:32 -03:00
committed by GitHub
parent 1719122f5e
commit ccc4acdef9
3 changed files with 46 additions and 30 deletions

View File

@@ -169,7 +169,7 @@ private fun BackupsSettingsContent(
OtherWaysToBackUpHeading() OtherWaysToBackUpHeading()
} }
is BackupState.ActiveFree, is BackupState.ActivePaid -> { is BackupState.ActiveFree, is BackupState.ActivePaid, is BackupState.Canceled -> {
ActiveBackupsRow( ActiveBackupsRow(
backupState = backupsSettingsState.backupState, backupState = backupsSettingsState.backupState,
onBackupsRowClick = onBackupsRowClick, onBackupsRowClick = onBackupsRowClick,
@@ -210,15 +210,6 @@ private fun BackupsSettingsContent(
OtherWaysToBackUpHeading() OtherWaysToBackUpHeading()
} }
is BackupState.Canceled -> {
ActiveBackupsRow(
backupState = backupsSettingsState.backupState,
lastBackupAt = backupsSettingsState.lastBackupAt
)
OtherWaysToBackUpHeading()
}
is BackupState.SubscriptionMismatchMissingGooglePlay -> { is BackupState.SubscriptionMismatchMissingGooglePlay -> {
ActiveBackupsRow( ActiveBackupsRow(
backupState = backupsSettingsState.backupState, backupState = backupsSettingsState.backupState,
@@ -421,13 +412,25 @@ private fun ActiveBackupsRow(
when (val type = backupState.messageBackupsType) { when (val type = backupState.messageBackupsType) {
is MessageBackupsType.Paid -> { is MessageBackupsType.Paid -> {
Text( val body = if (backupState is BackupState.Canceled) {
text = stringResource( stringResource(R.string.BackupsSettingsFragment__subscription_canceled)
} else {
stringResource(
R.string.BackupsSettingsFragment_s_month_renews_s, R.string.BackupsSettingsFragment_s_month_renews_s,
FiatMoneyUtil.format(LocalContext.current.resources, type.pricePerMonth), FiatMoneyUtil.format(LocalContext.current.resources, type.pricePerMonth),
DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds) DateUtils.formatDateWithYear(Locale.getDefault(), backupState.renewalTime.inWholeMilliseconds)
), )
color = MaterialTheme.colorScheme.onSurfaceVariant, }
val color = if (backupState is BackupState.Canceled) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.onSurfaceVariant
}
Text(
text = body,
color = color,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
} }

View File

@@ -71,43 +71,43 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
override suspend fun doRun(): Result { override suspend fun doRun(): Result {
if (!SignalStore.account.isRegistered) { if (!SignalStore.account.isRegistered) {
Log.i(TAG, "User is not registered. Clearing mismatch value and exiting.") Log.i(TAG, "User is not registered. Clearing mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} }
if (!RemoteConfig.messageBackups) { if (!RemoteConfig.messageBackups) {
Log.i(TAG, "Message backups feature is not available. Clearing mismatch value and exiting.") Log.i(TAG, "Message backups feature is not available. Clearing mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} }
if (!AppDependencies.billingApi.isApiAvailable()) { if (!AppDependencies.billingApi.isApiAvailable()) {
Log.i(TAG, "Google Play Billing API is not available on this device. Clearing mismatch value and exiting.") Log.i(TAG, "Google Play Billing API is not available on this device. Clearing mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} }
if (SignalStore.backup.deletionState != DeletionState.NONE) { if (SignalStore.backup.deletionState != DeletionState.NONE) {
Log.i(TAG, "User is in the process of or has delete their backup. Clearing mismatch value and exiting.") Log.i(TAG, "User is in the process of or has delete their backup. Clearing mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} }
if (!SignalStore.backup.areBackupsEnabled) { if (!SignalStore.backup.areBackupsEnabled) {
Log.i(TAG, "Backups are not enabled on this device. Clearing mismatch value and exiting.") Log.i(TAG, "Backups are not enabled on this device. Clearing mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} }
val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases() val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases()
Log.i(TAG, "Retrieved purchase result from Billing api: $purchase") Log.i(TAG, "Retrieved purchase result from Billing api: $purchase", true)
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged
val product: BillingProduct? = AppDependencies.billingApi.queryProduct() val product: BillingProduct? = AppDependencies.billingApi.queryProduct()
if (product == null) { if (product == null) {
Log.w(TAG, "Google Play Billing product not available. Exiting.") Log.w(TAG, "Google Play Billing product not available. Exiting.", true)
return Result.failure() return Result.failure()
} }
@@ -115,7 +115,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP) val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
if (inAppPayment?.state == InAppPaymentTable.State.PENDING) { if (inAppPayment?.state == InAppPaymentTable.State.PENDING) {
Log.i(TAG, "User has a pending in-app payment. Clearing mismatch value and re-checking later.") Log.i(TAG, "User has a pending in-app payment. Clearing mismatch value and re-checking later.", true)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} }
@@ -125,9 +125,12 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
checkForFailedOrCanceledSubscriptionState(activeSubscription) checkForFailedOrCanceledSubscriptionState(activeSubscription)
Log.i(TAG, "Synchronizing backup tier with value from server.") val isSignalSubscriptionFailedOrCanceled = activeSubscription?.isFailedPayment == true || activeSubscription?.isCanceled == true
BackupRepository.getBackupTier().runIfSuccessful { if (hasActiveSignalSubscription && !isSignalSubscriptionFailedOrCanceled) {
SignalStore.backup.backupTier = it Log.i(TAG, "Detected an active, non-failed, non-canceled signal subscription. Synchronizing backup tier with value from server.", true)
BackupRepository.getBackupTier().runIfSuccessful {
SignalStore.backup.backupTier = it
}
} }
val hasActivePaidBackupTier = SignalStore.backup.backupTier == MessageBackupTier.PAID val hasActivePaidBackupTier = SignalStore.backup.backupTier == MessageBackupTier.PAID
@@ -135,14 +138,14 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val hasValidInactiveState = !hasActivePaidBackupTier && !hasActiveSignalSubscription && !hasActivePurchase val hasValidInactiveState = !hasActivePaidBackupTier && !hasActiveSignalSubscription && !hasActivePurchase
val purchaseToken = if (hasActivePurchase) { val purchaseToken = if (hasActivePurchase) {
(purchase as BillingPurchaseResult.Success).purchaseToken purchase.purchaseToken
} else { } else {
null null
} }
val hasTokenMismatch = purchaseToken?.let { hasLocalDevicePurchaseTokenMismatch(purchaseToken) } == true val hasTokenMismatch = purchaseToken?.let { hasLocalDevicePurchaseTokenMismatch(purchaseToken) } == true
if (hasActiveSignalSubscription && hasTokenMismatch) { if (hasActiveSignalSubscription && hasTokenMismatch) {
Log.i(TAG, "Encountered token mismatch with an active Signal subscription. Attempting to redeem against latest token.") Log.i(TAG, "Encountered token mismatch with an active Signal subscription. Attempting to redeem against latest token.", true)
enqueueRedemptionForNewToken(purchaseToken, product.price) enqueueRedemptionForNewToken(purchaseToken, product.price)
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
@@ -152,9 +155,17 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
SignalStore.backup.subscriptionStateMismatchDetected = false SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success() return Result.success()
} else { } else {
Log.w(TAG, "State mismatch: (hasActivePaidBackupTier: $hasActivePaidBackupTier, hasActiveSignalSubscription: $hasActiveSignalSubscription, hasActivePurchase: $hasActivePurchase). Setting mismatch value and exiting.", true) val isGooglePlayBillingCanceled = purchase is BillingPurchaseResult.Success && !purchase.isAutoRenewing
SignalStore.backup.subscriptionStateMismatchDetected = true
return Result.success() if (isGooglePlayBillingCanceled && (!hasActiveSignalSubscription || isSignalSubscriptionFailedOrCanceled)) {
Log.i(TAG, "Valid cancel state. Clearing mismatch. (isGooglePlayBillingCanceled: true, hasActiveSignalSubscription: $hasActiveSignalSubscription, isSignalSubscriptionFailedOrCanceled: $isSignalSubscriptionFailedOrCanceled", true)
SignalStore.backup.subscriptionStateMismatchDetected = false
return Result.success()
} else {
Log.w(TAG, "State mismatch: (hasActivePaidBackupTier: $hasActivePaidBackupTier, hasActiveSignalSubscription: $hasActiveSignalSubscription, hasActivePurchase: $hasActivePurchase). Setting mismatch value and exiting.", true)
SignalStore.backup.subscriptionStateMismatchDetected = true
return Result.success()
}
} }
} }
} }

View File

@@ -8072,6 +8072,8 @@
<string name="BackupsSettingsFragment_s_month_renews_s">%1$s/month, renews %2$s</string> <string name="BackupsSettingsFragment_s_month_renews_s">%1$s/month, renews %2$s</string>
<!-- Subtitle for row for active backup, placeholder is last date of backup --> <!-- Subtitle for row for active backup, placeholder is last date of backup -->
<string name="BackupsSettingsFragment_last_backup_s">Last backup %1$s</string> <string name="BackupsSettingsFragment_last_backup_s">Last backup %1$s</string>
<!-- Subtitle for row for canceled backup -->
<string name="BackupsSettingsFragment__subscription_canceled">Subscription canceled</string>
<!-- Subtitle for row for no backup ever created --> <!-- Subtitle for row for no backup ever created -->
<string name="BackupsSettingsFragment_automatic_backups_with_signals">Automatic backups with Signal\'s secure end-to-end encrypted storage service.</string> <string name="BackupsSettingsFragment_automatic_backups_with_signals">Automatic backups with Signal\'s secure end-to-end encrypted storage service.</string>
<!-- Subtitle for row for backups that are active but subscription not found --> <!-- Subtitle for row for backups that are active but subscription not found -->