diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt index f7162da0f4..f6f35c81cb 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.testing.runSync import org.thoughtcrime.securesms.testing.success import org.whispersystems.signalservice.api.SignalServiceDataStore import org.whispersystems.signalservice.api.SignalServiceMessageSender +import org.whispersystems.signalservice.api.account.AccountApi import org.whispersystems.signalservice.api.archive.ArchiveApi import org.whispersystems.signalservice.api.attachment.AttachmentApi import org.whispersystems.signalservice.api.donations.DonationsApi @@ -56,6 +57,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application, private val recipientCache: LiveRecipientCache private var signalServiceMessageSender: SignalServiceMessageSender? = null private var billingApi: BillingApi = mockk() + private var accountApi: AccountApi = mockk() init { runSync { @@ -118,6 +120,8 @@ class InstrumentationApplicationDependencyProvider(val application: Application, override fun provideBillingApi(): BillingApi = billingApi + override fun provideAccountApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): AccountApi = accountApi + override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess { return serviceNetworkAccessMock } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt index 95814529c0..706fdb8e0a 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJobTest.kt @@ -12,10 +12,10 @@ 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 okio.IOException import org.junit.After import org.junit.Before import org.junit.Rule @@ -30,6 +30,7 @@ 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.backups.BackupStateObserver import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase @@ -38,6 +39,7 @@ 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.net.SignalNetwork import org.thoughtcrime.securesms.testing.SignalActivityRule import org.thoughtcrime.securesms.util.RemoteConfig import org.whispersystems.signalservice.api.NetworkResult @@ -47,6 +49,7 @@ 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 org.whispersystems.signalservice.internal.push.WhoAmIResponse import java.math.BigDecimal import java.util.Currency import kotlin.time.Duration.Companion.days @@ -68,13 +71,21 @@ class BackupSubscriptionCheckJobTest { every { RemoteConfig.internalUser } returns true coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK - coEvery { AppDependencies.billingApi.queryPurchases() } returns mockk() - coEvery { AppDependencies.billingApi.queryProduct() } returns null + + coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success( + purchaseState = BillingPurchaseState.PURCHASED, + purchaseToken = "test-token", + isAcknowledged = true, + isAutoRenewing = true, + purchaseTime = System.currentTimeMillis() + ) + + coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD"))) SignalStore.backup.backupTier = MessageBackupTier.PAID mockkObject(RecurringInAppPaymentRepository) - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription() ) @@ -97,6 +108,18 @@ class BackupSubscriptionCheckJobTest { } } + every { BackupRepository.resetInitializedStateAndAuthCredentials() } returns Unit + + mockkObject(BackupStateObserver) + every { BackupStateObserver.notifyBackupStateChanged() } returns Unit + + mockkObject(SignalNetwork) + every { AppDependencies.accountApi.whoAmI() } returns NetworkResult.Success( + WhoAmIResponse( + number = "+1234567890" + ) + ) + every { AppDependencies.donationsApi.putSubscription(any()) } returns NetworkResult.Success(Unit) insertSubscriber() @@ -173,9 +196,18 @@ class BackupSubscriptionCheckJobTest { assertEarlyExit(result) } + @Test + fun givenPrePendingRecurringTransaction_whenIRun_thenIExpectSuccessAndEarlyExit() { + insertPrePendingInAppPayment() + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertEarlyExit(result) + } + @Test fun givenAPendingPayment_whenIRun_thenIExpectSuccessAndEarlyExit() { - mockProduct() insertPendingInAppPayment() val job = BackupSubscriptionCheckJob.create() @@ -187,9 +219,7 @@ class BackupSubscriptionCheckJobTest { @Test fun givenInactiveSubscription_whenIRun_thenIExpectStateMismatchDetected() { - mockProduct() - - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = false) ) @@ -201,9 +231,9 @@ class BackupSubscriptionCheckJobTest { } @Test - fun givenRepositoryFailure_whenIRun_thenIExpectFailureResult() { - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.failure( - RuntimeException("Network error") + fun givenAnApplicationErrorWhenAccessingTheActiveSubscription_whenIRun_thenIExpectAFailure() { + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.ApplicationError( + RuntimeException("Application Error") ) val job = BackupSubscriptionCheckJob.create() @@ -213,8 +243,10 @@ class BackupSubscriptionCheckJobTest { } @Test - fun givenBillingApiReturnsAFailure_whenIRun_thenIExpectFailureResult() { - coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.BillingUnavailable + fun givenANetworkErrorWhenAccessingTheActiveSubscription_whenIRun_thenIExpectAFailure() { + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.NetworkError( + IOException() + ) val job = BackupSubscriptionCheckJob.create() val result = job.run() @@ -223,10 +255,32 @@ class BackupSubscriptionCheckJobTest { } @Test - fun givenPastDueSubscription_whenIRun_thenIExpectStateMismatchDetected() { - mockProduct() + fun givenAStatusCodeErrorWhenAccessingTheActiveSubscription_whenIRun_thenIExpectAMismatch() { + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.StatusCodeError( + NonSuccessfulResponseCodeException(404) + ) - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isTrue() + } + + @Test + fun givenBillingApiReturnsAFailure_whenIRun_thenIExpectSuccessAndEarlyExit() { + coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.BillingUnavailable + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + assertThat(SignalStore.backup.subscriptionStateMismatchDetected).isFalse() + } + + @Test + fun givenPastDueSubscription_whenIRun_thenIExpectStateMismatchDetected() { + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription( isActive = false, billingPeriodEndSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds - 1.days.inWholeSeconds, @@ -243,9 +297,7 @@ class BackupSubscriptionCheckJobTest { @Test fun givenCancelledSubscription_whenIRun_thenIExpectStateMismatchDetected() { - mockProduct() - - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription( isActive = false, status = "canceled", @@ -262,9 +314,7 @@ class BackupSubscriptionCheckJobTest { @Test fun givenFreeBackupTier_whenIRun_thenIExpectSuccessAndEarlyExit() { - mockProduct() - - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( ActiveSubscription.EMPTY ) @@ -277,25 +327,12 @@ class BackupSubscriptionCheckJobTest { 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( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true) ) @@ -315,10 +352,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenActiveSubscriptionAndPurchaseWithoutEntitlement_whenIRun_thenIExpectRedemption() { - mockProduct() mockActivePurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true) ) @@ -342,10 +378,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenValidActiveState_whenIRun_thenIExpectSuccessAndNoMismatch() { - mockProduct() mockActivePurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true) ) @@ -360,10 +395,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenValidInactiveState_whenIRun_thenIExpectSuccessAndNoMismatch() { - mockProduct() mockInactivePurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = false) ) @@ -379,10 +413,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenGooglePlayBillingCanceledWithoutActiveSignalSubscription_whenIRun_thenIExpectValidCancelState() { - mockProduct() mockCanceledPurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = false) ) @@ -395,10 +428,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenGooglePlayBillingCanceledWithFailedSignalSubscription_whenIRun_thenIExpectValidCancelState() { - mockProduct() mockCanceledPurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true, status = "past_due", chargeFailure = ChargeFailure("test", "", "", "", "")) ) @@ -411,11 +443,10 @@ class BackupSubscriptionCheckJobTest { @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( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = false) ) @@ -430,10 +461,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenActiveSubscriptionWithMismatchedZkCredentials_whenIRun_thenIExpectCredentialRefresh() { - mockProduct() mockActivePurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true) ) @@ -452,10 +482,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenActiveSubscriptionWithSyncedZkCredentials_whenIRun_thenIExpectNoCredentialRefresh() { - mockProduct() mockActivePurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true) ) @@ -472,10 +501,9 @@ class BackupSubscriptionCheckJobTest { @Test fun givenActiveSubscriptionWithZkCredentialFailure_whenIRun_thenIExpectCredentialRefresh() { - mockProduct() mockActivePurchase() - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true) ) @@ -493,11 +521,10 @@ class BackupSubscriptionCheckJobTest { @Test fun givenSubscriptionWillCancelAtPeriodEnd_whenIRun_thenIExpectValidCancelState() { - mockProduct() mockCanceledPurchase() // Create subscription that will cancel at period end - every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns Result.success( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true, cancelled = true) // cancelled = true means willCancelAtPeriodEnd ) @@ -510,11 +537,10 @@ class BackupSubscriptionCheckJobTest { @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( + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( createActiveSubscription(isActive = true, cancelled = false) ) @@ -528,6 +554,37 @@ class BackupSubscriptionCheckJobTest { verify { BackupRepository.getBackupTierWithoutDowngrade() } } + @Test + fun givenSubscriptionWillCancelWithValidEntitlement_whenIRun_thenIExpectBackupStateNotification() { + mockCanceledPurchase() + + every { AppDependencies.accountApi.whoAmI() } returns NetworkResult.Success( + WhoAmIResponse( + number = "+1234567890", + entitlements = WhoAmIResponse.Entitlements( + backup = WhoAmIResponse.BackupEntitlement( + backupLevel = 201, + expirationSeconds = System.currentTimeMillis() / 1000 + 3600 // 1 hour from now + ) + ) + ) + ) + + every { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } returns NetworkResult.Success( + createActiveSubscription( + isActive = true, + cancelled = true, + chargeFailure = ChargeFailure("test", "", "", "", "") + ) + ) + + val job = BackupSubscriptionCheckJob.create() + val result = job.run() + + assertThat(result.isSuccess).isTrue() + verify { BackupStateObserver.notifyBackupStateChanged() } + } + private fun createActiveSubscription( isActive: Boolean = true, billingPeriodEndSeconds: Long = 2147472000, @@ -553,15 +610,6 @@ class BackupSubscriptionCheckJobTest { ) } - 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( @@ -575,6 +623,16 @@ class BackupSubscriptionCheckJobTest { ) } + private fun insertPrePendingInAppPayment() { + SignalDatabase.inAppPayments.insert( + type = InAppPaymentType.RECURRING_BACKUP, + state = InAppPaymentTable.State.TRANSACTING, + subscriberId = null, + endOfPeriod = null, + inAppPaymentData = InAppPaymentData() + ) + } + private fun insertPendingInAppPayment() { SignalDatabase.inAppPayments.insert( type = InAppPaymentType.RECURRING_BACKUP, @@ -593,20 +651,6 @@ class BackupSubscriptionCheckJobTest { } } - 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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index 2b8a42d63b..53ea55b50b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -1938,7 +1938,7 @@ object BackupRepository { suspend fun getPaidType(): NetworkResult { val productPrice: FiatMoney? = if (SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID) { Log.d(TAG, "Accessing price via mock subscription.") - RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription?.let { + RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).successOrNull()?.activeSubscription?.let { FiatMoney.fromSignalNetworkAmount(it.amount, Currency.getInstance(it.currency)) } } else if (AppDependencies.billingApi.getApiAvailability().isSuccess) { 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 694d6294b7..5d94993742 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 @@ -181,7 +181,7 @@ class MessageBackupsFlowViewModel( RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } - activeSubscription.onSuccess { subscription -> + activeSubscription.runIfSuccessful { subscription -> if (subscription.willCancelAtPeriodEnd()) { Log.d(TAG, "Active subscription is cancelled. Clearing tier.") internalStateFlow.update { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt index 2984e811cb..27af5f68fe 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/backups/BackupStateObserver.kt @@ -269,8 +269,27 @@ class BackupStateObserver( Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription") - val activeSubscription = withContext(Dispatchers.IO) { - RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull() + val activeSubscriptionResult = withContext(Dispatchers.IO) { + RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) + } + + val activeSubscription: ActiveSubscription? = when (activeSubscriptionResult) { + is NetworkResult.ApplicationError -> { + Log.w(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Failed to load active subscription due to an application error.", activeSubscriptionResult.getCause(), true) + return getStateOnError() + } + is NetworkResult.NetworkError -> { + Log.w(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Failed to load active subscription due to a network error.", activeSubscriptionResult.getCause(), true) + return getStateOnError() + } + is NetworkResult.StatusCodeError -> { + Log.i(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Failed to load active subscription due to a status code error.", activeSubscriptionResult.getCause(), true) + null + } + is NetworkResult.Success -> { + Log.i(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Successfully loaded active subscription.", true) + activeSubscriptionResult.result + } } val hasActiveSignalSubscription = activeSubscription?.isActive == true @@ -358,10 +377,10 @@ class BackupStateObserver( RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) } - return if (activeSubscription.isSuccess) { + return if (activeSubscription is NetworkResult.Success) { Log.d(TAG, "[getPaidBackupState] Retrieved subscription details.") - val subscription = activeSubscription.getOrThrow().activeSubscription + val subscription = activeSubscription.successOrThrow().activeSubscription if (subscription != null) { Log.d(TAG, "[getPaidBackupState] Subscription found. Updating UI state with subscription details. Status: ${subscription.status}") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt index 25513e0d98..5b71f239bb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/subscription/RecurringInAppPaymentRepository.kt @@ -11,6 +11,10 @@ import org.thoughtcrime.securesms.backup.v2.MessageBackupTier import org.thoughtcrime.securesms.badges.Badges import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.requireSubscriberType import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository.toPaymentSourceType +import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository.cancelActiveSubscriptionIfNecessarySync +import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository.cancelActiveSubscriptionSync +import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository.getActiveSubscriptionSync +import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository.rotateSubscriberIdSync import org.thoughtcrime.securesms.database.InAppPaymentTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord @@ -25,6 +29,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.subscription.LevelUpdate import org.thoughtcrime.securesms.subscription.LevelUpdateOperation import org.thoughtcrime.securesms.subscription.Subscription +import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.storage.IAPSubscriptionId import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey @@ -51,7 +56,7 @@ object RecurringInAppPaymentRepository { @CheckResult fun getActiveSubscription(type: InAppPaymentSubscriberRecord.Type): Single { return Single.fromCallable { - getActiveSubscriptionSync(type).getOrThrow() + getActiveSubscriptionSync(type).successOrThrow() }.subscribeOn(Schedulers.io()) } @@ -77,26 +82,23 @@ object RecurringInAppPaymentRepository { * Gets the active subscription if it exists for the given [InAppPaymentSubscriberRecord.Type] */ @WorkerThread - fun getActiveSubscriptionSync(type: InAppPaymentSubscriberRecord.Type): Result { + fun getActiveSubscriptionSync(type: InAppPaymentSubscriberRecord.Type): NetworkResult { if (type == InAppPaymentSubscriberRecord.Type.BACKUP && SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID) { Log.d(TAG, "Returning mock paid subscription.") - return Result.success(MOCK_PAID_SUBSCRIPTION) + return NetworkResult.Success(MOCK_PAID_SUBSCRIPTION) } val response = InAppPaymentsRepository.getSubscriber(type)?.let { donationsService.getSubscription(it.subscriberId) - } ?: return Result.success(ActiveSubscription.EMPTY) + } ?: return NetworkResult.Success(ActiveSubscription.EMPTY) - return try { - val result = response.resultOrThrow + response.result.ifPresent { result -> if (result.isActive && result.activeSubscription.endOfCurrentPeriod > SignalStore.inAppPayments.getLastEndOfPeriod()) { InAppPaymentKeepAliveJob.enqueueAndTrackTime(System.currentTimeMillis().milliseconds) } - - Result.success(result) - } catch (e: Exception) { - Result.failure(e) } + + return response.toNetworkResult() } /** 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 80446a8daf..f4833ac49a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -141,7 +141,22 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C return Result.success() } - val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull() + val activeSubscriptionResult = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) + val activeSubscription: ActiveSubscription? = when (activeSubscriptionResult) { + is NetworkResult.ApplicationError, is NetworkResult.NetworkError -> { + Log.w(TAG, "Encountered an app-level or network-level error. Failing.", activeSubscriptionResult.getCause(), true) + return Result.failure() + } + is NetworkResult.StatusCodeError -> { + Log.w(TAG, "Encountered a status-code error.", activeSubscriptionResult.getCause(), true) + null + } + is NetworkResult.Success -> { + Log.i(TAG, "Successfully retrieved the user's active subscription object.", true) + activeSubscriptionResult.result + } + } + val hasActiveSignalSubscription = activeSubscription?.isActive == true checkForFailedOrCanceledSubscriptionState(activeSubscription) @@ -234,7 +249,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C * the "download your data" notifier sheet. */ private fun checkForFailedOrCanceledSubscriptionState(activeSubscription: ActiveSubscription?) { - if (activeSubscription?.willCancelAtPeriodEnd() == true && 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/app/src/main/java/org/thoughtcrime/securesms/jobs/PostRegistrationBackupRedemptionJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/PostRegistrationBackupRedemptionJob.kt index 84d632c7d4..320d6bcd2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PostRegistrationBackupRedemptionJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PostRegistrationBackupRedemptionJob.kt @@ -92,7 +92,7 @@ class PostRegistrationBackupRedemptionJob : CoroutineJob { } info("Attempting to grab price information for records...") - val subscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription + val subscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).successOrNull()?.activeSubscription val emptyPrice = FiatMoney(BigDecimal.ZERO, Currency.getInstance(Locale.getDefault())) val price: FiatMoney = if (subscription != null) { diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt index 56fead328c..ebf3792087 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResult.kt @@ -246,6 +246,16 @@ sealed class NetworkResult( } } + /** + * Returns the result if successful, otherwise null. + */ + fun successOrNull(): T? { + return when (this) { + is Success -> result + else -> null + } + } + /** * Returns the [Throwable] associated with the result, or null if the result is successful. */