mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Add job after registration to try to redeem subscription data.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user