summaryrefslogtreecommitdiffhomepage
path: root/android/lib/billing/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 00:56:10 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 09:34:26 +0100
commitae7471c50a653133aae6472199d7b0d16ad2a145 (patch)
tree20488d174aae09caee907ac20960e26146b1aa9f /android/lib/billing/src
parent5c5c2a95d676648ffbd953b5f9e8587a8a80bf66 (diff)
downloadmullvadvpn-ae7471c50a653133aae6472199d7b0d16ad2a145.tar.xz
mullvadvpn-ae7471c50a653133aae6472199d7b0d16ad2a145.zip
Add payment module and billing payment repository
Diffstat (limited to 'android/lib/billing/src')
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt167
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultExtensions.kt13
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt37
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsToPaymentProduct.kt17
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseEventToPurchaseResult.kt11
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseStateToPaymentStatus.kt11
-rw-r--r--android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchasesResultExtensions.kt13
7 files changed, 269 insertions, 0 deletions
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt
new file mode 100644
index 0000000000..76df623ada
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt
@@ -0,0 +1,167 @@
+package net.mullvad.mullvadvpn.lib.billing
+
+import android.app.Activity
+import com.android.billingclient.api.BillingClient.BillingResponseCode
+import com.android.billingclient.api.Purchase
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flow
+import net.mullvad.mullvadvpn.lib.billing.extension.getProductDetails
+import net.mullvad.mullvadvpn.lib.billing.extension.nonPendingPurchases
+import net.mullvad.mullvadvpn.lib.billing.extension.responseCode
+import net.mullvad.mullvadvpn.lib.billing.extension.toBillingException
+import net.mullvad.mullvadvpn.lib.billing.extension.toPaymentAvailability
+import net.mullvad.mullvadvpn.lib.billing.extension.toPaymentStatus
+import net.mullvad.mullvadvpn.lib.billing.extension.toPurchaseResult
+import net.mullvad.mullvadvpn.lib.billing.model.BillingException
+import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent
+import net.mullvad.mullvadvpn.lib.payment.PaymentRepository
+import net.mullvad.mullvadvpn.lib.payment.ProductIds
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
+import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
+import net.mullvad.mullvadvpn.model.PlayPurchase
+import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult
+import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult
+
+class BillingPaymentRepository(
+ private val billingRepository: BillingRepository,
+ private val playPurchaseRepository: PlayPurchaseRepository
+) : PaymentRepository {
+
+ override fun queryPaymentAvailability(): Flow<PaymentAvailability> = flow {
+ emit(PaymentAvailability.Loading)
+ val purchases = billingRepository.queryPurchases()
+ val productIdToPaymentStatus =
+ purchases.purchasesList
+ .filter { it.products.isNotEmpty() }
+ .associate { it.products.first() to it.purchaseState.toPaymentStatus() }
+ emit(
+ billingRepository
+ .queryProducts(listOf(ProductIds.OneMonth))
+ .toPaymentAvailability(productIdToPaymentStatus)
+ )
+ }
+
+ override fun purchaseProduct(
+ productId: ProductId,
+ activityProvider: () -> Activity
+ ): Flow<PurchaseResult> = flow {
+ emit(PurchaseResult.FetchingProducts)
+
+ val productDetailsResult = billingRepository.queryProducts(listOf(productId.value))
+
+ val productDetails =
+ when (productDetailsResult.responseCode()) {
+ BillingResponseCode.OK -> {
+ productDetailsResult.getProductDetails(productId.value)
+ ?: run {
+ emit(PurchaseResult.Error.NoProductFound(productId))
+ return@flow
+ }
+ }
+ else -> {
+ emit(
+ PurchaseResult.Error.FetchProductsError(
+ productId,
+ productDetailsResult.toBillingException()
+ )
+ )
+ return@flow
+ }
+ }
+
+ // Get transaction id
+ emit(PurchaseResult.FetchingObfuscationId)
+ val obfuscatedId: String =
+ when (val result = initialisePurchase()) {
+ is PlayPurchaseInitResult.Ok -> result.obfuscatedId
+ else -> {
+ emit(PurchaseResult.Error.TransactionIdError(productId, null))
+ return@flow
+ }
+ }
+
+ val result =
+ billingRepository.startPurchaseFlow(
+ productDetails = productDetails,
+ obfuscatedId = obfuscatedId,
+ activityProvider = activityProvider
+ )
+
+ if (result.responseCode == BillingResponseCode.OK) {
+ emit(PurchaseResult.BillingFlowStarted)
+ } else {
+ emit(
+ PurchaseResult.Error.BillingError(
+ BillingException(result.responseCode, result.debugMessage)
+ )
+ )
+ return@flow
+ }
+
+ // Wait for a callback from the billing library
+ when (val event = billingRepository.purchaseEvents.firstOrNull()) {
+ is PurchaseEvent.Error -> emit(event.toPurchaseResult())
+ is PurchaseEvent.Completed -> {
+ val purchase =
+ event.purchases.firstOrNull()
+ ?: run {
+ emit(PurchaseResult.Error.BillingError(null))
+ return@flow
+ }
+ if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
+ emit(PurchaseResult.Completed.Pending)
+ } else {
+ emit(PurchaseResult.VerificationStarted)
+ if (verifyPurchase(event.purchases.first()) == PlayPurchaseVerifyResult.Ok) {
+ emit(PurchaseResult.Completed.Success)
+ } else {
+ emit(PurchaseResult.Error.VerificationError(null))
+ }
+ }
+ }
+ PurchaseEvent.UserCanceled -> emit(event.toPurchaseResult())
+ else -> emit(PurchaseResult.Error.BillingError(null))
+ }
+ }
+
+ override fun verifyPurchases(): Flow<VerificationResult> = flow {
+ emit(VerificationResult.FetchingUnfinishedPurchases)
+ val purchasesResult = billingRepository.queryPurchases()
+ when (purchasesResult.responseCode()) {
+ BillingResponseCode.OK -> {
+ val purchases = purchasesResult.nonPendingPurchases()
+ if (purchases.isNotEmpty()) {
+ emit(VerificationResult.VerificationStarted)
+ val verificationResult = verifyPurchase(purchases.first())
+ emit(
+ when (verificationResult) {
+ is PlayPurchaseVerifyResult.Error ->
+ VerificationResult.Error.VerificationError(null)
+ PlayPurchaseVerifyResult.Ok -> VerificationResult.Success
+ }
+ )
+ } else {
+ emit(VerificationResult.NothingToVerify)
+ }
+ }
+ else ->
+ emit(VerificationResult.Error.BillingError(purchasesResult.toBillingException()))
+ }
+ }
+
+ private suspend fun initialisePurchase(): PlayPurchaseInitResult {
+ return playPurchaseRepository.initializePlayPurchase()
+ }
+
+ private suspend fun verifyPurchase(purchase: Purchase): PlayPurchaseVerifyResult {
+ return playPurchaseRepository.verifyPlayPurchase(
+ PlayPurchase(
+ productId = purchase.products.first(),
+ purchaseToken = purchase.purchaseToken,
+ )
+ )
+ }
+}
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultExtensions.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultExtensions.kt
new file mode 100644
index 0000000000..3e4aee180a
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultExtensions.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.lib.billing.extension
+
+import com.android.billingclient.api.ProductDetails
+import com.android.billingclient.api.ProductDetailsResult
+import net.mullvad.mullvadvpn.lib.billing.model.BillingException
+
+fun ProductDetailsResult.getProductDetails(productId: String): ProductDetails? =
+ this.productDetailsList?.firstOrNull { it.productId == productId }
+
+fun ProductDetailsResult.responseCode(): Int = this.billingResult.responseCode
+
+fun ProductDetailsResult.toBillingException(): BillingException =
+ BillingException(responseCode = this.responseCode(), message = this.billingResult.debugMessage)
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt
new file mode 100644
index 0000000000..37cc701724
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsResultToPaymentAvailability.kt
@@ -0,0 +1,37 @@
+package net.mullvad.mullvadvpn.lib.billing.extension
+
+import com.android.billingclient.api.BillingClient
+import com.android.billingclient.api.ProductDetailsResult
+import net.mullvad.mullvadvpn.lib.billing.model.BillingException
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
+
+fun ProductDetailsResult.toPaymentAvailability(
+ productIdToPaymentStatus: Map<String, PaymentStatus?>
+) =
+ when (this.billingResult.responseCode) {
+ BillingClient.BillingResponseCode.OK -> {
+ val productDetailsList = this.productDetailsList
+ if (productDetailsList?.isNotEmpty() == true) {
+ PaymentAvailability.ProductsAvailable(
+ productDetailsList.toPaymentProducts(productIdToPaymentStatus)
+ )
+ } else {
+ PaymentAvailability.NoProductsFounds
+ }
+ }
+ BillingClient.BillingResponseCode.BILLING_UNAVAILABLE ->
+ PaymentAvailability.Error.BillingUnavailable
+ BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE ->
+ PaymentAvailability.Error.ServiceUnavailable
+ BillingClient.BillingResponseCode.DEVELOPER_ERROR ->
+ PaymentAvailability.Error.DeveloperError
+ BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED ->
+ PaymentAvailability.Error.FeatureNotSupported
+ BillingClient.BillingResponseCode.ITEM_UNAVAILABLE ->
+ PaymentAvailability.Error.ItemUnavailable
+ else ->
+ PaymentAvailability.Error.Other(
+ BillingException(this.billingResult.responseCode, this.billingResult.debugMessage)
+ )
+ }
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsToPaymentProduct.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsToPaymentProduct.kt
new file mode 100644
index 0000000000..fa9a20613f
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/ProductDetailsToPaymentProduct.kt
@@ -0,0 +1,17 @@
+package net.mullvad.mullvadvpn.lib.billing.extension
+
+import com.android.billingclient.api.ProductDetails
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.ProductPrice
+
+fun ProductDetails.toPaymentProduct(productIdToStatus: Map<String, PaymentStatus?>) =
+ PaymentProduct(
+ productId = ProductId(this.productId),
+ price = ProductPrice(this.oneTimePurchaseOfferDetails?.formattedPrice ?: ""),
+ productIdToStatus[this.productId]
+ )
+
+fun List<ProductDetails>.toPaymentProducts(productIdToStatus: Map<String, PaymentStatus?>) =
+ this.map { it.toPaymentProduct(productIdToStatus) }
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseEventToPurchaseResult.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseEventToPurchaseResult.kt
new file mode 100644
index 0000000000..e0e4bf0a77
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseEventToPurchaseResult.kt
@@ -0,0 +1,11 @@
+package net.mullvad.mullvadvpn.lib.billing.extension
+
+import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
+
+fun PurchaseEvent.toPurchaseResult() =
+ when (this) {
+ is PurchaseEvent.Error -> PurchaseResult.Error.BillingError(this.exception)
+ is PurchaseEvent.Completed -> PurchaseResult.VerificationStarted
+ PurchaseEvent.UserCanceled -> PurchaseResult.Completed.Cancelled
+ }
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseStateToPaymentStatus.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseStateToPaymentStatus.kt
new file mode 100644
index 0000000000..701e5fde3d
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchaseStateToPaymentStatus.kt
@@ -0,0 +1,11 @@
+package net.mullvad.mullvadvpn.lib.billing.extension
+
+import com.android.billingclient.api.Purchase
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
+
+internal fun Int.toPaymentStatus(): PaymentStatus? =
+ when (this) {
+ Purchase.PurchaseState.PURCHASED -> PaymentStatus.VERIFICATION_IN_PROGRESS
+ Purchase.PurchaseState.PENDING -> PaymentStatus.PENDING
+ else -> null
+ }
diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchasesResultExtensions.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchasesResultExtensions.kt
new file mode 100644
index 0000000000..d76d1a8b7e
--- /dev/null
+++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/extension/PurchasesResultExtensions.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.lib.billing.extension
+
+import com.android.billingclient.api.Purchase
+import com.android.billingclient.api.PurchasesResult
+import net.mullvad.mullvadvpn.lib.billing.model.BillingException
+
+fun PurchasesResult.nonPendingPurchases(): List<Purchase> =
+ this.purchasesList.filter { it.purchaseState != Purchase.PurchaseState.PENDING }
+
+fun PurchasesResult.responseCode(): Int = this.billingResult.responseCode
+
+fun PurchasesResult.toBillingException(): BillingException =
+ BillingException(responseCode = this.responseCode(), message = this.billingResult.debugMessage)