Update local inapppayment cancelation state to match that of the activesubscription when we discover it.

This commit is contained in:
Alex Hart
2025-10-23 10:29:09 -03:00
committed by jeffrey-signal
parent 7301dda5d1
commit 9d545412a5
3 changed files with 83 additions and 7 deletions

View File

@@ -255,7 +255,7 @@ class BackupStateObserver(
val purchaseResult = AppDependencies.billingApi.queryPurchases()
Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] queryPurchase result: $purchaseResult")
val hasActiveGooglePlayBillingSubscription = when (purchaseResult) {
val googlePlayBillingSubscriptionIsActiveAndWillRenew = when (purchaseResult) {
is BillingPurchaseResult.Success -> {
Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Found a purchase: $purchaseResult")
purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing
@@ -267,7 +267,7 @@ class BackupStateObserver(
}
} || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID
Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription")
Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] googlePlayBillingSubscriptionIsActiveAndWillRenew: $googlePlayBillingSubscriptionIsActiveAndWillRenew")
val activeSubscriptionResult = withContext(Dispatchers.IO) {
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
@@ -278,26 +278,29 @@ class BackupStateObserver(
Log.w(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Failed to load active subscription due to an application error.", activeSubscriptionResult.getCause(), true)
return getStateOnError()
}
is NetworkResult.NetworkError<ActiveSubscription> -> {
Log.w(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Failed to load active subscription due to a network error.", activeSubscriptionResult.getCause(), true)
return getStateOnError()
}
is NetworkResult.StatusCodeError<ActiveSubscription> -> {
Log.i(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Failed to load active subscription due to a status code error.", activeSubscriptionResult.getCause(), true)
null
}
is NetworkResult.Success<ActiveSubscription> -> {
Log.i(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Successfully loaded active subscription.", true)
activeSubscriptionResult.result
}
}
val hasActiveSignalSubscription = activeSubscription?.isActive == true
val signalServiceSubscriptionIsActiveAndWillRenew = activeSubscription?.isActive == true && (!activeSubscription.isCanceled || activeSubscription.willCancelAtPeriodEnd())
Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription")
Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] signalServiceSubscriptionIsActiveAndWillRenew: $signalServiceSubscriptionIsActiveAndWillRenew")
when {
hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
signalServiceSubscriptionIsActiveAndWillRenew && !googlePlayBillingSubscriptionIsActiveAndWillRenew -> {
val type = buildPaidTypeFromSubscription(activeSubscription.activeSubscription)
if (type == null) {
@@ -312,12 +315,12 @@ class BackupStateObserver(
)
}
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
signalServiceSubscriptionIsActiveAndWillRenew && googlePlayBillingSubscriptionIsActiveAndWillRenew -> {
Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Found active signal subscription and active google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false
}
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
!signalServiceSubscriptionIsActiveAndWillRenew && !googlePlayBillingSubscriptionIsActiveAndWillRenew -> {
Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false
}
@@ -393,6 +396,8 @@ class BackupStateObserver(
when {
(subscription.isCanceled || subscription.willCancelAtPeriodEnd()) && subscription.isActive -> {
Log.d(TAG, "[getPaidBackupState] Found a canceled subscription.")
InAppPaymentsRepository.updateBackupInAppPaymentWithCancelation(activeSubscription.successOrThrow())
BackupState.Canceled(
messageBackupsType = subscriberType,
renewalTime = subscription.endOfCurrentPeriod.seconds
@@ -401,6 +406,7 @@ class BackupStateObserver(
subscription.isActive -> {
Log.d(TAG, "[getPaidBackupState] Found an active subscription.")
InAppPaymentsRepository.clearCancelation(activeSubscription.successOrThrow())
BackupState.ActivePaid(
messageBackupsType = subscriberType,
price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)),

View File

@@ -75,6 +75,73 @@ object InAppPaymentsRepository {
private val temporaryErrorProcessor = PublishProcessor.create<Pair<InAppPaymentTable.InAppPaymentId, Throwable>>()
/**
* Updates the latest payment object for the given subscription with cancelation information as necessary.
*
* This operation will only be performed if we find a latest payment for the given subscriber id in the END state without cancelation data
*/
fun updateBackupInAppPaymentWithCancelation(activeSubscription: ActiveSubscription) {
if (activeSubscription.isCanceled || activeSubscription.willCancelAtPeriodEnd()) {
val subscriber = getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) ?: return
val latestPayment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return
if (latestPayment.state == InAppPaymentTable.State.END && latestPayment.data.cancellation == null) {
synchronized(subscriber.type.lock) {
val payment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return
val chargeFailure: ActiveSubscription.ChargeFailure? = activeSubscription.chargeFailure
Log.i(TAG, "Recording cancelation in the database. (has charge failure? ${chargeFailure != null})")
SignalDatabase.inAppPayments.update(
payment.copy(
data = payment.data.newBuilder()
.cancellation(
InAppPaymentData.Cancellation(
reason = if (chargeFailure != null) InAppPaymentData.Cancellation.Reason.PAST_DUE else InAppPaymentData.Cancellation.Reason.CANCELED,
chargeFailure = chargeFailure?.let {
InAppPaymentData.ChargeFailure(
code = it.code,
message = it.message,
outcomeType = it.outcomeType,
outcomeNetworkReason = it.outcomeNetworkReason ?: "",
outcomeNetworkStatus = it.outcomeNetworkStatus
)
}
)
)
.build()
)
)
}
}
}
}
/**
* Updates the latest payment object clearing cancelation information as necessary.
*
* This operation will only be performed if we find a latest payment for the given subscriber id in the END state with cancelation data
*/
fun clearCancelation(activeSubscription: ActiveSubscription) {
if (!activeSubscription.isCanceled && !activeSubscription.willCancelAtPeriodEnd()) {
val subscriber = getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) ?: return
val latestPayment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return
if (latestPayment.data.cancellation != null && latestPayment.state == InAppPaymentTable.State.END) {
synchronized(subscriber.type.lock) {
val payment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return
Log.i(TAG, "Clearing cancelation in the database.")
SignalDatabase.inAppPayments.update(
payment.copy(
data = payment.data.newBuilder()
.cancellation(null)
.build()
)
)
}
}
}
}
/**
* Wraps an in-app-payment update in a completable.
*/

View File

@@ -258,6 +258,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
if (backupExpiration != null) {
Log.i(TAG, "Marking subscription failed or canceled.")
SignalStore.backup.setDownloadNotifierToTriggerAtHalfwayPoint(backupExpiration)
InAppPaymentsRepository.updateBackupInAppPaymentWithCancelation(activeSubscription)
BackupStateObserver.notifyBackupStateChanged()
} else {
Log.w(TAG, "Failed to mark, no entitlement was found on WhoAmIResponse")
@@ -267,6 +268,8 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
if (response.getCause() != null) {
Log.w(TAG, "Failed to get WhoAmI from service.", response.getCause())
}
} else if (activeSubscription != null) {
InAppPaymentsRepository.clearCancelation(activeSubscription)
}
}