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 27af5f68fe..417c0f41e6 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 @@ -255,7 +255,7 @@ class BackupStateObserver( val purchaseResult = AppDependencies.billingApi.queryPurchases() Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] queryPurchase result: $purchaseResult") - val hasActiveGooglePlayBillingSubscription = when (purchaseResult) { + val googlePlayBillingSubscriptionIsActiveAndWillRenew = when (purchaseResult) { is BillingPurchaseResult.Success -> { Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] Found a purchase: $purchaseResult") purchaseResult.isAcknowledged && purchaseResult.isAutoRenewing @@ -267,7 +267,7 @@ class BackupStateObserver( } } || SignalStore.backup.backupTierInternalOverride == MessageBackupTier.PAID - Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveGooglePlayBillingSubscription: $hasActiveGooglePlayBillingSubscription") + Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] googlePlayBillingSubscriptionIsActiveAndWillRenew: $googlePlayBillingSubscriptionIsActiveAndWillRenew") val activeSubscriptionResult = withContext(Dispatchers.IO) { RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP) @@ -278,26 +278,29 @@ class BackupStateObserver( 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 + val signalServiceSubscriptionIsActiveAndWillRenew = activeSubscription?.isActive == true && (!activeSubscription.isCanceled || activeSubscription.willCancelAtPeriodEnd()) - Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] hasActiveSignalSubscription: $hasActiveSignalSubscription") + Log.d(TAG, "[getNetworkBackupState][subscriptionStateMismatchDetected] signalServiceSubscriptionIsActiveAndWillRenew: $signalServiceSubscriptionIsActiveAndWillRenew") when { - hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> { + signalServiceSubscriptionIsActiveAndWillRenew && !googlePlayBillingSubscriptionIsActiveAndWillRenew -> { val type = buildPaidTypeFromSubscription(activeSubscription.activeSubscription) if (type == null) { @@ -312,12 +315,12 @@ class BackupStateObserver( ) } - hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> { + signalServiceSubscriptionIsActiveAndWillRenew && googlePlayBillingSubscriptionIsActiveAndWillRenew -> { Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Found active signal subscription and active google play subscription. Clearing mismatch.") SignalStore.backup.subscriptionStateMismatchDetected = false } - !hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> { + !signalServiceSubscriptionIsActiveAndWillRenew && !googlePlayBillingSubscriptionIsActiveAndWillRenew -> { Log.d(TAG, "[getNetworkBackupState][subscriptionMismatchDetected] Found inactive signal subscription and inactive google play subscription. Clearing mismatch.") SignalStore.backup.subscriptionStateMismatchDetected = false } @@ -393,6 +396,8 @@ class BackupStateObserver( when { (subscription.isCanceled || subscription.willCancelAtPeriodEnd()) && subscription.isActive -> { Log.d(TAG, "[getPaidBackupState] Found a canceled subscription.") + InAppPaymentsRepository.updateBackupInAppPaymentWithCancelation(activeSubscription.successOrThrow()) + BackupState.Canceled( messageBackupsType = subscriberType, renewalTime = subscription.endOfCurrentPeriod.seconds @@ -401,6 +406,7 @@ class BackupStateObserver( subscription.isActive -> { Log.d(TAG, "[getPaidBackupState] Found an active subscription.") + InAppPaymentsRepository.clearCancelation(activeSubscription.successOrThrow()) BackupState.ActivePaid( messageBackupsType = subscriberType, price = FiatMoney.fromSignalNetworkAmount(subscription.amount, Currency.getInstance(subscription.currency)), 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 698599f2c8..d45e39764e 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 @@ -75,6 +75,73 @@ object InAppPaymentsRepository { private val temporaryErrorProcessor = PublishProcessor.create>() + /** + * Updates the latest payment object for the given subscription with cancelation information as necessary. + * + * This operation will only be performed if we find a latest payment for the given subscriber id in the END state without cancelation data + */ + fun updateBackupInAppPaymentWithCancelation(activeSubscription: ActiveSubscription) { + if (activeSubscription.isCanceled || activeSubscription.willCancelAtPeriodEnd()) { + val subscriber = getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) ?: return + val latestPayment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return + if (latestPayment.state == InAppPaymentTable.State.END && latestPayment.data.cancellation == null) { + synchronized(subscriber.type.lock) { + val payment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return + val chargeFailure: ActiveSubscription.ChargeFailure? = activeSubscription.chargeFailure + + Log.i(TAG, "Recording cancelation in the database. (has charge failure? ${chargeFailure != null})") + SignalDatabase.inAppPayments.update( + payment.copy( + data = payment.data.newBuilder() + .cancellation( + InAppPaymentData.Cancellation( + reason = if (chargeFailure != null) InAppPaymentData.Cancellation.Reason.PAST_DUE else InAppPaymentData.Cancellation.Reason.CANCELED, + chargeFailure = chargeFailure?.let { + InAppPaymentData.ChargeFailure( + code = it.code, + message = it.message, + outcomeType = it.outcomeType, + outcomeNetworkReason = it.outcomeNetworkReason ?: "", + outcomeNetworkStatus = it.outcomeNetworkStatus + ) + } + ) + ) + .build() + ) + ) + } + } + } + } + + /** + * Updates the latest payment object clearing cancelation information as necessary. + * + * This operation will only be performed if we find a latest payment for the given subscriber id in the END state with cancelation data + */ + fun clearCancelation(activeSubscription: ActiveSubscription) { + if (!activeSubscription.isCanceled && !activeSubscription.willCancelAtPeriodEnd()) { + val subscriber = getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) ?: return + + val latestPayment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return + if (latestPayment.data.cancellation != null && latestPayment.state == InAppPaymentTable.State.END) { + synchronized(subscriber.type.lock) { + val payment = SignalDatabase.inAppPayments.getLatestBySubscriberId(subscriber.subscriberId) ?: return + + Log.i(TAG, "Clearing cancelation in the database.") + SignalDatabase.inAppPayments.update( + payment.copy( + data = payment.data.newBuilder() + .cancellation(null) + .build() + ) + ) + } + } + } + } + /** * Wraps an in-app-payment update in a completable. */ 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 f4833ac49a..37075a4f5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/BackupSubscriptionCheckJob.kt @@ -258,6 +258,7 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C if (backupExpiration != null) { Log.i(TAG, "Marking subscription failed or canceled.") SignalStore.backup.setDownloadNotifierToTriggerAtHalfwayPoint(backupExpiration) + InAppPaymentsRepository.updateBackupInAppPaymentWithCancelation(activeSubscription) BackupStateObserver.notifyBackupStateChanged() } else { Log.w(TAG, "Failed to mark, no entitlement was found on WhoAmIResponse") @@ -267,6 +268,8 @@ class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : C if (response.getCause() != null) { Log.w(TAG, "Failed to get WhoAmI from service.", response.getCause()) } + } else if (activeSubscription != null) { + InAppPaymentsRepository.clearCancelation(activeSubscription) } }