Add job after registration to try to redeem subscription data.

This commit is contained in:
Alex Hart
2025-07-30 13:26:06 -03:00
committed by GitHub
parent 65e114e55f
commit 1f243bca74
6 changed files with 182 additions and 0 deletions

View File

@@ -14,6 +14,7 @@ import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequestContext
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toInAppPaymentDataChargeFailure
@@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager.Chain
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
@@ -226,6 +228,19 @@ class InAppPaymentRecurringContextJob private constructor(
return false
}
val tier = when (val result = BackupRepository.getBackupTier()) {
is NetworkResult.Success -> result.result
else -> {
warning("Failed to get backup tier via zk check.")
MessageBackupTier.FREE
}
}
if (tier != MessageBackupTier.PAID) {
warning("ZK credential does not align with entitlement. Forcing a redemption.")
return false
}
val backupExpirationSeconds = whoAmIResponse.entitlements?.backup?.expirationSeconds ?: return false
backupExpirationSeconds >= endOfCurrentSubscriptionPeriod

View File

@@ -228,6 +228,7 @@ public final class JobManagerFactories {
put(PushGroupSendJob.KEY, new PushGroupSendJob.Factory());
put(PushGroupSilentUpdateSendJob.KEY, new PushGroupSilentUpdateSendJob.Factory());
put(MessageFetchJob.KEY, new MessageFetchJob.Factory());
put(PostRegistrationBackupRedemptionJob.KEY, new PostRegistrationBackupRedemptionJob.Factory());
put(PushProcessEarlyMessagesJob.KEY, new PushProcessEarlyMessagesJob.Factory());
put(PushProcessMessageErrorJob.KEY, new PushProcessMessageErrorJob.Factory());
put(PushProcessMessageJob.KEY, new PushProcessMessageJob.Factory());

View File

@@ -0,0 +1,151 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.DeletionState
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.internal.push.SubscriptionsConfiguration
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
/**
* Runs after registration to make sure we are on the backup level we expect on this device.
*/
class PostRegistrationBackupRedemptionJob : CoroutineJob {
companion object {
private val TAG = Log.tag(PostRegistrationBackupRedemptionJob::class)
const val KEY = "PostRestoreBackupRedemptionJob"
}
constructor() : super(
Parameters.Builder()
.setQueue(InAppPaymentsRepository.getRecurringJobQueueKey(InAppPaymentType.RECURRING_BACKUP))
.addConstraint(NetworkConstraint.KEY)
.setMaxAttempts(Parameters.UNLIMITED)
.setLifespan(Parameters.IMMORTAL)
.build()
)
constructor(parameters: Parameters) : super(parameters)
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override suspend fun doRun(): Result {
if (!SignalStore.account.isRegistered) {
info("User is not registered. Exiting.")
return Result.success()
}
if (!RemoteConfig.messageBackups) {
info("Message backups feature is not available. Exiting.")
return Result.success()
}
if (SignalStore.backup.deletionState != DeletionState.NONE) {
info("User is in the process of or has delete their backup. Exiting.")
return Result.success()
}
if (SignalStore.backup.backupTier != MessageBackupTier.PAID) {
info("Paid backups are not enabled on this device. Exiting.")
return Result.success()
}
if (SignalStore.backup.backupTierInternalOverride != null) {
info("User has internal override set for backup version. Exiting.")
return Result.success()
}
if (SignalDatabase.inAppPayments.hasPendingBackupRedemption()) {
info("User has a pending backup redemption. Retrying later.")
return Result.retry(defaultBackoff())
}
val subscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
if (subscriber == null) {
info("No subscriber information was available in the database. Exiting.")
return Result.success()
}
info("Attempting to grab price information for records...")
val subscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription
val emptyPrice = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault()))
val price: FiatMoney = if (subscription != null) {
FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency))
} else if (AppDependencies.billingApi.isApiAvailable()) {
AppDependencies.billingApi.queryProduct()?.price ?: emptyPrice
} else {
emptyPrice
}
if (price == emptyPrice) {
warning("Could not resolve price, using empty price.")
}
info("Creating a pending payment...")
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_BACKUP,
state = InAppPaymentTable.State.PENDING,
subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
badge = null,
amount = price.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
)
)
)
info("Submitting job chain.")
InAppPaymentPurchaseTokenJob.createJobChain(
inAppPayment = SignalDatabase.inAppPayments.getById(id)!!
).enqueue()
return Result.success()
}
override fun onFailure() = Unit
private fun info(message: String, throwable: Throwable? = null) {
Log.i(TAG, message, throwable, true)
}
private fun warning(message: String, throwable: Throwable? = null) {
Log.w(TAG, message, throwable, true)
}
class Factory : Job.Factory<PostRegistrationBackupRedemptionJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): PostRegistrationBackupRedemptionJob {
return PostRegistrationBackupRedemptionJob(parameters)
}
}
}

View File

@@ -6,10 +6,12 @@
package org.thoughtcrime.securesms.registration.util;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ArchiveBackupIdReservationJob;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.PostRegistrationBackupRedemptionJob;
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
import org.thoughtcrime.securesms.jobs.StorageSyncJob;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode;
@@ -51,6 +53,10 @@ public final class RegistrationUtil {
.then(new DirectoryRefreshJob(false))
.enqueue();
if (SignalStore.backup().getBackupTier() == MessageBackupTier.PAID) {
AppDependencies.getJobManager().add(new PostRegistrationBackupRedemptionJob());
}
SignalStore.emoji().clearSearchIndexMetadata();
EmojiSearchIndexDownloadJob.scheduleImmediately();

View File

@@ -20,6 +20,8 @@ import org.robolectric.annotation.Config
import org.signal.core.util.logging.Log
import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toInAppPaymentDataChargeFailure
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsTestRule
@@ -35,6 +37,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
import org.thoughtcrime.securesms.testutil.MockSignalStoreRule
import org.thoughtcrime.securesms.testutil.SystemOutLogger
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
@@ -439,6 +442,9 @@ class InAppPaymentRecurringContextJobTest {
)
}
mockkObject(BackupRepository)
every { BackupRepository.getBackupTier() } returns NetworkResult.Success(MessageBackupTier.PAID)
val iap = insertInAppPayment(
type = InAppPaymentType.RECURRING_BACKUP
)

View File

@@ -27,6 +27,7 @@ import org.robolectric.annotation.Config
import org.signal.core.util.logging.Log.initialize
import org.thoughtcrime.securesms.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.Skipped
import org.thoughtcrime.securesms.keyvalue.Start
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -54,6 +55,8 @@ class RegistrationUtilTest {
logRecorder = LogRecorder()
initialize(logRecorder)
every { SignalStore.backup.backupTier } returns null
}
@After