From 87535a917a94a34f22aa6a230b9b3bc69f25c990 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 1 Oct 2025 09:50:42 -0300 Subject: [PATCH] Fully check result code when processing purchase results. --- .../app/backups/BackupStateObserver.kt | 5 +- .../jobs/BackupSubscriptionCheckJob.kt | 6 + .../java/org/signal/billing/BillingApiImpl.kt | 180 +++++++++--------- 3 files changed, 98 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt index a18324cc52..2984e811cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt @@ -252,7 +252,10 @@ class BackupStateObserver( if (SignalStore.backup.subscriptionStateMismatchDetected) { Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] A mismatch was detected.") - val hasActiveGooglePlayBillingSubscription = when (val purchaseResult = AppDependencies.billingApi.queryPurchases()) { + val purchaseResult = AppDependencies.billingApi.queryPurchases() + Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] queryPurchase result: $purchaseResult") + + val hasActiveGooglePlayBillingSubscription = when (purchaseResult) { is BillingPurchaseResult.Success -> { Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Found a purchase: $purchaseResult") purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt index 6da314ce30..80446a8daf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -118,6 +118,12 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases() Log.i(TAG, "Retrieved purchase result from Billing api: $purchase", true) + if (purchase !is BillingPurchaseResult.Success && purchase !is BillingPurchaseResult.None) { + Log.w(TAG, "Possible error when grabbing purchase from billing API. Clearing mismatch and exiting.") + SignalStore.backup.subscriptionStateMismatchDetected = false + return Result.success() + } + val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged val product: BillingProduct? = AppDependencies.billingApi.queryProduct() diff --git a/billing/src/main/java/org/signal/billing/BillingApiImpl.kt b/billing/src/main/java/org/signal/billing/BillingApiImpl.kt index 7cdff5dc8a..b0e03d2e9f 100644 --- a/billing/src/main/java/org/signal/billing/BillingApiImpl.kt +++ b/billing/src/main/java/org/signal/billing/BillingApiImpl.kt @@ -74,89 +74,7 @@ internal class BillingApiImpl( private val internalResults = MutableSharedFlow() private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> - val result = when (billingResult.responseCode) { - BillingResponseCode.OK -> { - if (purchases == null) { - Log.d(TAG, "purchasesUpdatedListener: No purchases.", true) - BillingPurchaseResult.None - } else { - Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.", true) - val newestPurchase = purchases.maxByOrNull { it.purchaseTime } - if (newestPurchase == null) { - Log.d(TAG, "purchasesUpdatedListener: no purchase.", true) - BillingPurchaseResult.None - } else { - Log.d(TAG, "purchasesUpdatedListener: successful purchase at ${newestPurchase.purchaseTime}", true) - BillingPurchaseResult.Success( - purchaseState = newestPurchase.purchaseState.toBillingPurchaseState(), - purchaseToken = newestPurchase.purchaseToken, - isAcknowledged = newestPurchase.isAcknowledged, - purchaseTime = newestPurchase.purchaseTime, - isAutoRenewing = newestPurchase.isAutoRenewing - ) - } - } - } - - BillingResponseCode.BILLING_UNAVAILABLE -> { - Log.d(TAG, "purchasesUpdatedListener: Billing unavailable.", true) - BillingPurchaseResult.BillingUnavailable - } - - BillingResponseCode.USER_CANCELED -> { - Log.d(TAG, "purchasesUpdatedListener: User cancelled.", true) - BillingPurchaseResult.UserCancelled - } - - BillingResponseCode.ERROR -> { - Log.d(TAG, "purchasesUpdatedListener: error.", true) - BillingPurchaseResult.GenericError - } - - BillingResponseCode.NETWORK_ERROR -> { - Log.d(TAG, "purchasesUpdatedListener: Network error.", true) - BillingPurchaseResult.NetworkError - } - - BillingResponseCode.DEVELOPER_ERROR -> { - Log.d(TAG, "purchasesUpdatedListener: Developer error.", true) - BillingPurchaseResult.GenericError - } - - BillingResponseCode.FEATURE_NOT_SUPPORTED -> { - Log.d(TAG, "purchasesUpdatedListener: Feature not supported.", true) - BillingPurchaseResult.FeatureNotSupported - } - - BillingResponseCode.ITEM_ALREADY_OWNED -> { - Log.d(TAG, "purchasesUpdatedListener: Already owned.", true) - BillingPurchaseResult.AlreadySubscribed - } - - BillingResponseCode.ITEM_NOT_OWNED -> { - error("This shouldn't happen during the purchase process") - } - - BillingResponseCode.ITEM_UNAVAILABLE -> { - Log.d(TAG, "purchasesUpdatedListener: Item is unavailable", true) - BillingPurchaseResult.TryAgainLater - } - - BillingResponseCode.SERVICE_UNAVAILABLE -> { - Log.d(TAG, "purchasesUpdatedListener: Service is unavailable.", true) - BillingPurchaseResult.TryAgainLater - } - - BillingResponseCode.SERVICE_DISCONNECTED -> { - Log.d(TAG, "purchasesUpdatedListener: Service is disconnected.", true) - BillingPurchaseResult.TryAgainLater - } - - else -> { - Log.d(TAG, "purchasesUpdatedListener: No purchases.", true) - BillingPurchaseResult.None - } - } + val result = handlePurchaseResult(billingResult, purchases) coroutineScope.launch { internalResults.emit(result) } } @@ -208,15 +126,7 @@ internal class BillingApiImpl( billingClient.queryPurchasesAsync(param) } - val purchase = result.purchasesList.maxByOrNull { it.purchaseTime } ?: return BillingPurchaseResult.None - - return BillingPurchaseResult.Success( - purchaseState = purchase.purchaseState.toBillingPurchaseState(), - purchaseTime = purchase.purchaseTime, - purchaseToken = purchase.purchaseToken, - isAcknowledged = purchase.isAcknowledged, - isAutoRenewing = purchase.isAutoRenewing - ) + return handlePurchaseResult(result.billingResult, result.purchasesList) } /** @@ -369,6 +279,92 @@ internal class BillingApiImpl( } } + private fun handlePurchaseResult(billingResult: BillingResult, purchases: List?): BillingPurchaseResult { + return when (billingResult.responseCode) { + BillingResponseCode.OK -> { + if (purchases == null) { + Log.d(TAG, "handlePurchaseResult: No purchases.", true) + BillingPurchaseResult.None + } else { + Log.d(TAG, "handlePurchaseResult: ${purchases.size} purchases.", true) + val newestPurchase = purchases.maxByOrNull { it.purchaseTime } + if (newestPurchase == null) { + Log.d(TAG, "handlePurchaseResult: no purchase.", true) + BillingPurchaseResult.None + } else { + Log.d(TAG, "handlePurchaseResult: successful purchase at ${newestPurchase.purchaseTime}", true) + BillingPurchaseResult.Success( + purchaseState = newestPurchase.purchaseState.toBillingPurchaseState(), + purchaseToken = newestPurchase.purchaseToken, + isAcknowledged = newestPurchase.isAcknowledged, + purchaseTime = newestPurchase.purchaseTime, + isAutoRenewing = newestPurchase.isAutoRenewing + ) + } + } + } + + BillingResponseCode.BILLING_UNAVAILABLE -> { + Log.d(TAG, "handlePurchaseResult: Billing unavailable.", true) + BillingPurchaseResult.BillingUnavailable + } + + BillingResponseCode.USER_CANCELED -> { + Log.d(TAG, "handlePurchaseResult: User cancelled.", true) + BillingPurchaseResult.UserCancelled + } + + BillingResponseCode.ERROR -> { + Log.d(TAG, "handlePurchaseResult: error.", true) + BillingPurchaseResult.GenericError + } + + BillingResponseCode.NETWORK_ERROR -> { + Log.d(TAG, "handlePurchaseResult: Network error.", true) + BillingPurchaseResult.NetworkError + } + + BillingResponseCode.DEVELOPER_ERROR -> { + Log.d(TAG, "handlePurchaseResult: Developer error.", true) + BillingPurchaseResult.GenericError + } + + BillingResponseCode.FEATURE_NOT_SUPPORTED -> { + Log.d(TAG, "handlePurchaseResult: Feature not supported.", true) + BillingPurchaseResult.FeatureNotSupported + } + + BillingResponseCode.ITEM_ALREADY_OWNED -> { + Log.d(TAG, "handlePurchaseResult: Already owned.", true) + BillingPurchaseResult.AlreadySubscribed + } + + BillingResponseCode.ITEM_NOT_OWNED -> { + error("This shouldn't happen during the purchase process") + } + + BillingResponseCode.ITEM_UNAVAILABLE -> { + Log.d(TAG, "handlePurchaseResult: Item is unavailable", true) + BillingPurchaseResult.TryAgainLater + } + + BillingResponseCode.SERVICE_UNAVAILABLE -> { + Log.d(TAG, "handlePurchaseResult: Service is unavailable.", true) + BillingPurchaseResult.TryAgainLater + } + + BillingResponseCode.SERVICE_DISCONNECTED -> { + Log.d(TAG, "handlePurchaseResult: Service is disconnected.", true) + BillingPurchaseResult.TryAgainLater + } + + else -> { + Log.d(TAG, "handlePurchaseResult: No purchases.", true) + BillingPurchaseResult.None + } + } + } + private class BillingListener( private val onStateUpdate: (State) -> Unit ) : BillingClientStateListener {