Acknowledge subscriptions

pull/1451/head
Alex Baker 5 years ago
parent 775b5b56ca
commit 7e5151cd03

@ -2,6 +2,7 @@ package org.tasks.billing
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
@ -28,6 +29,7 @@ class InventoryTest : InjectingTestCase() {
withPurchases(monthly01)
assertTrue(inventory.hasPro)
assertEquals(1, inventory.subscription.value?.subscriptionPrice)
}
@Test
@ -59,7 +61,17 @@ class InventoryTest : InjectingTestCase() {
}
private fun withPurchases(vararg purchases: String) {
preferences.setPurchases(purchases.toHashSet())
val asPurchases =
purchases
.map(::JSONObject)
.map {
com.android.billingclient.api.Purchase(
it.getString("zza"),
it.getString("zzb")
)
}
.map(::Purchase)
preferences.setPurchases(asPurchases)
runOnMainSync {
inventory = Inventory(
context,
@ -72,9 +84,9 @@ class InventoryTest : InjectingTestCase() {
}
companion object {
private const val annual03 = """{"mOriginalJson":"{\"orderId\":\"GPA.3372-3222-8630-38485\",\"packageName\":\"org.tasks\",\"productId\":\"annual_03\",\"purchaseTime\":1603917413542,\"purchaseState\":0,\"purchaseToken\":\"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3372-3222-8630-38485","packageName":"org.tasks","productId":"annual_03","purchaseTime":1603917413542,"purchaseState":0,"purchaseToken":"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg","autoRenewing":true}},"mSignature":"Od2ulMjFethYNdA1rTm7AvNMyfFgefCaZhtBeYuTHlMB/XbEd/m9noRlKWMnShFthnQyw97CfrB86aaB52OSWm9pGkPzaRtOJPyL8BJHP9LEjXHOQIQ2Nx9zRF30+EWgV4O0IyeL/o5eUvTQRNnfyUXFdJQRLiKTblQojO6mTCX2fA6lTAntjJpbTbYGuYZjg782gX5HvmwQN5CJu7ZVZCH9AmsnAqZgb7h+MXhquQjv0L4pDVDp3dyDDwgpCAvSRy3550ZANPfNGsQpPr9Iv9IGoK0/INZRrq63VEEAz2mBGkzJgyQUYVtT6AylvNrqdo0w17hs0MLfsj6dwvSlYw\u003d\u003d"}"""
private const val monthly01 = """{"mOriginalJson":"{\"orderId\":\"GPA.3369-0544-4429-52590\",\"packageName\":\"org.tasks\",\"productId\":\"monthly_01\",\"purchaseTime\":1603912474316,\"purchaseState\":0,\"purchaseToken\":\"iibbhlkglfjcgdebphiklajb.AO-J1OyJd2kCytLMfT8Vszibf_E99ffLha5cHgOM8o3gYPKy1kD8nIZh0hcEEyOPe7fsdFJrR1-gtvg8WKLFNJoCdqrerJ2Z6Q\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3369-0544-4429-52590","packageName":"org.tasks","productId":"monthly_01","purchaseTime":1603912474316,"purchaseState":0,"purchaseToken":"iibbhlkglfjcgdebphiklajb.AO-J1OyJd2kCytLMfT8Vszibf_E99ffLha5cHgOM8o3gYPKy1kD8nIZh0hcEEyOPe7fsdFJrR1-gtvg8WKLFNJoCdqrerJ2Z6Q","autoRenewing":true}},"mSignature":"UK7fdCY61QownZW8jDLB1myUKf1llFh9rj5I7P8V03AgdA6LGpEUiCvMvCqHfMGpY3VewmawezqiCUdGWGr+UgS+6QHEuFjpO8L+E36JUDqlU9uoGrTsXLI1gXQNQElGJ71DrKlFBbyyBHSeGWnzijcq4DyyHQzpmsqijxfs0KGjkta2TiOCtyxS+YA569xaGi6lcLGTyMEe7wS5bcjdfwFir0uVtCP+iqjoEd3kt4/03l9BEJYgf8eBxI0vrm4O+jYDJu8gGMTSQZiSqb0wN4sq8D9ksV+BcI4az6LVa1d6nuD+ob0Woe0/P2uoXG8nTEZJnrAZjkG6q8736HP6rw\u003d\u003d"}"""
private const val monthly03 = """{"mOriginalJson":"{\"orderId\":\"GPA.3348-6247-8527-38213\",\"packageName\":\"org.tasks\",\"productId\":\"monthly_03\",\"purchaseTime\":1603912730414,\"purchaseState\":0,\"purchaseToken\":\"cmomnojdllomadpoinoabbkd.AO-J1OypdY4iXbMrF21L6Evn3wZSccwiBq-d55G1BVcrkwuH69zOuqb35yZnVynEb9KEvnQvgYQsUpv1AD5749iU-eDo4TRV5A\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3348-6247-8527-38213","packageName":"org.tasks","productId":"monthly_03","purchaseTime":1603912730414,"purchaseState":0,"purchaseToken":"cmomnojdllomadpoinoabbkd.AO-J1OypdY4iXbMrF21L6Evn3wZSccwiBq-d55G1BVcrkwuH69zOuqb35yZnVynEb9KEvnQvgYQsUpv1AD5749iU-eDo4TRV5A","autoRenewing":true}},"mSignature":"FkkW5FPw2elWnenIoQT7U5BnL2prcuK0GJEaHKtObPujSGRfJWFfThe3yuQ0w9AuTO0EDbm7LbJI44AiVJmpva3Iz3U2np2eNBuUAJIw9eECvQjEvuYk6Vq7LIgJwEsTyA8xRwjLJm+R1mmMWOxURmvDVBgDTHCOJsdUI9s52CSTQf2Ek+XABHugrMJudO43LzDuV2sP9mCqXUnSLbBXe3zZKyhhuz7gD+/5yavkRsPOVcZnsJetdxEmnrip8JEvgtHAvciPkvSD/fYeXdAlY2HiQWK/S0/I+yRaCEK8V+Um78ibbYc4Ng5NcXDm44nTv3F6jQEzYy4qRv/ohmwEQg\u003d\u003d"}"""
private const val annual03Cancelled = """{"mOriginalJson":"{\"orderId\":\"GPA.3372-3222-8630-38485\",\"packageName\":\"org.tasks\",\"productId\":\"annual_03\",\"purchaseTime\":1603917413542,\"purchaseState\":0,\"purchaseToken\":\"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg\",\"autoRenewing\":false}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3372-3222-8630-38485","packageName":"org.tasks","productId":"annual_03","purchaseTime":1603917413542,"purchaseState":0,"purchaseToken":"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg","autoRenewing":false}},"mSignature":"jL+2qRv0LtCutoJ86NWaInbx/9/kIWbxXRKYkou74TBjwu9KZ89EpJY632ImEy2xfLd8DHuVuWOcZY646I29Ny2E4HYNAsQEg2du4NRXEHZvu+py4Mi212KF8S2EPNdZCor1wiOJ0zRVBiRAtiCfqxHjQdfKn7FpDiHFrUhMu1huEAxJ0Xrnvxcmkouizw3wzKnAvI+O75LIWWZHCy+1o7s285cSKtQoztVY/nHInJLxV6dk93lAivOlEox+VCLU978lUvv45Rue50fMzS2CRsVFmRt9/yTP8RCiQKzGC/pyHtqNj/ceCrDi4VV8JPhsPd4NUaKk82Oq1xmGXtzEcQ\u003d\u003d"}"""
private const val annual03 = """{"zza":"{\"orderId\":\"GPA.3372-3222-8630-38485\",\"packageName\":\"org.tasks\",\"productId\":\"annual_03\",\"purchaseTime\":1603917413542,\"purchaseState\":0,\"purchaseToken\":\"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3372-3222-8630-38485","packageName":"org.tasks","productId":"annual_03","purchaseTime":1603917413542,"purchaseState":0,"purchaseToken":"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg","autoRenewing":true}},"zzb":"Od2ulMjFethYNdA1rTm7AvNMyfFgefCaZhtBeYuTHlMB/XbEd/m9noRlKWMnShFthnQyw97CfrB86aaB52OSWm9pGkPzaRtOJPyL8BJHP9LEjXHOQIQ2Nx9zRF30+EWgV4O0IyeL/o5eUvTQRNnfyUXFdJQRLiKTblQojO6mTCX2fA6lTAntjJpbTbYGuYZjg782gX5HvmwQN5CJu7ZVZCH9AmsnAqZgb7h+MXhquQjv0L4pDVDp3dyDDwgpCAvSRy3550ZANPfNGsQpPr9Iv9IGoK0/INZRrq63VEEAz2mBGkzJgyQUYVtT6AylvNrqdo0w17hs0MLfsj6dwvSlYw\u003d\u003d"}"""
private const val monthly01 = """{"zza":"{\"orderId\":\"GPA.3369-0544-4429-52590\",\"packageName\":\"org.tasks\",\"productId\":\"monthly_01\",\"purchaseTime\":1603912474316,\"purchaseState\":0,\"purchaseToken\":\"iibbhlkglfjcgdebphiklajb.AO-J1OyJd2kCytLMfT8Vszibf_E99ffLha5cHgOM8o3gYPKy1kD8nIZh0hcEEyOPe7fsdFJrR1-gtvg8WKLFNJoCdqrerJ2Z6Q\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3369-0544-4429-52590","packageName":"org.tasks","productId":"monthly_01","purchaseTime":1603912474316,"purchaseState":0,"purchaseToken":"iibbhlkglfjcgdebphiklajb.AO-J1OyJd2kCytLMfT8Vszibf_E99ffLha5cHgOM8o3gYPKy1kD8nIZh0hcEEyOPe7fsdFJrR1-gtvg8WKLFNJoCdqrerJ2Z6Q","autoRenewing":true}},"zzb":"UK7fdCY61QownZW8jDLB1myUKf1llFh9rj5I7P8V03AgdA6LGpEUiCvMvCqHfMGpY3VewmawezqiCUdGWGr+UgS+6QHEuFjpO8L+E36JUDqlU9uoGrTsXLI1gXQNQElGJ71DrKlFBbyyBHSeGWnzijcq4DyyHQzpmsqijxfs0KGjkta2TiOCtyxS+YA569xaGi6lcLGTyMEe7wS5bcjdfwFir0uVtCP+iqjoEd3kt4/03l9BEJYgf8eBxI0vrm4O+jYDJu8gGMTSQZiSqb0wN4sq8D9ksV+BcI4az6LVa1d6nuD+ob0Woe0/P2uoXG8nTEZJnrAZjkG6q8736HP6rw\u003d\u003d"}"""
private const val monthly03 = """{"zza":"{\"orderId\":\"GPA.3348-6247-8527-38213\",\"packageName\":\"org.tasks\",\"productId\":\"monthly_03\",\"purchaseTime\":1603912730414,\"purchaseState\":0,\"purchaseToken\":\"cmomnojdllomadpoinoabbkd.AO-J1OypdY4iXbMrF21L6Evn3wZSccwiBq-d55G1BVcrkwuH69zOuqb35yZnVynEb9KEvnQvgYQsUpv1AD5749iU-eDo4TRV5A\",\"autoRenewing\":true}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3348-6247-8527-38213","packageName":"org.tasks","productId":"monthly_03","purchaseTime":1603912730414,"purchaseState":0,"purchaseToken":"cmomnojdllomadpoinoabbkd.AO-J1OypdY4iXbMrF21L6Evn3wZSccwiBq-d55G1BVcrkwuH69zOuqb35yZnVynEb9KEvnQvgYQsUpv1AD5749iU-eDo4TRV5A","autoRenewing":true}},"zzb":"FkkW5FPw2elWnenIoQT7U5BnL2prcuK0GJEaHKtObPujSGRfJWFfThe3yuQ0w9AuTO0EDbm7LbJI44AiVJmpva3Iz3U2np2eNBuUAJIw9eECvQjEvuYk6Vq7LIgJwEsTyA8xRwjLJm+R1mmMWOxURmvDVBgDTHCOJsdUI9s52CSTQf2Ek+XABHugrMJudO43LzDuV2sP9mCqXUnSLbBXe3zZKyhhuz7gD+/5yavkRsPOVcZnsJetdxEmnrip8JEvgtHAvciPkvSD/fYeXdAlY2HiQWK/S0/I+yRaCEK8V+Um78ibbYc4Ng5NcXDm44nTv3F6jQEzYy4qRv/ohmwEQg\u003d\u003d"}"""
private const val annual03Cancelled = """{"zza":"{\"orderId\":\"GPA.3372-3222-8630-38485\",\"packageName\":\"org.tasks\",\"productId\":\"annual_03\",\"purchaseTime\":1603917413542,\"purchaseState\":0,\"purchaseToken\":\"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg\",\"autoRenewing\":false}","mParsedJson":{"nameValuePairs":{"orderId":"GPA.3372-3222-8630-38485","packageName":"org.tasks","productId":"annual_03","purchaseTime":1603917413542,"purchaseState":0,"purchaseToken":"ciogggalpbohflhmjamciehl.AO-J1OyjBpBOnwKCOBSN0Cil7yM65ZYnkn9nGqZO1n3nsHIF_LHv0ZuQqNnThB_JuCt-s9wBij1PyOCoP8axqVILXSiavcyPmg","autoRenewing":false}},"zzb":"jL+2qRv0LtCutoJ86NWaInbx/9/kIWbxXRKYkou74TBjwu9KZ89EpJY632ImEy2xfLd8DHuVuWOcZY646I29Ny2E4HYNAsQEg2du4NRXEHZvu+py4Mi212KF8S2EPNdZCor1wiOJ0zRVBiRAtiCfqxHjQdfKn7FpDiHFrUhMu1huEAxJ0Xrnvxcmkouizw3wzKnAvI+O75LIWWZHCy+1o7s285cSKtQoztVY/nHInJLxV6dk93lAivOlEox+VCLU978lUvv45Rue50fMzS2CRsVFmRt9/yTP8RCiQKzGC/pyHtqNj/ceCrDi4VV8JPhsPd4NUaKk82Oq1xmGXtzEcQ\u003d\u003d"}"""
}
}

@ -3,10 +3,16 @@ package org.tasks.billing
import android.app.Activity
import android.content.Context
import org.tasks.analytics.Firebase
import org.tasks.jobs.WorkManager
@Suppress("UNUSED_PARAMETER")
class BillingClientImpl(context: Context, inventory: Inventory, firebase: Firebase) : BillingClient {
override suspend fun queryPurchases() {}
class BillingClientImpl(
context: Context,
inventory: Inventory,
firebase: Firebase,
workManager: WorkManager
) : BillingClient {
override suspend fun queryPurchases(throwError: Boolean) {}
override suspend fun initiatePurchaseFlow(
activity: Activity,
sku: String,
@ -14,6 +20,8 @@ class BillingClientImpl(context: Context, inventory: Inventory, firebase: Fireba
oldPurchase: Purchase?
) {}
override suspend fun acknowledge(purchase: Purchase) {}
override suspend fun consume(sku: String) {}
companion object {

@ -17,4 +17,8 @@ class Purchase(json: String?) {
val isTasksSubscription = false
val purchaseToken = ""
val needsAcknowledgement = false
val isPurchased = true
}

@ -3,14 +3,12 @@ package org.tasks.analytics
import android.content.Context
import android.os.Bundle
import androidx.annotation.StringRes
import com.android.billingclient.api.BillingResult
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.ktx.remoteConfigSettings
import dagger.hilt.android.qualifiers.ApplicationContext
import org.tasks.R
import org.tasks.billing.BillingClientImpl.Companion.responseCodeString
import org.tasks.jobs.WorkManager
import org.tasks.preferences.Preferences
import timber.log.Timber
@ -33,11 +31,13 @@ class Firebase @Inject constructor(
crashlytics?.recordException(t)
}
fun reportIabResult(response: BillingResult, sku: String?) {
analytics?.logEvent(FirebaseAnalytics.Event.ECOMMERCE_PURCHASE, Bundle().apply {
putString(FirebaseAnalytics.Param.ITEM_ID, sku)
putString(FirebaseAnalytics.Param.SUCCESS, response.responseCodeString)
})
fun reportIabResult(result: String, sku: String, state: String) {
logEvent(
R.string.event_purchase_result,
R.string.param_sku to sku,
R.string.param_result to result,
R.string.param_state to state,
)
}
fun updateRemoteConfig() {

@ -2,12 +2,14 @@ package org.tasks.billing
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient.*
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.ProrationMode
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.ConsumeParams
import com.android.billingclient.api.Purchase.PurchaseState
import com.android.billingclient.api.Purchase.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.SkuDetailsParams
@ -19,14 +21,19 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import org.tasks.BuildConfig
import org.tasks.analytics.Firebase
import org.tasks.billing.Purchase.Companion.isPurchased
import org.tasks.billing.Purchase.Companion.needsAcknowledgement
import org.tasks.jobs.WorkManager
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class BillingClientImpl(
@ApplicationContext context: Context?,
private val inventory: Inventory,
private val firebase: Firebase
private val firebase: Firebase,
private val workManager: WorkManager
) : BillingClient, PurchasesUpdatedListener {
private val billingClient =
newBuilder(context!!)
@ -36,7 +43,7 @@ class BillingClientImpl(
private var connected = false
private var onPurchasesUpdated: OnPurchasesUpdated? = null
override suspend fun queryPurchases() = try {
override suspend fun queryPurchases(throwError: Boolean) = try {
executeServiceRequest {
withContext(Dispatchers.IO + NonCancellable) {
val subs = billingClient.queryPurchases(SkuType.SUBS)
@ -52,7 +59,11 @@ class BillingClientImpl(
}
}
} catch (e: IllegalStateException) {
Timber.e(e.message)
if (throwError) {
throw e
} else {
Timber.e(e.message)
}
}
override fun onPurchasesUpdated(
@ -62,10 +73,15 @@ class BillingClientImpl(
if (success) {
add(purchases ?: emptyList())
}
workManager.updatePurchases()
onPurchasesUpdated?.onPurchasesUpdated(success)
val skus = purchases?.joinToString(";") { it.sku } ?: "null"
Timber.i("onPurchasesUpdated(${result.responseCodeString}, $skus)")
firebase.reportIabResult(result, skus)
purchases?.forEach {
firebase.reportIabResult(
result.responseCodeString,
it.sku,
it.purchaseState.purchaseStateString
)
}
}
private fun add(purchases: List<com.android.billingclient.api.Purchase>) {
@ -93,7 +109,6 @@ class BillingClientImpl(
val skuDetails =
skuDetailsResult
.skuDetailsList
?.takeIf { it.isNotEmpty() }
?.firstOrNull()
?: throw IllegalStateException("Sku $sku not found")
val params = BillingFlowParams.newBuilder().setSkuDetails(skuDetails)
@ -109,6 +124,22 @@ class BillingClientImpl(
}
}
override suspend fun acknowledge(purchase: Purchase) {
if (purchase.needsAcknowledgement) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
withContext(Dispatchers.IO) {
suspendCoroutine<BillingResult> { cont ->
billingClient.acknowledgePurchase(params) {
Timber.d("acknowledge: ${it.responseCodeString} $purchase")
cont.resume(it)
}
}
}
}
}
private suspend fun connect(): BillingResult =
suspendCoroutine { cont ->
billingClient.startConnection(
@ -154,6 +185,7 @@ class BillingClientImpl(
companion object {
const val TYPE_SUBS = SkuType.SUBS
const val STATE_PURCHASED = PurchaseState.PURCHASED
private val PurchasesResult.success: Boolean
get() = responseCode == BillingResponseCode.OK
@ -177,6 +209,14 @@ class BillingClientImpl(
else -> "UNKNOWN"
}
val Int.purchaseStateString: String
get() = when (this) {
PurchaseState.UNSPECIFIED_STATE -> "UNSPECIFIED_STATE"
PurchaseState.PURCHASED -> "PURCHASED"
PurchaseState.PENDING -> "PENDING"
else -> this.toString()
}
private val PurchasesResult.responseCodeString: String
get() = billingResult.responseCodeString

@ -2,6 +2,7 @@ package org.tasks.billing
import com.android.billingclient.api.Purchase
import com.google.gson.GsonBuilder
import org.tasks.billing.BillingClientImpl.Companion.STATE_PURCHASED
import java.util.regex.Pattern
class Purchase(private val purchase: Purchase) {
@ -37,6 +38,12 @@ class Purchase(private val purchase: Purchase) {
val isCanceled: Boolean
get() = !purchase.isAutoRenewing
val needsAcknowledgement: Boolean
get() = purchase.needsAcknowledgement
val isPurchased: Boolean
get() = purchase.isPurchased
val subscriptionPrice: Int?
get() {
val matcher = PATTERN.matcher(sku)
@ -62,5 +69,11 @@ class Purchase(private val purchase: Purchase) {
companion object {
private val PATTERN = Pattern.compile("^(annual|monthly)_([0-3][0-9]|499)$")
val Purchase.isPurchased: Boolean
get() = purchaseState == STATE_PURCHASED
val Purchase.needsAcknowledgement: Boolean
get() = isPurchased && !isAcknowledged
}
}

@ -15,7 +15,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory
import org.tasks.caldav.CaldavSynchronizer
import org.tasks.files.FileHelper
@ -46,7 +45,6 @@ class Tasks : Application(), Configuration.Provider {
@Inject lateinit var workManager: Lazy<WorkManager>
@Inject lateinit var refreshScheduler: Lazy<RefreshScheduler>
@Inject lateinit var geofenceApi: Lazy<GeofenceApi>
@Inject lateinit var billingClient: Lazy<BillingClient>
@Inject lateinit var appWidgetManager: Lazy<AppWidgetManager>
@Inject lateinit var workerFactory: HiltWorkerFactory
@Inject lateinit var contentObserver: Lazy<OpenTaskContentObserver>
@ -86,10 +84,10 @@ class Tasks : Application(), Configuration.Provider {
scheduleBackup()
scheduleConfigRefresh()
OpenTaskContentObserver.registerObserver(context, contentObserver.get())
updatePurchases()
}
geofenceApi.get().registerAll()
FileHelper.delete(context, preferences.cacheDirectory)
billingClient.get().queryPurchases()
appWidgetManager.get().reconfigureWidgets()
CaldavSynchronizer.registerFactories()
}

@ -3,7 +3,7 @@ package org.tasks.billing
import android.app.Activity
interface BillingClient {
suspend fun queryPurchases()
suspend fun queryPurchases(throwError: Boolean = false)
suspend fun consume(sku: String)
suspend fun initiatePurchaseFlow(
activity: Activity,
@ -11,4 +11,5 @@ interface BillingClient {
skuType: String,
oldPurchase: Purchase? = null
)
suspend fun acknowledge(purchase: Purchase)
}

@ -24,7 +24,7 @@ class Inventory @Inject constructor(
private val localBroadcastManager: LocalBroadcastManager,
private val caldavDao: CaldavDao
) {
private val purchases: MutableMap<String, Purchase> = HashMap()
val purchases: MutableMap<String, Purchase> = HashMap()
val subscription = MutableLiveData<Purchase?>()
var hasTasksAccount = false

@ -66,7 +66,11 @@ class PurchaseActivity : InjectingAppCompatActivity(), OnPurchasesUpdated {
localBroadcastManager.registerPurchaseReceiver(purchaseReceiver)
lifecycleScope.launch {
billingClient.queryPurchases()
try {
billingClient.queryPurchases(throwError = true)
} catch (e: Exception) {
toast(e.message)
}
}
}

@ -12,6 +12,7 @@ import org.tasks.billing.BillingClient
import org.tasks.billing.BillingClientImpl
import org.tasks.billing.Inventory
import org.tasks.data.*
import org.tasks.jobs.WorkManager
import org.tasks.locale.Locale
import org.tasks.notifications.NotificationDao
import javax.inject.Singleton
@ -96,6 +97,10 @@ class ApplicationModule {
fun getPrincipalDao(db: Database) = db.principalDao
@Provides
fun getBillingClient(@ApplicationContext context: Context, inventory: Inventory, firebase: Firebase): BillingClient
= BillingClientImpl(context, inventory, firebase)
fun getBillingClient(
@ApplicationContext context: Context,
inventory: Inventory,
firebase: Firebase,
workManager: WorkManager,
): BillingClient = BillingClientImpl(context, inventory, firebase, workManager)
}

@ -0,0 +1,40 @@
package org.tasks.jobs
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.tasks.analytics.Firebase
import org.tasks.billing.BillingClient
import org.tasks.billing.Inventory
import org.tasks.injection.BaseWorker
@HiltWorker
class UpdatePurchaseWork @AssistedInject constructor(
@Assisted context: Context,
@Assisted workerParams: WorkerParameters,
firebase: Firebase,
private val inventory: Inventory,
private val billingClient: BillingClient,
) : BaseWorker(context, workerParams, firebase) {
override suspend fun run(): Result {
try {
billingClient.queryPurchases(throwError = true)
} catch (e: Exception) {
return Result.retry()
}
inventory.purchases.values
.filter { it.needsAcknowledgement }
.takeIf { it.isNotEmpty() }
?.forEach { billingClient.acknowledge(it) }
?.apply { billingClient.queryPurchases() }
return with(inventory.purchases.values) {
when {
any { it.needsAcknowledgement } -> Result.retry()
all { it.isPurchased } -> Result.success()
else -> Result.retry()
}
}
}
}

@ -45,6 +45,8 @@ interface WorkManager {
fun cancelNotifications()
fun updatePurchases()
companion object {
val REMOTE_CONFIG_INTERVAL_HOURS = if (BuildConfig.DEBUG) 1 else 12.toLong()
const val MAX_CLEANUP_LENGTH = 500
@ -63,5 +65,6 @@ interface WorkManager {
const val TAG_BACKGROUND_SYNC_OPENTASKS = "tag_background_sync_opentasks"
const val TAG_REMOTE_CONFIG = "tag_remote_config"
const val TAG_MIGRATE_LOCAL = "tag_migrate_local"
const val TAG_UPDATE_PURCHASES = "tag_update_purchases"
}
}

@ -44,6 +44,7 @@ import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETEBASE
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_ETESYNC
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_GOOGLE_TASKS
import org.tasks.jobs.WorkManager.Companion.TAG_SYNC_OPENTASK
import org.tasks.jobs.WorkManager.Companion.TAG_UPDATE_PURCHASES
import org.tasks.notifications.Throttle
import org.tasks.preferences.Preferences
import org.tasks.time.DateTimeUtils
@ -254,6 +255,9 @@ class WorkManagerImpl constructor(
alarmManager.cancel(notificationPendingIntent)
}
override fun updatePurchases() =
enqueueUnique(TAG_UPDATE_PURCHASES, UpdatePurchaseWork::class.java)
@SuppressLint("EnqueueWork")
private fun enqueueUnique(key: String, c: Class<out Worker?>, time: Long = 0) {
val delay = time - DateUtilities.now()

@ -19,6 +19,7 @@ import org.tasks.billing.PurchaseActivity
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.data.CaldavAccount
import org.tasks.data.GoogleTaskAccount
import org.tasks.extensions.Context.toast
import org.tasks.injection.InjectingPreferenceFragment
import org.tasks.preferences.IconPreference
import org.tasks.preferences.MainPreferences
@ -59,7 +60,14 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
findPreference(R.string.refresh_purchases).setOnPreferenceClickListener {
lifecycleScope.launch {
billingClient.queryPurchases()
try {
billingClient.queryPurchases(throwError = true)
if (inventory.subscription.value == null) {
activity?.toast(R.string.no_google_play_subscription)
}
} catch (e: Exception) {
activity?.toast(e.message)
}
}
false
}

@ -431,6 +431,10 @@
<string name="param_user_no_churn">user_no_churn</string>
<string name="param_user_pro">user_pro</string>
<string name="param_click">click</string>
<string name="event_purchase_result">billing_flow_result</string>
<string name="param_sku">sku</string>
<string name="param_result">result</string>
<string name="param_state">state</string>
<string name="event_todoagenda">cp_todoagenda</string>
<string name="event_astrid2taskprovider">cp_astrid2taskprovider</string>
<string name="event_sync_add_account">sync_add_account</string>

Loading…
Cancel
Save