Claim IAP subscription if we detect a payment token mismatch.

This commit is contained in:
Alex Hart 2025-01-10 11:21:15 -04:00 committed by Greyson Parrelli
parent fe5de65273
commit e5e74967dc
2 changed files with 68 additions and 7 deletions

View file

@ -215,8 +215,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
}
// TODO [backups] - handle other cases.
return
}

View file

@ -6,22 +6,29 @@
package org.thoughtcrime.securesms.jobs
import androidx.annotation.VisibleForTesting
import org.signal.core.util.billing.BillingProduct
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
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.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.CoroutineJob
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import kotlin.concurrent.withLock
/**
@ -85,6 +92,12 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases()
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
val product: BillingProduct? = AppDependencies.billingApi.queryProduct()
if (product == null) {
Log.w(TAG, "Google Play Billing product not available. Exiting.")
return Result.failure()
}
InAppPaymentSubscriberRecord.Type.BACKUP.lock.withLock {
val inAppPayment = SignalDatabase.inAppPayments.getLatestInAppPaymentByType(InAppPaymentType.RECURRING_BACKUP)
@ -107,18 +120,68 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C
val hasValidActiveState = hasActivePaidBackupTier && hasActiveSignalSubscription && hasActivePurchase
val hasValidInactiveState = !hasActivePaidBackupTier && !hasActiveSignalSubscription && !hasActivePurchase
if (hasValidActiveState || hasValidInactiveState) {
Log.i(TAG, "Valid state: (hasValidActiveState: $hasValidActiveState, hasValidInactiveState: $hasValidInactiveState). Clearing mismatch value and exiting.", true)
val purchaseToken = if (hasActivePurchase) {
(purchase as BillingPurchaseResult.Success).purchaseToken
} else {
null
}
if (purchaseToken?.let { hasLocalDevicePurchaseTokenMismatch(purchaseToken) } == true) {
Log.i(TAG, "Encountered token mismatch. Attempting to redeem.")
enqueueRedemptionForNewToken(purchaseToken, product.price)
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()
if (hasValidActiveState || hasValidInactiveState) {
Log.i(TAG, "Valid state: (hasValidActiveState: $hasValidActiveState, hasValidInactiveState: $hasValidInactiveState). Clearing mismatch value and exiting.", 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()
}
}
}
}
private fun enqueueRedemptionForNewToken(localDevicePurchaseToken: String, localProductPrice: FiatMoney) {
RecurringInAppPaymentRepository.ensureSubscriberId(
subscriberType = InAppPaymentSubscriberRecord.Type.BACKUP,
isRotation = true,
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken(localDevicePurchaseToken)
).blockingAwait()
SignalDatabase.inAppPayments.clearCreated()
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_BACKUP,
state = InAppPaymentTable.State.CREATED,
subscriberId = null,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
badge = null,
amount = localProductPrice.toFiatValue(),
level = SubscriptionsConfiguration.BACKUPS_LEVEL.toLong(),
recipientId = Recipient.self().id.serialize(),
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
redemption = InAppPaymentData.RedemptionState(
stage = InAppPaymentData.RedemptionState.Stage.INIT
)
)
)
InAppPaymentRecurringContextJob.createJobChain(
inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
).enqueue()
}
private fun hasLocalDevicePurchaseTokenMismatch(localDevicePurchaseToken: String): Boolean {
val subscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
return subscriber?.iapSubscriptionId?.purchaseToken != localDevicePurchaseToken
}
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY