Loading feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/FeatureFundingModule.kt +8 −1 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ import app.k9mail.feature.funding.googleplay.data.DataContract import app.k9mail.feature.funding.googleplay.data.GoogleBillingClient import app.k9mail.feature.funding.googleplay.data.mapper.BillingResultMapper import app.k9mail.feature.funding.googleplay.data.mapper.ProductDetailsMapper import app.k9mail.feature.funding.googleplay.data.remote.GoogleBillingClientProvider import app.k9mail.feature.funding.googleplay.domain.BillingManager import app.k9mail.feature.funding.googleplay.domain.ContributionIdProvider import app.k9mail.feature.funding.googleplay.domain.DomainContract Loading @@ -28,9 +29,15 @@ val featureFundingModule = module { BillingResultMapper() } single<DataContract.Remote.GoogleBillingClientProvider> { GoogleBillingClientProvider( context = get(), ) } single<DataContract.BillingClient> { GoogleBillingClient( context = get(), clientProvider = get(), productMapper = get(), resultMapper = get(), productCache = InMemoryCache(), Loading feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/data/DataContract.kt +18 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,8 @@ import app.k9mail.feature.funding.googleplay.domain.entity.Contribution import app.k9mail.feature.funding.googleplay.domain.entity.OneTimeContribution import app.k9mail.feature.funding.googleplay.domain.entity.RecurringContribution import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.BillingClient as GoogleBillingClient import com.android.billingclient.api.BillingResult as GoogleBillingResult interface DataContract { Loading @@ -27,6 +29,22 @@ interface DataContract { } } interface Remote { interface GoogleBillingClientProvider { val current: GoogleBillingClient /** * Set the listener to be notified of purchase updates. */ fun setPurchasesUpdatedListener(listener: PurchasesUpdatedListener) /** * Disconnect from the billing service and clear the instance. */ fun clear() } } interface BillingClient { /** Loading feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/data/GoogleBillingClient.kt +16 −25 Original line number Diff line number Diff line package app.k9mail.feature.funding.googleplay.data import android.app.Activity import android.content.Context import app.k9mail.core.common.cache.Cache import app.k9mail.feature.funding.googleplay.data.DataContract.Remote.GoogleBillingClientProvider import app.k9mail.feature.funding.googleplay.domain.entity.Contribution import app.k9mail.feature.funding.googleplay.domain.entity.OneTimeContribution import app.k9mail.feature.funding.googleplay.domain.entity.RecurringContribution import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.ProductType import com.android.billingclient.api.BillingClientStateListener Loading @@ -15,7 +14,6 @@ import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetailsResult import com.android.billingclient.api.Purchase Loading @@ -38,30 +36,23 @@ import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber @Suppress("TooManyFunctions") class GoogleBillingClient( private val context: Context, internal class GoogleBillingClient( private val clientProvider: GoogleBillingClientProvider, private val productMapper: DataContract.Mapper.Product, private val resultMapper: DataContract.Mapper.BillingResult, private val productCache: Cache<String, ProductDetails>, backgroundDispatcher: CoroutineContext = Dispatchers.IO, ) : DataContract.BillingClient, PurchasesUpdatedListener { private val coroutineScope = CoroutineScope(backgroundDispatcher) private val billingClient: BillingClient by lazy { BillingClient.newBuilder(context) .setListener(this) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build(), ) .build() init { clientProvider.setPurchasesUpdatedListener(this) } private val coroutineScope = CoroutineScope(backgroundDispatcher) override suspend fun <T> connect(onConnected: suspend () -> T): T { return suspendCancellableCoroutine { continuation -> billingClient.startConnection( clientProvider.current.startConnection( object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingResponseCode.OK) { Loading Loading @@ -95,8 +86,7 @@ class GoogleBillingClient( override fun disconnect() { productCache.clear() // TODO: this is not working as expected and leads to crashes: SERVICE_DISCONNECTED // billingClient.endConnection() clientProvider.clear() } override suspend fun loadOneTimeContributions(productIds: List<String>): List<OneTimeContribution> { Loading Loading @@ -153,7 +143,7 @@ class GoogleBillingClient( .setProductType(ProductType.INAPP) .build() val result = billingClient.queryPurchaseHistory(queryPurchaseHistoryParams) val result = clientProvider.current.queryPurchaseHistory(queryPurchaseHistoryParams) return if (result.billingResult.responseCode == BillingResponseCode.OK) { val recentPurchaseId = result.purchaseHistoryRecordList.orEmpty().firstOrNull()?.products?.filter { productCache.hasKey(it) Loading Loading @@ -182,7 +172,7 @@ class GoogleBillingClient( .setProductList(productList) .build() return billingClient.queryProductDetails(queryProductDetailsParams) return clientProvider.current.queryProductDetails(queryProductDetailsParams) } private fun mapIdToProduct( Loading @@ -200,7 +190,7 @@ class GoogleBillingClient( .setProductType(productType) .build() return billingClient.queryPurchasesAsync(queryPurchaseParams) return clientProvider.current.queryPurchasesAsync(queryPurchaseParams) } override suspend fun purchaseContribution(activity: Activity, contribution: Contribution): Contribution? { Loading @@ -222,7 +212,7 @@ class GoogleBillingClient( .setProductDetailsParamsList(productDetailsParamsList) .build() val result = billingClient.launchBillingFlow(activity, billingFlowParams) val result = clientProvider.current.launchBillingFlow(activity, billingFlowParams) return if (result.responseCode == BillingResponseCode.OK) { contribution } else { Loading Loading @@ -277,7 +267,8 @@ class GoogleBillingClient( .setPurchaseToken(purchase.purchaseToken) .build() val acknowledgeResult: BillingResult = billingClient.acknowledgePurchase(acknowledgePurchaseParams) val acknowledgeResult: BillingResult = clientProvider.current.acknowledgePurchase(acknowledgePurchaseParams) if (acknowledgeResult.responseCode != BillingResponseCode.OK) { contribution Loading @@ -303,6 +294,6 @@ class GoogleBillingClient( // This could fail but we can ignore the error as we handle purchases // the next time the purchases are requested billingClient.consumePurchase(consumeParams) clientProvider.current.consumePurchase(consumeParams) } } feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/data/remote/GoogleBillingClientProvider.kt 0 → 100644 +51 −0 Original line number Diff line number Diff line package app.k9mail.feature.funding.googleplay.data.remote import android.content.Context import app.k9mail.feature.funding.googleplay.data.DataContract import com.android.billingclient.api.BillingClient import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.PurchasesUpdatedListener /** * Google Billing client provider. * * * It is responsible for creating and managing the billing client instance */ class GoogleBillingClientProvider( private val context: Context, ) : DataContract.Remote.GoogleBillingClientProvider { private var clientInstance: BillingClient? = null override val current: BillingClient get() = if (clientInstance != null) { clientInstance!! } else { clientInstance = createBillingClient() clientInstance!! } private var listener: PurchasesUpdatedListener? = null override fun setPurchasesUpdatedListener(listener: PurchasesUpdatedListener) { this.listener = listener } private fun createBillingClient(): BillingClient { require(listener != null) { "PurchasesUpdatedListener must be set before creating the billing client" } return BillingClient.newBuilder(context) .setListener(listener!!) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build(), ) .build() } override fun clear() { clientInstance?.endConnection() clientInstance = null } } Loading
feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/FeatureFundingModule.kt +8 −1 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ import app.k9mail.feature.funding.googleplay.data.DataContract import app.k9mail.feature.funding.googleplay.data.GoogleBillingClient import app.k9mail.feature.funding.googleplay.data.mapper.BillingResultMapper import app.k9mail.feature.funding.googleplay.data.mapper.ProductDetailsMapper import app.k9mail.feature.funding.googleplay.data.remote.GoogleBillingClientProvider import app.k9mail.feature.funding.googleplay.domain.BillingManager import app.k9mail.feature.funding.googleplay.domain.ContributionIdProvider import app.k9mail.feature.funding.googleplay.domain.DomainContract Loading @@ -28,9 +29,15 @@ val featureFundingModule = module { BillingResultMapper() } single<DataContract.Remote.GoogleBillingClientProvider> { GoogleBillingClientProvider( context = get(), ) } single<DataContract.BillingClient> { GoogleBillingClient( context = get(), clientProvider = get(), productMapper = get(), resultMapper = get(), productCache = InMemoryCache(), Loading
feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/data/DataContract.kt +18 −0 Original line number Diff line number Diff line Loading @@ -7,6 +7,8 @@ import app.k9mail.feature.funding.googleplay.domain.entity.Contribution import app.k9mail.feature.funding.googleplay.domain.entity.OneTimeContribution import app.k9mail.feature.funding.googleplay.domain.entity.RecurringContribution import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.BillingClient as GoogleBillingClient import com.android.billingclient.api.BillingResult as GoogleBillingResult interface DataContract { Loading @@ -27,6 +29,22 @@ interface DataContract { } } interface Remote { interface GoogleBillingClientProvider { val current: GoogleBillingClient /** * Set the listener to be notified of purchase updates. */ fun setPurchasesUpdatedListener(listener: PurchasesUpdatedListener) /** * Disconnect from the billing service and clear the instance. */ fun clear() } } interface BillingClient { /** Loading
feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/data/GoogleBillingClient.kt +16 −25 Original line number Diff line number Diff line package app.k9mail.feature.funding.googleplay.data import android.app.Activity import android.content.Context import app.k9mail.core.common.cache.Cache import app.k9mail.feature.funding.googleplay.data.DataContract.Remote.GoogleBillingClientProvider import app.k9mail.feature.funding.googleplay.domain.entity.Contribution import app.k9mail.feature.funding.googleplay.domain.entity.OneTimeContribution import app.k9mail.feature.funding.googleplay.domain.entity.RecurringContribution import com.android.billingclient.api.AcknowledgePurchaseParams import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClient.ProductType import com.android.billingclient.api.BillingClientStateListener Loading @@ -15,7 +14,6 @@ import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ConsumeParams import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.ProductDetails import com.android.billingclient.api.ProductDetailsResult import com.android.billingclient.api.Purchase Loading @@ -38,30 +36,23 @@ import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber @Suppress("TooManyFunctions") class GoogleBillingClient( private val context: Context, internal class GoogleBillingClient( private val clientProvider: GoogleBillingClientProvider, private val productMapper: DataContract.Mapper.Product, private val resultMapper: DataContract.Mapper.BillingResult, private val productCache: Cache<String, ProductDetails>, backgroundDispatcher: CoroutineContext = Dispatchers.IO, ) : DataContract.BillingClient, PurchasesUpdatedListener { private val coroutineScope = CoroutineScope(backgroundDispatcher) private val billingClient: BillingClient by lazy { BillingClient.newBuilder(context) .setListener(this) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build(), ) .build() init { clientProvider.setPurchasesUpdatedListener(this) } private val coroutineScope = CoroutineScope(backgroundDispatcher) override suspend fun <T> connect(onConnected: suspend () -> T): T { return suspendCancellableCoroutine { continuation -> billingClient.startConnection( clientProvider.current.startConnection( object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: BillingResult) { if (billingResult.responseCode == BillingResponseCode.OK) { Loading Loading @@ -95,8 +86,7 @@ class GoogleBillingClient( override fun disconnect() { productCache.clear() // TODO: this is not working as expected and leads to crashes: SERVICE_DISCONNECTED // billingClient.endConnection() clientProvider.clear() } override suspend fun loadOneTimeContributions(productIds: List<String>): List<OneTimeContribution> { Loading Loading @@ -153,7 +143,7 @@ class GoogleBillingClient( .setProductType(ProductType.INAPP) .build() val result = billingClient.queryPurchaseHistory(queryPurchaseHistoryParams) val result = clientProvider.current.queryPurchaseHistory(queryPurchaseHistoryParams) return if (result.billingResult.responseCode == BillingResponseCode.OK) { val recentPurchaseId = result.purchaseHistoryRecordList.orEmpty().firstOrNull()?.products?.filter { productCache.hasKey(it) Loading Loading @@ -182,7 +172,7 @@ class GoogleBillingClient( .setProductList(productList) .build() return billingClient.queryProductDetails(queryProductDetailsParams) return clientProvider.current.queryProductDetails(queryProductDetailsParams) } private fun mapIdToProduct( Loading @@ -200,7 +190,7 @@ class GoogleBillingClient( .setProductType(productType) .build() return billingClient.queryPurchasesAsync(queryPurchaseParams) return clientProvider.current.queryPurchasesAsync(queryPurchaseParams) } override suspend fun purchaseContribution(activity: Activity, contribution: Contribution): Contribution? { Loading @@ -222,7 +212,7 @@ class GoogleBillingClient( .setProductDetailsParamsList(productDetailsParamsList) .build() val result = billingClient.launchBillingFlow(activity, billingFlowParams) val result = clientProvider.current.launchBillingFlow(activity, billingFlowParams) return if (result.responseCode == BillingResponseCode.OK) { contribution } else { Loading Loading @@ -277,7 +267,8 @@ class GoogleBillingClient( .setPurchaseToken(purchase.purchaseToken) .build() val acknowledgeResult: BillingResult = billingClient.acknowledgePurchase(acknowledgePurchaseParams) val acknowledgeResult: BillingResult = clientProvider.current.acknowledgePurchase(acknowledgePurchaseParams) if (acknowledgeResult.responseCode != BillingResponseCode.OK) { contribution Loading @@ -303,6 +294,6 @@ class GoogleBillingClient( // This could fail but we can ignore the error as we handle purchases // the next time the purchases are requested billingClient.consumePurchase(consumeParams) clientProvider.current.consumePurchase(consumeParams) } }
feature/funding/googleplay/src/main/kotlin/app/k9mail/feature/funding/googleplay/data/remote/GoogleBillingClientProvider.kt 0 → 100644 +51 −0 Original line number Diff line number Diff line package app.k9mail.feature.funding.googleplay.data.remote import android.content.Context import app.k9mail.feature.funding.googleplay.data.DataContract import com.android.billingclient.api.BillingClient import com.android.billingclient.api.PendingPurchasesParams import com.android.billingclient.api.PurchasesUpdatedListener /** * Google Billing client provider. * * * It is responsible for creating and managing the billing client instance */ class GoogleBillingClientProvider( private val context: Context, ) : DataContract.Remote.GoogleBillingClientProvider { private var clientInstance: BillingClient? = null override val current: BillingClient get() = if (clientInstance != null) { clientInstance!! } else { clientInstance = createBillingClient() clientInstance!! } private var listener: PurchasesUpdatedListener? = null override fun setPurchasesUpdatedListener(listener: PurchasesUpdatedListener) { this.listener = listener } private fun createBillingClient(): BillingClient { require(listener != null) { "PurchasesUpdatedListener must be set before creating the billing client" } return BillingClient.newBuilder(context) .setListener(listener!!) .enablePendingPurchases( PendingPurchasesParams.newBuilder() .enableOneTimeProducts() .build(), ) .build() } override fun clear() { clientInstance?.endConnection() clientInstance = null } }