diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-11-16 00:50:20 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-11-16 09:34:22 +0100 |
| commit | c3e17fdf1965697c61708aa186a7ff2d4f9f1ae2 (patch) | |
| tree | 46a60b77a1b3da1baab08035118c0f73bc79edb3 | |
| parent | cdd0b8e0d55de25fd6d2fef975cc7dab26ec324d (diff) | |
| download | mullvadvpn-c3e17fdf1965697c61708aa186a7ff2d4f9f1ae2.tar.xz mullvadvpn-c3e17fdf1965697c61708aa186a7ff2d4f9f1ae2.zip | |
Add BillingRepository that handles calls to billing client
3 files changed, 220 insertions, 0 deletions
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt new file mode 100644 index 0000000000..6274f8cb6f --- /dev/null +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt @@ -0,0 +1,194 @@ +package net.mullvad.mullvadvpn.lib.billing + +import android.app.Activity +import android.content.Context +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClient.BillingResponseCode +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.ProductDetailsResult +import com.android.billingclient.api.PurchasesResult +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.QueryProductDetailsParams +import com.android.billingclient.api.QueryProductDetailsParams.Product +import com.android.billingclient.api.QueryPurchasesParams +import com.android.billingclient.api.queryProductDetails +import com.android.billingclient.api.queryPurchasesAsync +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import net.mullvad.mullvadvpn.lib.billing.model.BillingException +import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent + +class BillingRepository(context: Context) { + + private val billingClient: BillingClient + + private val purchaseUpdateListener: PurchasesUpdatedListener = + PurchasesUpdatedListener { result, purchases -> + when (result.responseCode) { + BillingResponseCode.OK -> { + _purchaseEvents.tryEmit( + PurchaseEvent.Completed(purchases?.toList() ?: emptyList()) + ) + } + BillingResponseCode.USER_CANCELED -> { + _purchaseEvents.tryEmit(PurchaseEvent.UserCanceled) + } + else -> { + _purchaseEvents.tryEmit( + PurchaseEvent.Error( + exception = + BillingException( + responseCode = result.responseCode, + message = result.debugMessage + ) + ) + ) + } + } + } + + private val _purchaseEvents = MutableSharedFlow<PurchaseEvent>(extraBufferCapacity = 1) + val purchaseEvents = _purchaseEvents.asSharedFlow() + + init { + billingClient = + BillingClient.newBuilder(context) + .enablePendingPurchases() + .setListener(purchaseUpdateListener) + .build() + } + + private val ensureConnectedMutex = Mutex() + + private suspend fun ensureConnected() = + ensureConnectedMutex.withLock { + suspendCoroutine { + if ( + billingClient.isReady && + billingClient.connectionState == BillingClient.ConnectionState.CONNECTED + ) { + it.resume(Unit) + } else { + startConnection(it) + } + } + } + + private fun startConnection(continuation: Continuation<Unit>) { + billingClient.startConnection( + object : BillingClientStateListener { + override fun onBillingServiceDisconnected() { + // Maybe do something here? + continuation.resumeWithException( + BillingException( + BillingResponseCode.SERVICE_DISCONNECTED, + "Billing service disconnected" + ) + ) + } + + override fun onBillingSetupFinished(result: BillingResult) { + if (result.responseCode == BillingResponseCode.OK) { + continuation.resume(Unit) + } else { + continuation.resumeWithException( + BillingException(result.responseCode, result.debugMessage) + ) + } + } + } + ) + } + + suspend fun queryProducts(productIds: List<String>): ProductDetailsResult { + return queryProductDetails(productIds) + } + + suspend fun startPurchaseFlow( + productDetails: ProductDetails, + obfuscatedId: String, + activityProvider: () -> Activity + ): BillingResult { + return try { + ensureConnected() + + val productDetailsParamsList = + listOf( + BillingFlowParams.ProductDetailsParams.newBuilder() + .setProductDetails(productDetails) + .build() + ) + + val billingFlowParams = + BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) + .setObfuscatedAccountId(obfuscatedId) + .build() + + val activity = activityProvider() + // Launch the billing flow + billingClient.launchBillingFlow(activity, billingFlowParams) + } catch (t: Throwable) { + if (t is BillingException) { + t.toBillingResult() + } else { + throw t + } + } + } + + suspend fun queryPurchases(): PurchasesResult { + return try { + ensureConnected() + + val queryPurchaseHistoryParams: QueryPurchasesParams = + QueryPurchasesParams.newBuilder() + .setProductType(BillingClient.ProductType.INAPP) + .build() + + billingClient.queryPurchasesAsync(queryPurchaseHistoryParams) + } catch (t: Throwable) { + if (t is BillingException) { + t.toPurchasesResult() + } else { + throw t + } + } + } + + private suspend fun queryProductDetails(productIds: List<String>): ProductDetailsResult { + return try { + ensureConnected() + + val productList = + productIds.map { productId -> + Product.newBuilder() + .setProductId(productId) + .setProductType(BillingClient.ProductType.INAPP) + .build() + } + val params = QueryProductDetailsParams.newBuilder() + params.setProductList(productList) + + billingClient.queryProductDetails(params.build()) + } catch (t: Throwable) { + if (t is BillingException) { + return ProductDetailsResult(t.toBillingResult(), null) + } else { + return ProductDetailsResult( + BillingResult.newBuilder().setResponseCode(BillingResponseCode.ERROR).build(), + null + ) + } + } + } +} diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/BillingException.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/BillingException.kt new file mode 100644 index 0000000000..08f6a89cca --- /dev/null +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/BillingException.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.lib.billing.model + +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.PurchasesResult + +class BillingException(private val responseCode: Int, message: String) : Throwable(message) { + + fun toBillingResult(): BillingResult = + BillingResult.newBuilder() + .setResponseCode(responseCode) + .setDebugMessage(message ?: "") + .build() + + fun toPurchasesResult(): PurchasesResult = PurchasesResult(toBillingResult(), emptyList()) +} diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/PurchaseEvent.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/PurchaseEvent.kt new file mode 100644 index 0000000000..b88f31cae6 --- /dev/null +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/PurchaseEvent.kt @@ -0,0 +1,11 @@ +package net.mullvad.mullvadvpn.lib.billing.model + +import com.android.billingclient.api.Purchase + +sealed interface PurchaseEvent { + data object UserCanceled : PurchaseEvent + + data class Error(val exception: BillingException) : PurchaseEvent + + data class Completed(val purchases: List<Purchase>) : PurchaseEvent +} |
