summaryrefslogtreecommitdiffhomepage
path: root/android/lib
diff options
context:
space:
mode:
Diffstat (limited to 'android/lib')
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt194
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/BillingException.kt15
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/model/PurchaseEvent.kt11
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
+}