diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt index 4cea7e158f..f7c4e71101 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/remote/RemoteBackupsSettingsViewModel.kt @@ -215,8 +215,6 @@ class RemoteBackupsSettingsViewModel : ViewModel() { } } - // TODO [backups] - handle other cases. - return } 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 9b58baac76..a2358d757e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -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