diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt index dab6d69a4c..698599f2c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/InAppPaymentsRepository.kt @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation import org.thoughtcrime.securesms.dependencies.AppDependencies +import org.thoughtcrime.securesms.jobmanager.Job import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.storage.StorageSyncHelper @@ -223,10 +224,14 @@ object InAppPaymentsRepository { * Returns a duration to utilize for jobs tied to different payment methods. For long running bank transfers, we need to * allow extra time for completion. */ - fun resolveContextJobLifespan(inAppPayment: InAppPaymentTable.InAppPayment): Duration { - return when (inAppPayment.data.paymentMethodType) { - InAppPaymentData.PaymentMethodType.SEPA_DEBIT, InAppPaymentData.PaymentMethodType.IDEAL -> 30.days - else -> 1.days + fun resolveContextJobLifespanMillis(inAppPayment: InAppPaymentTable.InAppPayment): Long { + return if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) { + Job.Parameters.IMMORTAL + } else { + when (inAppPayment.data.paymentMethodType) { + InAppPaymentData.PaymentMethodType.SEPA_DEBIT, InAppPaymentData.PaymentMethodType.IDEAL -> 30.days.inWholeMilliseconds + else -> 1.days.inWholeMilliseconds + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt index e01bd09c59..b480339d5c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/InAppPaymentTable.kt @@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.util.parcelers.MillisecondDurationParceler import org.thoughtcrime.securesms.util.parcelers.NullableSubscriberIdParceler import org.whispersystems.signalservice.api.subscriptions.SubscriberId import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -184,6 +185,20 @@ class InAppPaymentTable(context: Context, databaseHelper: SignalDatabase) : Data AppDependencies.databaseObserver.notifyInAppPaymentsObservers(inAppPayment) } + fun getOldPendingPayments(type: InAppPaymentType): List { + val oneDayAgo = System.currentTimeMillis().milliseconds - 24.hours + return readableDatabase + .select() + .from(TABLE_NAME) + .where( + "$STATE = ? AND $TYPE = ? AND $UPDATED_AT <= ${oneDayAgo.inWholeSeconds}", + State.serialize(State.PENDING), + InAppPaymentType.serialize(type) + ) + .run() + .readToList(mapper = InAppPayment::deserialize) + } + /** * Returns true if the user has submitted a pre-pending recurring donation. * In this state, the user would have had to cancel their subscription or be in the process of trying 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 fc49b9c1a3..5792de127b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -174,7 +174,11 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C val isGooglePlayBillingCanceled = purchase is BillingPurchaseResult.Success && !purchase.isAutoRenewing if (isGooglePlayBillingCanceled && (!hasActiveSignalSubscription || isSignalSubscriptionFailedOrCanceled)) { - Log.i(TAG, "Valid cancel state. Clearing mismatch. (isGooglePlayBillingCanceled: true, hasActiveSignalSubscription: $hasActiveSignalSubscription, isSignalSubscriptionFailedOrCanceled: $isSignalSubscriptionFailedOrCanceled", true) + Log.i( + TAG, + "Valid cancel state. Clearing mismatch. (isGooglePlayBillingCanceled: true, hasActiveSignalSubscription: $hasActiveSignalSubscription, isSignalSubscriptionFailedOrCanceled: $isSignalSubscriptionFailedOrCanceled", + true + ) SignalStore.backup.subscriptionStateMismatchDetected = false return Result.success() } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt index efe4e5674e..338d0d4c73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentKeepAliveJob.kt @@ -151,6 +151,8 @@ class InAppPaymentKeepAliveJob private constructor( return } + clearOldPendingPaymentsIfRequired() + val activeInAppPayment = getActiveInAppPayment(subscriber, subscription) if (activeInAppPayment == null) { warn(type, "Failed to generate active in-app payment. Exiting") @@ -227,6 +229,38 @@ class InAppPaymentKeepAliveJob private constructor( } } + /** + * If we have a pending payment that is at least 24 hours old AND we have no jobs + * enqueued that are associated with it, then something weird has happened. We should + * fail the job and let the keep-alive check create a new one as necessary. + */ + private fun clearOldPendingPaymentsIfRequired() { + val inAppPayments = SignalDatabase.inAppPayments.getOldPendingPayments(type.inAppPaymentType) + + inAppPayments.forEach { inAppPayment -> + val queue = InAppPaymentsRepository.resolveJobQueueKey(inAppPayment) + + if (AppDependencies.jobManager.isQueueEmpty(queue)) { + Log.i(TAG, "User has an aged-out pending in-app payment [${inAppPayment.id}][${inAppPayment.type}]. Marking failed and proceeding with check.") + SignalDatabase.inAppPayments.update( + inAppPayment = inAppPayment.copy( + notified = true, + endOfPeriod = 0.seconds, + subscriberId = null, + state = InAppPaymentTable.State.END, + data = inAppPayment.data.newBuilder() + .error( + InAppPaymentData.Error( + type = InAppPaymentData.Error.Type.REDEMPTION + ) + ) + .build() + ) + ) + } + } + } + private fun getActiveInAppPayment( subscriber: InAppPaymentSubscriberRecord, subscription: ActiveSubscription.Subscription diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt index d55264748d..2af6e49e15 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentOneTimeContextJob.kt @@ -54,7 +54,7 @@ class InAppPaymentOneTimeContextJob private constructor( parameters = Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setQueue(InAppPaymentsRepository.resolveJobQueueKey(inAppPayment)) - .setLifespan(InAppPaymentsRepository.resolveContextJobLifespan(inAppPayment).inWholeMilliseconds) + .setLifespan(InAppPaymentsRepository.resolveContextJobLifespanMillis(inAppPayment)) .setMaxAttempts(Parameters.UNLIMITED) .build() ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt index b62a5d6a28..34efa73421 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentRecurringContextJob.kt @@ -61,7 +61,7 @@ class InAppPaymentRecurringContextJob private constructor( parameters = Parameters.Builder() .addConstraint(NetworkConstraint.KEY) .setQueue(InAppPaymentsRepository.resolveJobQueueKey(inAppPayment)) - .setLifespan(InAppPaymentsRepository.resolveContextJobLifespan(inAppPayment).inWholeMilliseconds) + .setLifespan(InAppPaymentsRepository.resolveContextJobLifespanMillis(inAppPayment)) .setMaxAttempts(Parameters.UNLIMITED) .build() ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentSetupJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentSetupJob.kt index a74b6a9d4d..61333e32fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentSetupJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/InAppPaymentSetupJob.kt @@ -44,7 +44,7 @@ abstract class InAppPaymentSetupJob( protected fun getParameters(inAppPayment: InAppPaymentTable.InAppPayment): Parameters { return Parameters.Builder() .setQueue(InAppPaymentsRepository.resolveJobQueueKey(inAppPayment)) - .setLifespan(InAppPaymentsRepository.resolveContextJobLifespan(inAppPayment).inWholeMilliseconds) + .setLifespan(InAppPaymentsRepository.resolveContextJobLifespanMillis(inAppPayment)) .setMaxAttempts(Parameters.UNLIMITED) .build() }