From bc41daff284b1bf93eba583346e7849060f665b9 Mon Sep 17 00:00:00 2001 From: Andrii Pedosenko Date: Fri, 13 Feb 2026 14:10:43 +0200 Subject: [PATCH 1/6] Update dependencies --- gradle/libs.versions.toml | 12 ++++++------ gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8cdc45a..278e59e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,18 +5,18 @@ sdk-compile = "35" sdk-target = "35" sdk-min = "24" -android-gp = "8.2.2" -kotlin = "2.0.21" +android-gp = "8.9.1" +kotlin = "2.2.0" -kotlin-ksp = "2.0.21-1.0.26" +kotlin-ksp = "2.2.0-2.0.2" kotlin-coroutines = "1.7.3" -android-core = "1.15.0" +android-core = "1.16.0" android-compose = "1.9.3" -android-appcompat = "1.7.0" +android-appcompat = "1.7.1" android-material = "1.12.0" androidTools = "31.7.2" -android-lifecycle = "2.8.7" +android-lifecycle = "2.9.1" google-billing = "8.0.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a5ff921..db71250 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Thu Jan 25 14:57:44 EET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 56d50a188358bf6a0a11773a68cbd31231044ca3 Mon Sep 17 00:00:00 2001 From: Andrii Pedosenko Date: Fri, 13 Feb 2026 14:11:35 +0200 Subject: [PATCH 2/6] Refactor BillingKtx into smaller components The monolithic `BillingKtx` class has been split into three distinct components to improve separation of concerns: - `BillingConnection`: Manages the connection to the `BillingClient` and observes purchase updates. - `BillingRepository`: Handles data-related operations like querying purchases, product details, and acknowledging/consuming products. - `BillingFlowLauncher`: Manages UI-related tasks such as launching the billing flow and showing in-app messages. The sample app has been updated to use these new components. --- .../appsci/billingktx/sample/MainActivity.kt | 25 +- .../appsci/billingktx/client/BillingKtx.kt | 236 ------------------ .../client/connection/BillingConnection.kt | 12 + .../connection/BillingConnectionImpl.kt | 68 +++++ .../client/repository/BillingRepository.kt | 28 +++ .../repository/BillingRepositoryImpl.kt | 106 ++++++++ .../client/ui/BillingFlowLauncher.kt | 16 ++ .../client/ui/BillingFlowLauncherImpl.kt | 51 ++++ .../lifecycle/BillingKtxLifecycle.kt | 4 +- 9 files changed, 298 insertions(+), 248 deletions(-) delete mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnection.kt create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt diff --git a/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt b/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt index da6a9f8..c32446b 100644 --- a/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt +++ b/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt @@ -20,8 +20,9 @@ import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.QueryProductDetailsParams -import com.appsci.billingktx.client.BillingKtx -import com.appsci.billingktx.client.BillingKtxImpl +import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.client.repository.BillingRepositoryImpl +import com.appsci.billingktx.client.ui.BillingFlowLauncherImpl import com.appsci.billingktx.connection.BillingKtxFactory import com.appsci.billingktx.lifecycle.keepConnection import com.appsci.billingktx.sample.theme.BillingKtxTheme @@ -30,23 +31,27 @@ import timber.log.Timber class MainActivity : ComponentActivity() { - private lateinit var billingKtx: BillingKtx + private lateinit var connection: BillingConnectionImpl + private lateinit var repository: BillingRepositoryImpl + private lateinit var flowLauncher: BillingFlowLauncherImpl override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - billingKtx = BillingKtxImpl( + connection = BillingConnectionImpl( billingFactory = BillingKtxFactory( context = this, enableOneTimeProducts = true, ) ) - billingKtx.keepConnection(this) + repository = BillingRepositoryImpl(connection) + flowLauncher = BillingFlowLauncherImpl(connection) + connection.keepConnection(this) lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - billingKtx.observeUpdates() + connection.observeUpdates() .collect { Timber.d("observeUpdates $it") } @@ -57,7 +62,7 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { val products: Result> = runCatching { - billingKtx.getPurchases(BillingClient.ProductType.SUBS) + repository.getPurchases(BillingClient.ProductType.SUBS) }.onSuccess { Timber.d("getPurchases $it") }.onFailure { @@ -65,7 +70,7 @@ class MainActivity : ComponentActivity() { } val subs: Result> = runCatching { - billingKtx.getPurchases(BillingClient.ProductType.INAPP) + repository.getPurchases(BillingClient.ProductType.INAPP) }.onSuccess { Timber.d("getPurchases $it") }.onFailure { @@ -84,7 +89,7 @@ class MainActivity : ComponentActivity() { .build() ) val productDetailsList = runCatching { - billingKtx.getProductDetails( + repository.getProductDetails( QueryProductDetailsParams.newBuilder() .setProductList( productList, @@ -108,7 +113,7 @@ class MainActivity : ComponentActivity() { ) ) .build() - billingKtx.launchFlow( + flowLauncher.launchFlow( activity = this@MainActivity, params = flowParams, ) diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt deleted file mode 100644 index 7fe643e..0000000 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.appsci.billingktx.client - -import android.app.Activity -import com.android.billingclient.api.AcknowledgePurchaseParams -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.BillingClient.FeatureType -import com.android.billingclient.api.BillingConfig -import com.android.billingclient.api.BillingFlowParams -import com.android.billingclient.api.ConsumeParams -import com.android.billingclient.api.GetBillingConfigParams -import com.android.billingclient.api.InAppMessageParams -import com.android.billingclient.api.InAppMessageResult -import com.android.billingclient.api.ProductDetails -import com.android.billingclient.api.Purchase -import com.android.billingclient.api.PurchasesUpdatedListener -import com.android.billingclient.api.QueryProductDetailsParams -import com.android.billingclient.api.QueryPurchasesParams -import com.android.billingclient.api.acknowledgePurchase -import com.android.billingclient.api.consumePurchase -import com.android.billingclient.api.queryProductDetails -import com.android.billingclient.api.queryPurchasesAsync -import com.appsci.billingktx.connection.BillingKtxFactory -import com.appsci.billingktx.exception.BillingException -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine - -interface BillingKtx { - - fun connect(): Flow - - suspend fun isFeatureSupported(@FeatureType feature: String): Boolean - - fun observeUpdates(): Flow - - suspend fun getPurchases(@BillingClient.ProductType productType: String): List - - /** - * do not mix subs and inapp types in the same params object - */ - suspend fun getProductDetails(params: QueryProductDetailsParams): List - - suspend fun launchFlow(activity: Activity, params: BillingFlowParams) - - suspend fun showInappMessages( - activity: Activity, - params: InAppMessageParams, - ): InAppMessageResult - - suspend fun consumeProduct(params: ConsumeParams) - - suspend fun acknowledge(params: AcknowledgePurchaseParams) - - suspend fun getBillingConfig(): BillingConfig -} - -class BillingKtxImpl( - billingFactory: BillingKtxFactory, - scope: CoroutineScope = CoroutineScope(SupervisorJob()), -) : BillingKtx { - - private val updatesFlow = MutableSharedFlow() - - private val updatedListener = PurchasesUpdatedListener { result, purchases -> - val event = when (val responseCode = result.responseCode) { - BillingClient.BillingResponseCode.OK -> PurchasesUpdate.Success( - responseCode, - purchases.orEmpty() - ) - - BillingClient.BillingResponseCode.USER_CANCELED -> PurchasesUpdate.Canceled( - responseCode, - purchases.orEmpty() - ) - - else -> PurchasesUpdate.Failed(responseCode, purchases.orEmpty()) - } - scope.launch { - updatesFlow.emit(event) - } - } - - private val connectionFlow = billingFactory - .createBillingClientFlow(updatedListener) - - override fun connect(): Flow { - return connectionFlow - } - - override suspend fun isFeatureSupported(@FeatureType feature: String): Boolean { - return withConnectedClient { - val result = it.isFeatureSupported(feature) - result.responseCode == BillingClient.BillingResponseCode.OK - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - override fun observeUpdates(): Flow { - return connectionFlow.flatMapLatest { - updatesFlow - } - } - - override suspend fun getPurchases(@BillingClient.ProductType productType: String): List { - return getBoughtItems(productType) - } - - override suspend fun getProductDetails(params: QueryProductDetailsParams): List { - return withConnectedClient { client -> - val detailsResult = client.queryProductDetails(params) - val billingResult = detailsResult.billingResult - val productDetails = detailsResult.productDetailsList - val responseCode = billingResult.responseCode - if (isSuccess(responseCode)) { - productDetails.orEmpty() - } else { - throw BillingException.fromResult(billingResult) - } - } - } - - override suspend fun launchFlow(activity: Activity, params: BillingFlowParams) { - return withConnectedClient { - val billingResult = withContext(Dispatchers.Main) { - it.launchBillingFlow(activity, params) - } - if (isSuccess(billingResult.responseCode)) { - Unit - } else { - throw BillingException.fromResult(billingResult) - } - } - } - - override suspend fun showInappMessages( - activity: Activity, - params: InAppMessageParams, - ): InAppMessageResult { - return withConnectedClient { client -> - suspendCoroutine { - client.showInAppMessages(activity, params) { result: InAppMessageResult -> - val responseCode = result.responseCode - if (isSuccess(responseCode)) { - it.resume(result) - } else { - it.resumeWithException( - BillingException.fromResponseCode(responseCode) - ) - } - } - } - } - } - - override suspend fun consumeProduct(params: ConsumeParams) { - return withConnectedClient { client -> - val consumePurchase = client.consumePurchase(params) - val billingResult = consumePurchase.billingResult - val responseCode = billingResult.responseCode - if (isSuccess(responseCode)) { - Unit - } else { - throw BillingException.fromResult(billingResult) - } - } - } - - override suspend fun acknowledge(params: AcknowledgePurchaseParams) { - return withConnectedClient { client -> - val billingResult = client.acknowledgePurchase(params) - val responseCode = billingResult.responseCode - if (isSuccess(responseCode)) { - Unit - } else { - throw BillingException.fromResult(billingResult) - } - } - } - - override suspend fun getBillingConfig(): BillingConfig { - return withConnectedClient { client -> - val params = GetBillingConfigParams - .newBuilder() - .build() - suspendCoroutine { - client.getBillingConfigAsync(params) { billingResult, config -> - if (isSuccess(billingResult.responseCode) && config != null) { - it.resume(config) - } else { - it.resumeWithException(BillingException.fromResult(billingResult)) - } - } - } - } - } - - private suspend fun getBoughtItems(@BillingClient.ProductType type: String): List { - return withConnectedClient { - val params = QueryPurchasesParams.newBuilder() - .setProductType(type) - .build() - val purchasesResult = it.queryPurchasesAsync(params) - val billingResult = purchasesResult.billingResult - val purchasesList = purchasesResult.purchasesList - - if (isSuccess(billingResult.responseCode)) { - purchasesList - } else { - throw BillingException.fromResult(billingResult) - } - } - } - - private suspend fun withConnectedClient( - block: suspend (BillingClient) -> R, - ): R { - return connectionFlow.map { - block(it) - }.first() - } - - private fun isSuccess(@BillingClient.BillingResponseCode responseCode: Int): Boolean { - return responseCode == BillingClient.BillingResponseCode.OK - } -} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnection.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnection.kt new file mode 100644 index 0000000..00a10ff --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnection.kt @@ -0,0 +1,12 @@ +package com.appsci.billingktx.client.connection + +import com.android.billingclient.api.BillingClient +import com.appsci.billingktx.client.PurchasesUpdate +import kotlinx.coroutines.flow.Flow + +interface BillingConnection { + + fun connect(): Flow + + fun observeUpdates(): Flow +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt new file mode 100644 index 0000000..73b9a80 --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt @@ -0,0 +1,68 @@ +package com.appsci.billingktx.client.connection + +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.PurchasesUpdatedListener +import com.appsci.billingktx.client.PurchasesUpdate +import com.appsci.billingktx.connection.BillingKtxFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +class BillingConnectionImpl( + billingFactory: BillingKtxFactory, + scope: CoroutineScope = CoroutineScope(SupervisorJob()), +) : BillingConnection { + + private val updatesFlow = MutableSharedFlow() + + private val updatedListener = PurchasesUpdatedListener { result, purchases -> + val event = when (val responseCode = result.responseCode) { + BillingClient.BillingResponseCode.OK -> PurchasesUpdate.Success( + responseCode, + purchases.orEmpty() + ) + + BillingClient.BillingResponseCode.USER_CANCELED -> PurchasesUpdate.Canceled( + responseCode, + purchases.orEmpty() + ) + + else -> PurchasesUpdate.Failed(responseCode, purchases.orEmpty()) + } + scope.launch { + updatesFlow.emit(event) + } + } + + private val connectionFlow = billingFactory + .createBillingClientFlow(updatedListener) + + override fun connect(): Flow { + return connectionFlow + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun observeUpdates(): Flow { + return connectionFlow.flatMapLatest { + updatesFlow + } + } + + internal suspend fun withConnectedClient( + block: suspend (BillingClient) -> R, + ): R { + return connectionFlow.map { + block(it) + }.first() + } + + internal fun isSuccess(@BillingClient.BillingResponseCode responseCode: Int): Boolean { + return responseCode == BillingClient.BillingResponseCode.OK + } +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt new file mode 100644 index 0000000..3be6a7e --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt @@ -0,0 +1,28 @@ +package com.appsci.billingktx.client.repository + +import com.android.billingclient.api.AcknowledgePurchaseParams +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClient.FeatureType +import com.android.billingclient.api.BillingConfig +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.QueryProductDetailsParams + +interface BillingRepository { + + suspend fun isFeatureSupported(@FeatureType feature: String): Boolean + + suspend fun getPurchases(@BillingClient.ProductType productType: String): List + + /** + * do not mix subs and inapp types in the same params object + */ + suspend fun getProductDetails(params: QueryProductDetailsParams): List + + suspend fun getBillingConfig(): BillingConfig + + suspend fun consumeProduct(params: ConsumeParams) + + suspend fun acknowledge(params: AcknowledgePurchaseParams) +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt new file mode 100644 index 0000000..2837c01 --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt @@ -0,0 +1,106 @@ +package com.appsci.billingktx.client.repository + +import com.android.billingclient.api.AcknowledgePurchaseParams +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClient.FeatureType +import com.android.billingclient.api.BillingConfig +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.GetBillingConfigParams +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.QueryProductDetailsParams +import com.android.billingclient.api.QueryPurchasesParams +import com.android.billingclient.api.acknowledgePurchase +import com.android.billingclient.api.consumePurchase +import com.android.billingclient.api.queryProductDetails +import com.android.billingclient.api.queryPurchasesAsync +import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.exception.BillingException +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class BillingRepositoryImpl( + private val connection: BillingConnectionImpl, +) : BillingRepository { + + override suspend fun isFeatureSupported(@FeatureType feature: String): Boolean { + return connection.withConnectedClient { + val result = it.isFeatureSupported(feature) + result.responseCode == BillingClient.BillingResponseCode.OK + } + } + + override suspend fun getPurchases(@BillingClient.ProductType productType: String): List { + return connection.withConnectedClient { + val params = QueryPurchasesParams.newBuilder() + .setProductType(productType) + .build() + val purchasesResult = it.queryPurchasesAsync(params) + val billingResult = purchasesResult.billingResult + val purchasesList = purchasesResult.purchasesList + + if (connection.isSuccess(billingResult.responseCode)) { + purchasesList + } else { + throw BillingException.fromResult(billingResult) + } + } + } + + override suspend fun getProductDetails(params: QueryProductDetailsParams): List { + return connection.withConnectedClient { client -> + val detailsResult = client.queryProductDetails(params) + val billingResult = detailsResult.billingResult + val productDetails = detailsResult.productDetailsList + val responseCode = billingResult.responseCode + if (connection.isSuccess(responseCode)) { + productDetails.orEmpty() + } else { + throw BillingException.fromResult(billingResult) + } + } + } + + override suspend fun getBillingConfig(): BillingConfig { + return connection.withConnectedClient { client -> + val params = GetBillingConfigParams + .newBuilder() + .build() + suspendCancellableCoroutine { + client.getBillingConfigAsync(params) { billingResult, config -> + if (connection.isSuccess(billingResult.responseCode) && config != null) { + it.resume(config) + } else { + it.resumeWithException(BillingException.fromResult(billingResult)) + } + } + } + } + } + + override suspend fun consumeProduct(params: ConsumeParams) { + return connection.withConnectedClient { client -> + val consumePurchase = client.consumePurchase(params) + val billingResult = consumePurchase.billingResult + val responseCode = billingResult.responseCode + if (connection.isSuccess(responseCode)) { + Unit + } else { + throw BillingException.fromResult(billingResult) + } + } + } + + override suspend fun acknowledge(params: AcknowledgePurchaseParams) { + return connection.withConnectedClient { client -> + val billingResult = client.acknowledgePurchase(params) + val responseCode = billingResult.responseCode + if (connection.isSuccess(responseCode)) { + Unit + } else { + throw BillingException.fromResult(billingResult) + } + } + } +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt new file mode 100644 index 0000000..2718457 --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt @@ -0,0 +1,16 @@ +package com.appsci.billingktx.client.ui + +import android.app.Activity +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.InAppMessageParams +import com.android.billingclient.api.InAppMessageResult + +interface BillingFlowLauncher { + + suspend fun launchFlow(activity: Activity, params: BillingFlowParams) + + suspend fun showInappMessages( + activity: Activity, + params: InAppMessageParams, + ): InAppMessageResult +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt new file mode 100644 index 0000000..426e95e --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt @@ -0,0 +1,51 @@ +package com.appsci.billingktx.client.ui + +import android.app.Activity +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.InAppMessageParams +import com.android.billingclient.api.InAppMessageResult +import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.exception.BillingException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +class BillingFlowLauncherImpl( + private val connection: BillingConnectionImpl, +) : BillingFlowLauncher { + + override suspend fun launchFlow(activity: Activity, params: BillingFlowParams) { + return connection.withConnectedClient { + val billingResult = withContext(Dispatchers.Main) { + it.launchBillingFlow(activity, params) + } + if (connection.isSuccess(billingResult.responseCode)) { + Unit + } else { + throw BillingException.fromResult(billingResult) + } + } + } + + override suspend fun showInappMessages( + activity: Activity, + params: InAppMessageParams, + ): InAppMessageResult { + return connection.withConnectedClient { client -> + suspendCancellableCoroutine { + client.showInAppMessages(activity, params) { result: InAppMessageResult -> + val responseCode = result.responseCode + if (connection.isSuccess(responseCode)) { + it.resume(result) + } else { + it.resumeWithException( + BillingException.fromResponseCode(responseCode) + ) + } + } + } + } + } +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/lifecycle/BillingKtxLifecycle.kt b/billingKtx/src/main/java/com/appsci/billingktx/lifecycle/BillingKtxLifecycle.kt index c745a77..414292e 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/lifecycle/BillingKtxLifecycle.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/lifecycle/BillingKtxLifecycle.kt @@ -4,13 +4,13 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.appsci.billingktx.client.BillingKtx +import com.appsci.billingktx.client.connection.BillingConnection import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import timber.log.Timber -fun BillingKtx.keepConnection( +fun BillingConnection.keepConnection( lifecycleOwner: LifecycleOwner, state: Lifecycle.State = Lifecycle.State.STARTED, ) { From 3088e42dcb8068b09245ea4792c257fc6f866006 Mon Sep 17 00:00:00 2001 From: Andrii Pedosenko Date: Fri, 13 Feb 2026 14:25:48 +0200 Subject: [PATCH 3/6] Update version to 1.1.0-RC1 --- billingKtx/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/billingKtx/build.gradle.kts b/billingKtx/build.gradle.kts index e18da20..76cbc57 100644 --- a/billingKtx/build.gradle.kts +++ b/billingKtx/build.gradle.kts @@ -49,7 +49,7 @@ afterEvaluate { from(components["release"]) groupId = "com.github.AppSci" artifactId = "billing-ktx" - version = "1.0.0" + version = "1.1.0-RC1" } } From aa83399c039b5715ef0ad5dcdcbfdb9a8f9c8a7c Mon Sep 17 00:00:00 2001 From: Andrii Pedosenko Date: Fri, 13 Feb 2026 15:12:31 +0200 Subject: [PATCH 4/6] Refactor isSuccess to a top-level function --- .../com/appsci/billingktx/sample/MainActivity.kt | 16 ++++++++++------ .../client/connection/BillingConnectionImpl.kt | 3 --- .../billingktx/client/connection/BillingUtils.kt | 7 +++++++ .../client/repository/BillingRepositoryImpl.kt | 13 +++++++------ .../client/ui/BillingFlowLauncherImpl.kt | 5 +++-- 5 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingUtils.kt diff --git a/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt b/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt index c32446b..b321941 100644 --- a/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt +++ b/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt @@ -20,8 +20,11 @@ import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.QueryProductDetailsParams +import com.appsci.billingktx.client.connection.BillingConnection import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.client.repository.BillingRepository import com.appsci.billingktx.client.repository.BillingRepositoryImpl +import com.appsci.billingktx.client.ui.BillingFlowLauncher import com.appsci.billingktx.client.ui.BillingFlowLauncherImpl import com.appsci.billingktx.connection.BillingKtxFactory import com.appsci.billingktx.lifecycle.keepConnection @@ -31,22 +34,23 @@ import timber.log.Timber class MainActivity : ComponentActivity() { - private lateinit var connection: BillingConnectionImpl - private lateinit var repository: BillingRepositoryImpl - private lateinit var flowLauncher: BillingFlowLauncherImpl + private lateinit var connection: BillingConnection + private lateinit var repository: BillingRepository + private lateinit var flowLauncher: BillingFlowLauncher override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - connection = BillingConnectionImpl( + val connectionImpl = BillingConnectionImpl( billingFactory = BillingKtxFactory( context = this, enableOneTimeProducts = true, ) ) - repository = BillingRepositoryImpl(connection) - flowLauncher = BillingFlowLauncherImpl(connection) + connection = connectionImpl + repository = BillingRepositoryImpl(connectionImpl) + flowLauncher = BillingFlowLauncherImpl(connectionImpl) connection.keepConnection(this) lifecycleScope.launch { diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt index 73b9a80..7639f52 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt @@ -62,7 +62,4 @@ class BillingConnectionImpl( }.first() } - internal fun isSuccess(@BillingClient.BillingResponseCode responseCode: Int): Boolean { - return responseCode == BillingClient.BillingResponseCode.OK - } } diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingUtils.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingUtils.kt new file mode 100644 index 0000000..e21995b --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingUtils.kt @@ -0,0 +1,7 @@ +package com.appsci.billingktx.client.connection + +import com.android.billingclient.api.BillingClient + +internal fun isSuccess(@BillingClient.BillingResponseCode responseCode: Int): Boolean { + return responseCode == BillingClient.BillingResponseCode.OK +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt index 2837c01..9b5694b 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt @@ -15,6 +15,7 @@ import com.android.billingclient.api.consumePurchase import com.android.billingclient.api.queryProductDetails import com.android.billingclient.api.queryPurchasesAsync import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.client.connection.isSuccess import com.appsci.billingktx.exception.BillingException import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume @@ -27,7 +28,7 @@ class BillingRepositoryImpl( override suspend fun isFeatureSupported(@FeatureType feature: String): Boolean { return connection.withConnectedClient { val result = it.isFeatureSupported(feature) - result.responseCode == BillingClient.BillingResponseCode.OK + isSuccess(result.responseCode) } } @@ -40,7 +41,7 @@ class BillingRepositoryImpl( val billingResult = purchasesResult.billingResult val purchasesList = purchasesResult.purchasesList - if (connection.isSuccess(billingResult.responseCode)) { + if (isSuccess(billingResult.responseCode)) { purchasesList } else { throw BillingException.fromResult(billingResult) @@ -54,7 +55,7 @@ class BillingRepositoryImpl( val billingResult = detailsResult.billingResult val productDetails = detailsResult.productDetailsList val responseCode = billingResult.responseCode - if (connection.isSuccess(responseCode)) { + if (isSuccess(responseCode)) { productDetails.orEmpty() } else { throw BillingException.fromResult(billingResult) @@ -69,7 +70,7 @@ class BillingRepositoryImpl( .build() suspendCancellableCoroutine { client.getBillingConfigAsync(params) { billingResult, config -> - if (connection.isSuccess(billingResult.responseCode) && config != null) { + if (isSuccess(billingResult.responseCode) && config != null) { it.resume(config) } else { it.resumeWithException(BillingException.fromResult(billingResult)) @@ -84,7 +85,7 @@ class BillingRepositoryImpl( val consumePurchase = client.consumePurchase(params) val billingResult = consumePurchase.billingResult val responseCode = billingResult.responseCode - if (connection.isSuccess(responseCode)) { + if (isSuccess(responseCode)) { Unit } else { throw BillingException.fromResult(billingResult) @@ -96,7 +97,7 @@ class BillingRepositoryImpl( return connection.withConnectedClient { client -> val billingResult = client.acknowledgePurchase(params) val responseCode = billingResult.responseCode - if (connection.isSuccess(responseCode)) { + if (isSuccess(responseCode)) { Unit } else { throw BillingException.fromResult(billingResult) diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt index 426e95e..a1eeb9e 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt @@ -5,6 +5,7 @@ import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.InAppMessageParams import com.android.billingclient.api.InAppMessageResult import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.client.connection.isSuccess import com.appsci.billingktx.exception.BillingException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine @@ -21,7 +22,7 @@ class BillingFlowLauncherImpl( val billingResult = withContext(Dispatchers.Main) { it.launchBillingFlow(activity, params) } - if (connection.isSuccess(billingResult.responseCode)) { + if (isSuccess(billingResult.responseCode)) { Unit } else { throw BillingException.fromResult(billingResult) @@ -37,7 +38,7 @@ class BillingFlowLauncherImpl( suspendCancellableCoroutine { client.showInAppMessages(activity, params) { result: InAppMessageResult -> val responseCode = result.responseCode - if (connection.isSuccess(responseCode)) { + if (isSuccess(responseCode)) { it.resume(result) } else { it.resumeWithException( From 8eece242414c32e50cf61e8f82abfc30ab322f37 Mon Sep 17 00:00:00 2001 From: Andrii Pedosenko Date: Wed, 18 Feb 2026 19:17:54 +0200 Subject: [PATCH 5/6] Refactor: Introduce BillingKtx facade Introduced `BillingKtx` as a unified entry point to the library, simplifying its public API. This facade encapsulates the previously separate `BillingRepository`, `BillingFlowLauncher`, and `BillingConnection` components. Internal implementation details, including `BillingApi`, `BillingFlowLauncher`, and `BillingConnectionImpl`, are now marked as `internal` to hide them from consumers. Additionally, the Google Billing dependency is now exposed as an `api` dependency to allow consumers to use its types directly. --- .../appsci/billingktx/sample/MainActivity.kt | 36 +++------ billingKtx/build.gradle.kts | 2 +- .../appsci/billingktx/client/BillingKtx.kt | 74 +++++++++++++++++++ .../connection/BillingConnectionImpl.kt | 2 +- ...BillingRepositoryImpl.kt => BillingApi.kt} | 16 ++-- .../client/repository/BillingRepository.kt | 28 ------- .../client/ui/BillingFlowLauncher.kt | 42 ++++++++++- .../client/ui/BillingFlowLauncherImpl.kt | 52 ------------- .../connection/BillingKtxFactory.kt | 4 +- 9 files changed, 136 insertions(+), 120 deletions(-) create mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt rename billingKtx/src/main/java/com/appsci/billingktx/client/repository/{BillingRepositoryImpl.kt => BillingApi.kt} (87%) delete mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt delete mode 100644 billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt diff --git a/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt b/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt index b321941..ca679be 100644 --- a/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt +++ b/app/src/main/java/com/appsci/billingktx/sample/MainActivity.kt @@ -20,13 +20,7 @@ import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.Purchase import com.android.billingclient.api.QueryProductDetailsParams -import com.appsci.billingktx.client.connection.BillingConnection -import com.appsci.billingktx.client.connection.BillingConnectionImpl -import com.appsci.billingktx.client.repository.BillingRepository -import com.appsci.billingktx.client.repository.BillingRepositoryImpl -import com.appsci.billingktx.client.ui.BillingFlowLauncher -import com.appsci.billingktx.client.ui.BillingFlowLauncherImpl -import com.appsci.billingktx.connection.BillingKtxFactory +import com.appsci.billingktx.client.BillingKtx import com.appsci.billingktx.lifecycle.keepConnection import com.appsci.billingktx.sample.theme.BillingKtxTheme import kotlinx.coroutines.launch @@ -34,28 +28,21 @@ import timber.log.Timber class MainActivity : ComponentActivity() { - private lateinit var connection: BillingConnection - private lateinit var repository: BillingRepository - private lateinit var flowLauncher: BillingFlowLauncher + private lateinit var billingKtx: BillingKtx override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - val connectionImpl = BillingConnectionImpl( - billingFactory = BillingKtxFactory( - context = this, - enableOneTimeProducts = true, - ) + billingKtx = BillingKtx( + context = this, + enableOneTimeProducts = true, ) - connection = connectionImpl - repository = BillingRepositoryImpl(connectionImpl) - flowLauncher = BillingFlowLauncherImpl(connectionImpl) - connection.keepConnection(this) + billingKtx.keepConnection(this) lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - connection.observeUpdates() + billingKtx.observeUpdates() .collect { Timber.d("observeUpdates $it") } @@ -66,7 +53,7 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { val products: Result> = runCatching { - repository.getPurchases(BillingClient.ProductType.SUBS) + billingKtx.getPurchases(BillingClient.ProductType.SUBS) }.onSuccess { Timber.d("getPurchases $it") }.onFailure { @@ -74,7 +61,7 @@ class MainActivity : ComponentActivity() { } val subs: Result> = runCatching { - repository.getPurchases(BillingClient.ProductType.INAPP) + billingKtx.getPurchases(BillingClient.ProductType.INAPP) }.onSuccess { Timber.d("getPurchases $it") }.onFailure { @@ -93,7 +80,7 @@ class MainActivity : ComponentActivity() { .build() ) val productDetailsList = runCatching { - repository.getProductDetails( + billingKtx.getProductDetails( QueryProductDetailsParams.newBuilder() .setProductList( productList, @@ -117,7 +104,7 @@ class MainActivity : ComponentActivity() { ) ) .build() - flowLauncher.launchFlow( + billingKtx.launchFlow( activity = this@MainActivity, params = flowParams, ) @@ -127,7 +114,6 @@ class MainActivity : ComponentActivity() { } setContent { BillingKtxTheme { - // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background diff --git a/billingKtx/build.gradle.kts b/billingKtx/build.gradle.kts index 76cbc57..16ff64c 100644 --- a/billingKtx/build.gradle.kts +++ b/billingKtx/build.gradle.kts @@ -78,7 +78,7 @@ dependencies { implementation(libs.android.material) implementation(libs.android.lifecycle) - implementation(libs.google.billing) + api(libs.google.billing) implementation(libs.utils.timber) } diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt new file mode 100644 index 0000000..87ea5f1 --- /dev/null +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/BillingKtx.kt @@ -0,0 +1,74 @@ +package com.appsci.billingktx.client + +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.BillingClient.FeatureType +import com.android.billingclient.api.BillingConfig +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.ConsumeParams +import com.android.billingclient.api.InAppMessageParams +import com.android.billingclient.api.InAppMessageResult +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.Purchase +import com.android.billingclient.api.QueryProductDetailsParams +import com.appsci.billingktx.client.connection.BillingConnection +import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.client.repository.BillingApi +import com.appsci.billingktx.client.ui.BillingFlowLauncher +import com.appsci.billingktx.connection.BillingKtxFactory +import kotlinx.coroutines.flow.Flow + +class BillingKtx( + context: Context, + enableOneTimeProducts: Boolean = false, + enablePrepaidPlans: Boolean = false, + enableAutoServiceReconnection: Boolean = false, +) : BillingConnection { + + private val connection = BillingConnectionImpl( + BillingKtxFactory( + context = context, + enableOneTimeProducts = enableOneTimeProducts, + enablePrepaidPlans = enablePrepaidPlans, + enableAutoServiceReconnection = enableAutoServiceReconnection, + ), + ) + + private val api = BillingApi(connection) + private val flowLauncher = BillingFlowLauncher(connection) + + override fun connect(): Flow = connection.connect() + + override fun observeUpdates(): Flow = connection.observeUpdates() + + suspend fun isFeatureSupported(@FeatureType feature: String): Boolean = + api.isFeatureSupported(feature) + + suspend fun getPurchases(@BillingClient.ProductType productType: String): List = + api.getPurchases(productType) + + /** + * Do not mix subs and inapp types in the same params object. + */ + suspend fun getProductDetails(params: QueryProductDetailsParams): List = + api.getProductDetails(params) + + suspend fun getBillingConfig(): BillingConfig = + api.getBillingConfig() + + suspend fun consumeProduct(params: ConsumeParams) = + api.consumeProduct(params) + + suspend fun acknowledge(params: AcknowledgePurchaseParams) = + api.acknowledge(params) + + suspend fun launchFlow(activity: Activity, params: BillingFlowParams) = + flowLauncher.launchFlow(activity, params) + + suspend fun showInappMessages( + activity: Activity, + params: InAppMessageParams, + ): InAppMessageResult = flowLauncher.showInappMessages(activity, params) +} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt index 7639f52..d4e7614 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/connection/BillingConnectionImpl.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -class BillingConnectionImpl( +internal class BillingConnectionImpl( billingFactory: BillingKtxFactory, scope: CoroutineScope = CoroutineScope(SupervisorJob()), ) : BillingConnection { diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingApi.kt similarity index 87% rename from billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt rename to billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingApi.kt index 9b5694b..c28a180 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepositoryImpl.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingApi.kt @@ -21,18 +21,18 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException -class BillingRepositoryImpl( +internal class BillingApi( private val connection: BillingConnectionImpl, -) : BillingRepository { +) { - override suspend fun isFeatureSupported(@FeatureType feature: String): Boolean { + suspend fun isFeatureSupported(@FeatureType feature: String): Boolean { return connection.withConnectedClient { val result = it.isFeatureSupported(feature) isSuccess(result.responseCode) } } - override suspend fun getPurchases(@BillingClient.ProductType productType: String): List { + suspend fun getPurchases(@BillingClient.ProductType productType: String): List { return connection.withConnectedClient { val params = QueryPurchasesParams.newBuilder() .setProductType(productType) @@ -49,7 +49,7 @@ class BillingRepositoryImpl( } } - override suspend fun getProductDetails(params: QueryProductDetailsParams): List { + suspend fun getProductDetails(params: QueryProductDetailsParams): List { return connection.withConnectedClient { client -> val detailsResult = client.queryProductDetails(params) val billingResult = detailsResult.billingResult @@ -63,7 +63,7 @@ class BillingRepositoryImpl( } } - override suspend fun getBillingConfig(): BillingConfig { + suspend fun getBillingConfig(): BillingConfig { return connection.withConnectedClient { client -> val params = GetBillingConfigParams .newBuilder() @@ -80,7 +80,7 @@ class BillingRepositoryImpl( } } - override suspend fun consumeProduct(params: ConsumeParams) { + suspend fun consumeProduct(params: ConsumeParams) { return connection.withConnectedClient { client -> val consumePurchase = client.consumePurchase(params) val billingResult = consumePurchase.billingResult @@ -93,7 +93,7 @@ class BillingRepositoryImpl( } } - override suspend fun acknowledge(params: AcknowledgePurchaseParams) { + suspend fun acknowledge(params: AcknowledgePurchaseParams) { return connection.withConnectedClient { client -> val billingResult = client.acknowledgePurchase(params) val responseCode = billingResult.responseCode diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt deleted file mode 100644 index 3be6a7e..0000000 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/repository/BillingRepository.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.appsci.billingktx.client.repository - -import com.android.billingclient.api.AcknowledgePurchaseParams -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.BillingClient.FeatureType -import com.android.billingclient.api.BillingConfig -import com.android.billingclient.api.ConsumeParams -import com.android.billingclient.api.ProductDetails -import com.android.billingclient.api.Purchase -import com.android.billingclient.api.QueryProductDetailsParams - -interface BillingRepository { - - suspend fun isFeatureSupported(@FeatureType feature: String): Boolean - - suspend fun getPurchases(@BillingClient.ProductType productType: String): List - - /** - * do not mix subs and inapp types in the same params object - */ - suspend fun getProductDetails(params: QueryProductDetailsParams): List - - suspend fun getBillingConfig(): BillingConfig - - suspend fun consumeProduct(params: ConsumeParams) - - suspend fun acknowledge(params: AcknowledgePurchaseParams) -} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt index 2718457..45e2bae 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncher.kt @@ -4,13 +4,49 @@ import android.app.Activity import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.InAppMessageParams import com.android.billingclient.api.InAppMessageResult +import com.appsci.billingktx.client.connection.BillingConnectionImpl +import com.appsci.billingktx.client.connection.isSuccess +import com.appsci.billingktx.exception.BillingException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException -interface BillingFlowLauncher { +internal class BillingFlowLauncher( + private val connection: BillingConnectionImpl, +) { - suspend fun launchFlow(activity: Activity, params: BillingFlowParams) + suspend fun launchFlow(activity: Activity, params: BillingFlowParams) { + return connection.withConnectedClient { + val billingResult = withContext(Dispatchers.Main) { + it.launchBillingFlow(activity, params) + } + if (isSuccess(billingResult.responseCode)) { + Unit + } else { + throw BillingException.fromResult(billingResult) + } + } + } suspend fun showInappMessages( activity: Activity, params: InAppMessageParams, - ): InAppMessageResult + ): InAppMessageResult { + return connection.withConnectedClient { client -> + suspendCancellableCoroutine { + client.showInAppMessages(activity, params) { result: InAppMessageResult -> + val responseCode = result.responseCode + if (isSuccess(responseCode)) { + it.resume(result) + } else { + it.resumeWithException( + BillingException.fromResponseCode(responseCode) + ) + } + } + } + } + } } diff --git a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt b/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt deleted file mode 100644 index a1eeb9e..0000000 --- a/billingKtx/src/main/java/com/appsci/billingktx/client/ui/BillingFlowLauncherImpl.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.appsci.billingktx.client.ui - -import android.app.Activity -import com.android.billingclient.api.BillingFlowParams -import com.android.billingclient.api.InAppMessageParams -import com.android.billingclient.api.InAppMessageResult -import com.appsci.billingktx.client.connection.BillingConnectionImpl -import com.appsci.billingktx.client.connection.isSuccess -import com.appsci.billingktx.exception.BillingException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -class BillingFlowLauncherImpl( - private val connection: BillingConnectionImpl, -) : BillingFlowLauncher { - - override suspend fun launchFlow(activity: Activity, params: BillingFlowParams) { - return connection.withConnectedClient { - val billingResult = withContext(Dispatchers.Main) { - it.launchBillingFlow(activity, params) - } - if (isSuccess(billingResult.responseCode)) { - Unit - } else { - throw BillingException.fromResult(billingResult) - } - } - } - - override suspend fun showInappMessages( - activity: Activity, - params: InAppMessageParams, - ): InAppMessageResult { - return connection.withConnectedClient { client -> - suspendCancellableCoroutine { - client.showInAppMessages(activity, params) { result: InAppMessageResult -> - val responseCode = result.responseCode - if (isSuccess(responseCode)) { - it.resume(result) - } else { - it.resumeWithException( - BillingException.fromResponseCode(responseCode) - ) - } - } - } - } - } -} diff --git a/billingKtx/src/main/java/com/appsci/billingktx/connection/BillingKtxFactory.kt b/billingKtx/src/main/java/com/appsci/billingktx/connection/BillingKtxFactory.kt index fa30346..da8a04a 100644 --- a/billingKtx/src/main/java/com/appsci/billingktx/connection/BillingKtxFactory.kt +++ b/billingKtx/src/main/java/com/appsci/billingktx/connection/BillingKtxFactory.kt @@ -22,7 +22,7 @@ import kotlinx.coroutines.flow.retryWhen import kotlinx.coroutines.flow.shareIn import timber.log.Timber -class BillingKtxFactory( +internal class BillingKtxFactory( private val context: Context, private val transform: (Flow) -> Flow = DefaultTransform( sharingScope = CoroutineScope(SupervisorJob()), @@ -93,7 +93,7 @@ class BillingKtxFactory( } } -class DefaultTransform( +internal class DefaultTransform( private val sharingScope: CoroutineScope, private val timeOut: Long = 3_000, ) : (Flow) -> Flow { From d84caf1eda3d7104ebe2cb07d4751b3ef250c17a Mon Sep 17 00:00:00 2001 From: Andrii Pedosenko Date: Wed, 18 Feb 2026 19:18:20 +0200 Subject: [PATCH 6/6] Update version to 1.1.0-RC2 --- billingKtx/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/billingKtx/build.gradle.kts b/billingKtx/build.gradle.kts index 16ff64c..d56deda 100644 --- a/billingKtx/build.gradle.kts +++ b/billingKtx/build.gradle.kts @@ -49,7 +49,7 @@ afterEvaluate { from(components["release"]) groupId = "com.github.AppSci" artifactId = "billing-ktx" - version = "1.1.0-RC1" + version = "1.1.0-RC2" } }