Add BackupSubscriptionCheckJob.
This commit is contained in:
parent
24209756e3
commit
5bc8435d25
10 changed files with 326 additions and 26 deletions
|
@ -0,0 +1,168 @@
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import okhttp3.mockwebserver.MockResponse
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.signal.core.util.billing.BillingPurchaseResult
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
|
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.dependencies.InstrumentationApplicationDependencyProvider
|
||||||
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
|
import org.thoughtcrime.securesms.testing.Get
|
||||||
|
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIs
|
||||||
|
import org.thoughtcrime.securesms.testing.assertIsNull
|
||||||
|
import org.thoughtcrime.securesms.testing.success
|
||||||
|
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||||
|
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||||
|
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 {
|
||||||
|
@get:Rule
|
||||||
|
val harness = SignalActivityRule()
|
||||||
|
|
||||||
|
private val testSubject = BackupSubscriptionCheckJob.create()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
mockkStatic(AppDependencies::class)
|
||||||
|
mockkStatic(RemoteConfig::class)
|
||||||
|
|
||||||
|
every { RemoteConfig.messageBackups } returns true
|
||||||
|
every { AppDependencies.billingApi } returns mockk()
|
||||||
|
every { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||||
|
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
|
||||||
|
|
||||||
|
val billingApi = AppDependencies.billingApi
|
||||||
|
|
||||||
|
every { billingApi.isApiAvailable() } returns true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenMessageBackupsAreDisabled_whenICheck_thenIExpectSuccess() {
|
||||||
|
every { RemoteConfig.messageBackups } returns false
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenBillingApiIsUnavailable_whenICheck_thenIExpectSuccess() {
|
||||||
|
every { AppDependencies.billingApi.isApiAvailable() } returns false
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenAGooglePlaySubscriptionAndNoSubscriberId_whenICheck_thenIExpectToTurnOffBackups() {
|
||||||
|
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
||||||
|
purchaseToken = "",
|
||||||
|
isAcknowledged = true,
|
||||||
|
purchaseTime = System.currentTimeMillis(),
|
||||||
|
isAutoRenewing = true
|
||||||
|
)
|
||||||
|
|
||||||
|
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
SignalStore.backup.backupTier.assertIsNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenNoSubscriberIdButPaidTier_whenICheck_thenIExpectToTurnOffBackups() {
|
||||||
|
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
SignalStore.backup.backupTier.assertIsNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun givenActiveSubscription_whenICheck_thenIExpectToTurnOnBackups() {
|
||||||
|
initialiseActiveSubscription()
|
||||||
|
SignalStore.backup.backupTier = null
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
SignalStore.backup.backupTier.assertIs(MessageBackupTier.PAID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenInactiveSubscription_whenICheck_thenIExpectToTurnOffBackups() {
|
||||||
|
initialiseActiveSubscription("canceled")
|
||||||
|
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
SignalStore.backup.backupTier.assertIsNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun givenInactiveSubscriptionAndNoLocalState_whenICheck_thenIExpectToTurnOffBackups() {
|
||||||
|
initialiseActiveSubscription("canceled")
|
||||||
|
SignalStore.backup.backupTier = null
|
||||||
|
|
||||||
|
val result = testSubject.run()
|
||||||
|
|
||||||
|
result.isSuccess.assertIs(true)
|
||||||
|
SignalStore.backup.backupTier.assertIsNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initialiseActiveSubscription(status: String = "active") {
|
||||||
|
val currency = Currency.getInstance("USD")
|
||||||
|
val subscriber = InAppPaymentSubscriberRecord(
|
||||||
|
subscriberId = SubscriberId.generate(),
|
||||||
|
currency = currency,
|
||||||
|
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||||
|
requiresCancel = false,
|
||||||
|
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||||
|
)
|
||||||
|
|
||||||
|
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||||
|
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
|
||||||
|
|
||||||
|
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||||
|
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||||
|
MockResponse().success(
|
||||||
|
ActiveSubscription(
|
||||||
|
ActiveSubscription.Subscription(
|
||||||
|
201,
|
||||||
|
currency.currencyCode,
|
||||||
|
BigDecimal.ONE,
|
||||||
|
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||||
|
true,
|
||||||
|
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
|
||||||
|
false,
|
||||||
|
status,
|
||||||
|
"STRIPE",
|
||||||
|
"GOOGLE_PLAY_BILLING",
|
||||||
|
false
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -112,20 +112,6 @@ object BackupRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
fun canAccessRemoteBackupSettings(): Boolean {
|
|
||||||
// TODO [message-backups]
|
|
||||||
|
|
||||||
// We need to check whether the user can access remote backup settings.
|
|
||||||
|
|
||||||
// 1. Do they have a receipt they need to be able to view?
|
|
||||||
// 2. Do they have a backup they need to be able to manage?
|
|
||||||
|
|
||||||
// The easy thing to do here would actually be to set a ui hint.
|
|
||||||
|
|
||||||
return SignalStore.backup.areBackupsEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
fun turnOffAndDeleteBackup() {
|
fun turnOffAndDeleteBackup() {
|
||||||
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||||
|
|
|
@ -260,7 +260,9 @@ class MessageBackupsFlowViewModel(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Log.d(TAG, "Enqueueing InAppPaymentPurchaseTokenJob chain.")
|
Log.d(TAG, "Enabling backups and enqueueing InAppPaymentPurchaseTokenJob chain.")
|
||||||
|
SignalStore.backup.areBackupsEnabled = true
|
||||||
|
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
||||||
InAppPaymentPurchaseTokenJob.createJobChain(inAppPayment).enqueue()
|
InAppPaymentPurchaseTokenJob.createJobChain(inAppPayment).enqueue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -178,10 +178,14 @@ object InAppPaymentsRepository {
|
||||||
return when (inAppPayment.type) {
|
return when (inAppPayment.type) {
|
||||||
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
InAppPaymentType.UNKNOWN -> error("Unsupported type UNKNOWN.")
|
||||||
InAppPaymentType.ONE_TIME_GIFT, InAppPaymentType.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
|
InAppPaymentType.ONE_TIME_GIFT, InAppPaymentType.ONE_TIME_DONATION -> "$JOB_PREFIX${inAppPayment.id.serialize()}"
|
||||||
InAppPaymentType.RECURRING_DONATION, InAppPaymentType.RECURRING_BACKUP -> "$JOB_PREFIX${inAppPayment.type.code}"
|
InAppPaymentType.RECURRING_DONATION, InAppPaymentType.RECURRING_BACKUP -> getRecurringJobQueueKey(inAppPayment.type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRecurringJobQueueKey(inAppPaymentType: InAppPaymentType): String {
|
||||||
|
return "$JOB_PREFIX${inAppPaymentType.code}"
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a duration to utilize for jobs tied to different payment methods. For long running bank transfers, we need to
|
* 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.
|
* allow extra time for completion.
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.jobs
|
||||||
|
|
||||||
|
import org.signal.core.util.billing.BillingPurchaseResult
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.signal.donations.InAppPaymentType
|
||||||
|
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
|
||||||
|
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||||
|
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.util.RemoteConfig
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks and rectifies state pertaining to backups subscriptions.
|
||||||
|
*/
|
||||||
|
class BackupSubscriptionCheckJob private constructor(parameters: Parameters) : CoroutineJob(parameters) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(BackupSubscriptionCheckJob::class)
|
||||||
|
|
||||||
|
const val KEY = "BackupSubscriptionCheckJob"
|
||||||
|
|
||||||
|
fun create(): BackupSubscriptionCheckJob {
|
||||||
|
return BackupSubscriptionCheckJob(
|
||||||
|
Parameters.Builder()
|
||||||
|
.setQueue(InAppPaymentsRepository.getRecurringJobQueueKey(InAppPaymentType.RECURRING_BACKUP))
|
||||||
|
.addConstraint(NetworkConstraint.KEY)
|
||||||
|
.setMaxAttempts(Parameters.UNLIMITED)
|
||||||
|
.setMaxInstancesForFactory(1)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun doRun(): Result {
|
||||||
|
if (!RemoteConfig.messageBackups) {
|
||||||
|
Log.i(TAG, "Message backups are not enabled. Exiting.")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AppDependencies.billingApi.isApiAvailable()) {
|
||||||
|
Log.i(TAG, "Google Play Billing API is not available on this device. Exiting.")
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
val purchase: BillingPurchaseResult = AppDependencies.billingApi.queryPurchases()
|
||||||
|
val hasActivePurchase = purchase is BillingPurchaseResult.Success && purchase.isAcknowledged && purchase.isWithinTheLastMonth()
|
||||||
|
|
||||||
|
val subscriberId = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||||
|
if (subscriberId == null && hasActivePurchase) {
|
||||||
|
Log.w(TAG, "User has active Google Play Billing purchase but no subscriber id! User should cancel backup and resubscribe.")
|
||||||
|
updateLocalState(null)
|
||||||
|
// TODO [message-backups] Set UI flag hint here to launch sheet (designs pending)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
val tier = SignalStore.backup.backupTier
|
||||||
|
if (subscriberId == null && tier == MessageBackupTier.PAID) {
|
||||||
|
Log.w(TAG, "User has no subscriber id but PAID backup tier. Reverting to no backup tier and informing the user.")
|
||||||
|
updateLocalState(null)
|
||||||
|
// TODO [message-backups] Set UI flag hint here to launch sheet (designs pending)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()
|
||||||
|
if (activeSubscription?.isActive == true && tier != MessageBackupTier.PAID) {
|
||||||
|
Log.w(TAG, "User has an active subscription but no backup tier. Setting to PAID and enabling backups.")
|
||||||
|
updateLocalState(MessageBackupTier.PAID)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSubscription?.isActive != true && tier == MessageBackupTier.PAID) {
|
||||||
|
Log.w(TAG, "User subscription is inactive or does not exist. Clearing backup tier.")
|
||||||
|
// TODO [message-backups] Set UI hint?
|
||||||
|
updateLocalState(null)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSubscription?.isActive != true && hasActivePurchase) {
|
||||||
|
Log.w(TAG, "User subscription is inactive but user has a recent purchase. Clearing backup tier.")
|
||||||
|
// TODO [message-backups] Set UI hint?
|
||||||
|
updateLocalState(null)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLocalState(backupTier: MessageBackupTier?) {
|
||||||
|
synchronized(InAppPaymentSubscriberRecord.Type.BACKUP) {
|
||||||
|
SignalStore.backup.backupTier = backupTier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(): ByteArray? = null
|
||||||
|
|
||||||
|
override fun getFactoryKey(): String = KEY
|
||||||
|
|
||||||
|
override fun onFailure() = Unit
|
||||||
|
|
||||||
|
class Factory : Job.Factory<BackupSubscriptionCheckJob> {
|
||||||
|
override fun create(parameters: Parameters, serializedData: ByteArray?): BackupSubscriptionCheckJob {
|
||||||
|
return BackupSubscriptionCheckJob(parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -255,10 +255,8 @@ class InAppPaymentRedemptionJob private constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) {
|
if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) {
|
||||||
Log.i(TAG, "Enabling backups and setting backup tier to PAID", true)
|
Log.i(TAG, "Setting backup tier to PAID", true)
|
||||||
SignalStore.backup.areBackupsEnabled = true
|
|
||||||
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
SignalStore.backup.backupTier = MessageBackupTier.PAID
|
||||||
SignalStore.uiHints.markHasEverEnabledRemoteBackups()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@ public final class JobManagerFactories {
|
||||||
put(BackupRestoreJob.KEY, new BackupRestoreJob.Factory());
|
put(BackupRestoreJob.KEY, new BackupRestoreJob.Factory());
|
||||||
put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory());
|
put(BackupRestoreMediaJob.KEY, new BackupRestoreMediaJob.Factory());
|
||||||
put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory());
|
put(BoostReceiptRequestResponseJob.KEY, new BoostReceiptRequestResponseJob.Factory());
|
||||||
|
put(BackupSubscriptionCheckJob.KEY, new BackupSubscriptionCheckJob.Factory());
|
||||||
put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory());
|
put(BuildExpirationConfirmationJob.KEY, new BuildExpirationConfirmationJob.Factory());
|
||||||
put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory());
|
put(CallLinkPeekJob.KEY, new CallLinkPeekJob.Factory());
|
||||||
put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory());
|
put(CallLinkUpdateSendJob.KEY, new CallLinkUpdateSendJob.Factory());
|
||||||
|
|
|
@ -81,7 +81,8 @@ internal class BillingApiImpl(
|
||||||
BillingPurchaseResult.Success(
|
BillingPurchaseResult.Success(
|
||||||
purchaseToken = newestPurchase.purchaseToken,
|
purchaseToken = newestPurchase.purchaseToken,
|
||||||
isAcknowledged = newestPurchase.isAcknowledged,
|
isAcknowledged = newestPurchase.isAcknowledged,
|
||||||
purchaseTime = newestPurchase.purchaseTime
|
purchaseTime = newestPurchase.purchaseTime,
|
||||||
|
isAutoRenewing = newestPurchase.isAutoRenewing
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,17 +186,26 @@ internal class BillingApiImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun queryPurchases() {
|
override suspend fun queryPurchases(): BillingPurchaseResult {
|
||||||
val param = QueryPurchasesParams.newBuilder()
|
val param = QueryPurchasesParams.newBuilder()
|
||||||
.setProductType(ProductType.SUBS)
|
.setProductType(ProductType.SUBS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val purchases = doOnConnectionReady {
|
val result = doOnConnectionReady {
|
||||||
Log.d(TAG, "Querying purchases.")
|
Log.d(TAG, "Querying purchases.")
|
||||||
billingClient.queryPurchasesAsync(param)
|
billingClient.queryPurchasesAsync(param)
|
||||||
}
|
}
|
||||||
|
|
||||||
purchasesUpdatedListener.onPurchasesUpdated(purchases.billingResult, purchases.purchasesList)
|
purchasesUpdatedListener.onPurchasesUpdated(result.billingResult, result.purchasesList)
|
||||||
|
|
||||||
|
val purchase = result.purchasesList.maxByOrNull { it.purchaseTime } ?: return BillingPurchaseResult.None
|
||||||
|
|
||||||
|
return BillingPurchaseResult.Success(
|
||||||
|
purchaseTime = purchase.purchaseTime,
|
||||||
|
purchaseToken = purchase.purchaseToken,
|
||||||
|
isAcknowledged = purchase.isAcknowledged,
|
||||||
|
isAutoRenewing = purchase.isAutoRenewing
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,7 +27,7 @@ interface BillingApi {
|
||||||
* Queries the user's current purchases. This enqueues a check and will
|
* Queries the user's current purchases. This enqueues a check and will
|
||||||
* propagate it to the normal callbacks in the api.
|
* propagate it to the normal callbacks in the api.
|
||||||
*/
|
*/
|
||||||
suspend fun queryPurchases() = Unit
|
suspend fun queryPurchases(): BillingPurchaseResult = BillingPurchaseResult.None
|
||||||
|
|
||||||
suspend fun launchBillingFlow(activity: Activity) = Unit
|
suspend fun launchBillingFlow(activity: Activity) = Unit
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
|
|
||||||
package org.signal.core.util.billing
|
package org.signal.core.util.billing
|
||||||
|
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sealed class hierarchy representing the different success
|
* Sealed class hierarchy representing the different success
|
||||||
* and error states of google play billing purchases.
|
* and error states of google play billing purchases.
|
||||||
|
@ -13,8 +16,21 @@ sealed interface BillingPurchaseResult {
|
||||||
data class Success(
|
data class Success(
|
||||||
val purchaseToken: String,
|
val purchaseToken: String,
|
||||||
val isAcknowledged: Boolean,
|
val isAcknowledged: Boolean,
|
||||||
val purchaseTime: Long
|
val purchaseTime: Long,
|
||||||
) : BillingPurchaseResult
|
val isAutoRenewing: Boolean
|
||||||
|
) : BillingPurchaseResult {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if purchaseTime is within the last month.
|
||||||
|
*/
|
||||||
|
fun isWithinTheLastMonth(): Boolean {
|
||||||
|
val now = System.currentTimeMillis().milliseconds
|
||||||
|
val oneMonthAgo = now - 31.days
|
||||||
|
val purchaseTime = this.purchaseTime.milliseconds
|
||||||
|
|
||||||
|
return oneMonthAgo >= purchaseTime
|
||||||
|
}
|
||||||
|
}
|
||||||
data object UserCancelled : BillingPurchaseResult
|
data object UserCancelled : BillingPurchaseResult
|
||||||
data object None : BillingPurchaseResult
|
data object None : BillingPurchaseResult
|
||||||
data object TryAgainLater : BillingPurchaseResult
|
data object TryAgainLater : BillingPurchaseResult
|
||||||
|
|
Loading…
Add table
Reference in a new issue