diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt new file mode 100644 index 0000000000..7f73a1e949 --- /dev/null +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt @@ -0,0 +1,653 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import assertk.assertThat +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.signal.core.util.billing.BillingProduct +import org.signal.core.util.billing.BillingPurchaseResult +import org.signal.core.util.billing.BillingPurchaseState +import org.signal.core.util.money.FiatMoney +import org.signal.donations.InAppPaymentType +import org.thoughtcrime.securesms.backup.DeletionState +import org.thoughtcrime.securesms.backup.v2.BackupRepository +import org.thoughtcrime.securesms.backup.v2.MessageBackupTier +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.Job +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.testing.SignalActivityRule +import org.thoughtcrime.securesms.util.RemoteConfig +import org.whispersystems.signalservice.api.NetworkResult +import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException +import org.whispersystems.signalservice.api.storage.IAPSubscriptionId +import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription +import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription.ChargeFailure +import org.whispersystems.signalservice.api.subscriptions.SubscriberId +import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration +import java.math.BigDecimal +import java.util.Currency +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.milliseconds + +@RunWith(AndroidJUnit4::class) +class BackupSubscriptionCheckJobTest { + + companion object { + const val IAP_TOKEN = "test_token" + } + + @get:Rule + val harness = SignalActivityRule() + + @Before + fun setUp() { + mockkObject(RemoteConfig) + every { RemoteConfig.messageBackups } returns true + every { RemoteConfig.internalUser } returns true + + coEvery { AppDependencies.billingApi.isApiAvailable() } returns true + coEvery { AppDependencies.billingApi.queryPurchases() } returns mockk() + coEvery { AppDependencies.billingApi.queryProduct() } returns null + + SignalStore.backup.backupTier = MessageBackupTier.PAID + + mockkObject(RecurringInAppPaymentRepository) + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription() + ) + + mockkObject(BackupRepository) + every { BackupRepository.getBackupTier() } answers { + val tier = SignalStore.backup.backupTier + if (tier != null) { + NetworkResult.Success(tier) + } else { + NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(404)) + } + } + + every { BackupRepository.getBackupTierWithoutDowngrade() } answers { + val tier = SignalStore.backup.backupTier + if (tier != null) { + NetworkResult.Success(tier) + } else { + NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(404)) + } + } + + every { AppDependencies.donationsApi.putSubscription(any()) } returns NetworkResult.Success(Unit) + + insertSubscriber() + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun givenDefaultConfiguration_whenIRun_thenIExpectToQueryPurchases() { + val job = BackupSubscriptionCheckJob.create() + job.run() + + coVerify { + AppDependencies.billingApi.queryPurchases() + } + } + + @Test + fun givenUserIsNotRegistered_whenIRun_thenIExpectSuccessAndEarlyExit() { + mockkObject(SignalStore.account) { + every { SignalStore.account.isRegistered } returns false + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + } + + @Test + fun givenIsLinkedDevice_whenIRun_thenIExpectSuccessAndEarlyExit() { + mockkObject(SignalStore.account) { + every { SignalStore.account.isLinkedDevice } returns true + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + } + + @Test + fun givenRemoteBackupsNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() { + every { RemoteConfig.messageBackups } returns false + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + + @Test + fun givenBillingApiNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() { + coEvery { AppDependencies.billingApi.isApiAvailable() } returns false + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + + @Test + fun givenDeletionStateIsNotNone_whenIRun_thenIExpectSuccessAndEarlyExit() { + DeletionState.entries.filter { it != DeletionState.NONE }.forEach { deletionState -> + SignalStore.backup.deletionState = deletionState + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + } + + @Test + fun givenBackupsAreNotEnabled_whenIRun_thenIExpectSuccessAndEarlyExit() { + SignalStore.backup.backupTier = null + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + + @Test + fun givenInternalOverrideIsSet_whenIRun_thenIExpectSuccessAndEarlyExit() { + SignalStore.backup.backupTierInternalOverride = MessageBackupTier.PAID + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + + @Test + fun givenAPendingPayment_whenIRun_thenIExpectSuccessAndEarlyExit() { + mockProduct() + insertPendingInAppPayment() + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenInactiveSubscription_whenIRun_thenIExpectStateMismatchDetected() { + mockProduct() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = false) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isTrue() + } + + @Test + fun givenRepositoryFailure_whenIRun_thenIExpectFailureResult() { + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.failure( + RuntimeException("Network error") + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isFailure).isTrue() + } + + @Test + fun givenBillingApiReturnsAFailure_whenIRun_thenIExpectFailureResult() { + coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.BillingUnavailable + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isFailure).isTrue() + } + + @Test + fun givenPastDueSubscription_whenIRun_thenIExpectStateMismatchDetected() { + mockProduct() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription( + isActive = false, + billingPeriodEndSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds - 1.days.inWholeSeconds, + status = "past_due" + ) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isTrue() + } + + @Test + fun givenCancelledSubscription_whenIRun_thenIExpectStateMismatchDetected() { + mockProduct() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription( + isActive = false, + status = "canceled", + cancelled = true + ) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isTrue() + } + + @Test + fun givenFreeBackupTier_whenIRun_thenIExpectSuccessAndEarlyExit() { + mockProduct() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + ActiveSubscription.EMPTY + ) + + SignalStore.backup.backupTier = MessageBackupTier.FREE + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenFailedInAppPayment_whenIRun_thenIExpectStateMismatchDetected() { + mockProduct() + insertFailedInAppPayment() + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isTrue() + } + + @Test + fun givenActiveSignalSubscriptionWithTokenMismatch_whenIRun_thenIExpectTokenRedemption() { + mockProduct() + mockActivePurchase() + insertSubscriber("mismatch") + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + verify { + RecurringInAppPaymentRepository.ensureSubscriberIdSync( + eq(InAppPaymentSubscriberRecord.Type.BACKUP), + eq(true), + eq(IAPSubscriptionId.GooglePlayBillingPurchaseToken(purchaseToken = "test_token")) + ) + } + } + + @Test + fun givenActiveSubscriptionAndPurchaseWithoutEntitlement_whenIRun_thenIExpectRedemption() { + mockProduct() + mockActivePurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true) + ) + + // Set backup tier to FREE (no paid entitlement) + SignalStore.backup.backupTier = MessageBackupTier.FREE + + val job = BackupSubscriptionCheckJob.create() + + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + verify { + RecurringInAppPaymentRepository.ensureSubscriberIdSync( + eq(InAppPaymentSubscriberRecord.Type.BACKUP), + eq(true), + eq(IAPSubscriptionId.GooglePlayBillingPurchaseToken(purchaseToken = "test_token")) + ) + } + } + + @Test + fun givenValidActiveState_whenIRun_thenIExpectSuccessAndNoMismatch() { + mockProduct() + mockActivePurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true) + ) + + SignalStore.backup.backupTier = MessageBackupTier.PAID + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenValidInactiveState_whenIRun_thenIExpectSuccessAndNoMismatch() { + mockProduct() + mockInactivePurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = false) + ) + + // Set up valid inactive state: no paid tier + no active subscription + no active purchase + SignalStore.backup.backupTier = MessageBackupTier.FREE + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenGooglePlayBillingCanceledWithoutActiveSignalSubscription_whenIRun_thenIExpectValidCancelState() { + mockProduct() + mockCanceledPurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = false) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenGooglePlayBillingCanceledWithFailedSignalSubscription_whenIRun_thenIExpectValidCancelState() { + mockProduct() + mockCanceledPurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true, status = "past_due", chargeFailure = ChargeFailure("test", "", "", "", "")) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenInvalidStateConfiguration_whenIRun_thenIExpectStateMismatchDetected() { + mockProduct() + mockActivePurchase() + + // Create invalid state: active purchase but no active subscription, with paid tier + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = false) + ) + + SignalStore.backup.backupTier = MessageBackupTier.PAID + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isTrue() + } + + @Test + fun givenActiveSubscriptionWithMismatchedZkCredentials_whenIRun_thenIExpectCredentialRefresh() { + mockProduct() + mockActivePurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true) + ) + + // Set up mismatched state: local tier is PAID but ZK tier is FREE + SignalStore.backup.backupTier = MessageBackupTier.PAID + every { BackupRepository.getBackupTierWithoutDowngrade() } returns NetworkResult.Success(MessageBackupTier.FREE) + every { BackupRepository.resetInitializedStateAndAuthCredentials() } returns Unit + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + verify { BackupRepository.resetInitializedStateAndAuthCredentials() } + verify { BackupRepository.getBackupTier() } + } + + @Test + fun givenActiveSubscriptionWithSyncedZkCredentials_whenIRun_thenIExpectNoCredentialRefresh() { + mockProduct() + mockActivePurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true) + ) + + // Set up synced state: both local and ZK tiers are PAID + SignalStore.backup.backupTier = MessageBackupTier.PAID + every { BackupRepository.getBackupTierWithoutDowngrade() } returns NetworkResult.Success(MessageBackupTier.PAID) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + verify(exactly = 0) { BackupRepository.resetInitializedStateAndAuthCredentials() } + } + + @Test + fun givenActiveSubscriptionWithZkCredentialFailure_whenIRun_thenIExpectCredentialRefresh() { + mockProduct() + mockActivePurchase() + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true) + ) + + SignalStore.backup.backupTier = MessageBackupTier.PAID + // ZK credential fetch fails, should trigger refresh + every { BackupRepository.getBackupTierWithoutDowngrade() } returns NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(500)) + every { BackupRepository.resetInitializedStateAndAuthCredentials() } returns Unit + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + verify { BackupRepository.resetInitializedStateAndAuthCredentials() } + } + + @Test + fun givenSubscriptionWillCancelAtPeriodEnd_whenIRun_thenIExpectValidCancelState() { + mockProduct() + mockCanceledPurchase() + + // Create subscription that will cancel at period end + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true, cancelled = true) // cancelled = true means willCancelAtPeriodEnd + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenActiveSubscriptionNotWillCancelAtPeriodEnd_whenIRun_thenIExpectZkSynchronization() { + mockProduct() + mockActivePurchase() + + // Create active subscription that won't cancel at period end + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + createActiveSubscription(isActive = true, cancelled = false) + ) + + SignalStore.backup.backupTier = MessageBackupTier.PAID + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + // Should call ZK synchronization since subscription is active and not canceling + verify { BackupRepository.getBackupTierWithoutDowngrade() } + } + + private fun createActiveSubscription( + isActive: Boolean = true, + billingPeriodEndSeconds: Long = 2147472000, + status: String = "active", + cancelled: Boolean = false, + chargeFailure: ChargeFailure? = null + ): ActiveSubscription { + return ActiveSubscription( + ActiveSubscription.Subscription( + SubscriptionsConfiguration.BACKUPS_LEVEL, + "USD", + BigDecimal(42), + billingPeriodEndSeconds, + isActive, + 2147472000, + cancelled, + status, + "USA", + "credit-card", + false + ), + chargeFailure + ) + } + + private fun mockProduct() { + coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct( + price = FiatMoney( + BigDecimal.ONE, + Currency.getInstance("USD") + ) + ) + } + + private fun insertSubscriber(token: String = IAP_TOKEN) { + SignalDatabase.inAppPaymentSubscribers.insertOrReplace( + InAppPaymentSubscriberRecord( + type = InAppPaymentSubscriberRecord.Type.BACKUP, + iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken(token), + requiresCancel = false, + paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING, + currency = null, + subscriberId = SubscriberId.generate() + ) + ) + } + + private fun insertPendingInAppPayment() { + SignalDatabase.inAppPayments.insert( + type = InAppPaymentType.RECURRING_BACKUP, + state = InAppPaymentTable.State.PENDING, + subscriberId = null, + endOfPeriod = null, + inAppPaymentData = InAppPaymentData() + ) + } + + private fun assertEarlyExit(result: Job.Result) { + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + coVerify(atLeast = 0, atMost = 0) { + AppDependencies.billingApi.queryPurchases() + } + } + + private fun insertFailedInAppPayment() { + SignalDatabase.inAppPayments.insert( + type = InAppPaymentType.RECURRING_BACKUP, + state = InAppPaymentTable.State.END, + subscriberId = null, + endOfPeriod = null, + inAppPaymentData = InAppPaymentData( + error = InAppPaymentData.Error( + type = InAppPaymentData.Error.Type.PAYMENT_SETUP + ) + ) + ) + } + + private fun mockActivePurchase() { + coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success( + purchaseState = BillingPurchaseState.PURCHASED, + purchaseToken = IAP_TOKEN, + isAcknowledged = true, + purchaseTime = System.currentTimeMillis(), + isAutoRenewing = true + ) + } + + private fun mockInactivePurchase() { + coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None + } + + private fun mockCanceledPurchase() { + coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success( + purchaseState = BillingPurchaseState.PURCHASED, + purchaseToken = IAP_TOKEN, + isAcknowledged = true, + purchaseTime = System.currentTimeMillis(), + isAutoRenewing = false // Not auto-renewing means canceled + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt index 82045f81e6..a99149e361 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsFlowViewModel.kt @@ -167,7 +167,7 @@ class MessageBackupsFlowViewModel( } activeSubscription.onSuccess { subscription -> - if (subscription.isCanceled) { + if (subscription.willCancelAtPeriodEnd()) { Log.d(TAG, "Active subscription is cancelled. Clearing tier.") internalStateFlow.update { it.copy( 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 1356582947..29c5dcbecf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -138,7 +138,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C checkForFailedOrCanceledSubscriptionState(activeSubscription) - val isSignalSubscriptionFailedOrCanceled = activeSubscription?.isFailedPayment == true || activeSubscription?.isCanceled == true + val isSignalSubscriptionFailedOrCanceled = activeSubscription?.willCancelAtPeriodEnd() == true if (hasActiveSignalSubscription && !isSignalSubscriptionFailedOrCanceled) { checkAndSynchronizeZkCredentialTierWithStoredLocalTier() } @@ -216,8 +216,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C * the "download your data" notifier sheet. */ private fun checkForFailedOrCanceledSubscriptionState(activeSubscription: ActiveSubscription?) { - val containsFailedPaymentOrCancellation = activeSubscription?.isFailedPayment == true || activeSubscription?.isCanceled == true - if (containsFailedPaymentOrCancellation && activeSubscription?.activeSubscription != null) { + if (activeSubscription?.willCancelAtPeriodEnd() == true && activeSubscription?.activeSubscription != null) { Log.i(TAG, "Subscription either has a payment failure or has been canceled.") val response = SignalNetwork.account.whoAmI() diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java index 0a8dc3c618..0ca73187a6 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/subscriptions/ActiveSubscription.java @@ -181,6 +181,13 @@ public final class ActiveSubscription { return activeSubscription != null && activeSubscription.isCanceled(); } + /** + * Backups-specific call that gives us a value that should align with autoRenew from the GPB payment. + */ + public boolean willCancelAtPeriodEnd() { + return activeSubscription == null || activeSubscription.willCancelAtPeriodEnd; + } + public static final class Subscription { private final int level; private final String currency;