summaryrefslogtreecommitdiffhomepage
path: root/android/lib
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
parent5c5c2a95d676648ffbd953b5f9e8587a8a80bf66 (diff)
downloadmullvadvpn-ae7471c50a653133aae6472199d7b0d16ad2a145.tar.xz
mullvadvpn-ae7471c50a653133aae6472199d7b0d16ad2a145.zip
Add payment module and billing payment repository
Diffstat (limited to 'android/lib')
-rw-r--r--android/lib/billing/build.gradle.kts3
-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
-rw-r--r--android/lib/payment/build.gradle.kts44
-rw-r--r--android/lib/payment/src/main/AndroidManifest.xml2
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt20
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/ProductIds.kt5
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt26
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentProduct.kt7
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentStatus.kt6
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductId.kt4
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductPrice.kt4
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PurchaseResult.kt34
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt19
19 files changed, 443 insertions, 0 deletions
diff --git a/android/lib/billing/build.gradle.kts b/android/lib/billing/build.gradle.kts
index a010534085..255459f453 100644
--- a/android/lib/billing/build.gradle.kts
+++ b/android/lib/billing/build.gradle.kts
@@ -54,6 +54,9 @@ dependencies {
//IPC
implementation(project(Dependencies.Mullvad.ipcLib))
+ //Payment library
+ implementation(project(Dependencies.Mullvad.paymentLib))
+
// Test dependencies
testImplementation(project(Dependencies.Mullvad.commonTestLib))
testImplementation(Dependencies.Kotlin.test)
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)
diff --git a/android/lib/payment/build.gradle.kts b/android/lib/payment/build.gradle.kts
new file mode 100644
index 0000000000..23f945b4f9
--- /dev/null
+++ b/android/lib/payment/build.gradle.kts
@@ -0,0 +1,44 @@
+plugins {
+ id(Dependencies.Plugin.androidLibraryId)
+ id(Dependencies.Plugin.kotlinAndroidId)
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.lib.payment"
+ compileSdk = Versions.Android.compileSdkVersion
+
+ defaultConfig {
+ minSdk = Versions.Android.minSdkVersion
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = Versions.jvmTarget
+ }
+
+ lint {
+ lintConfig = file("${rootProject.projectDir}/config/lint.xml")
+ abortOnError = true
+ warningsAsErrors = true
+ }
+
+ packaging {
+ resources {
+ pickFirsts += setOf(
+ // Fixes packaging error caused by: jetified-junit-*
+ "META-INF/LICENSE.md",
+ "META-INF/LICENSE-notice.md"
+ )
+ }
+ }
+}
+
+dependencies {
+ implementation(Dependencies.Kotlin.stdlib)
+ implementation(Dependencies.KotlinX.coroutinesAndroid)
+}
diff --git a/android/lib/payment/src/main/AndroidManifest.xml b/android/lib/payment/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..b2d3ea1235
--- /dev/null
+++ b/android/lib/payment/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt
new file mode 100644
index 0000000000..73fd0c061d
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt
@@ -0,0 +1,20 @@
+package net.mullvad.mullvadvpn.lib.payment
+
+import android.app.Activity
+import kotlinx.coroutines.flow.Flow
+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
+
+interface PaymentRepository {
+
+ fun purchaseProduct(
+ productId: ProductId,
+ activityProvider: () -> Activity
+ ): Flow<PurchaseResult>
+
+ fun verifyPurchases(): Flow<VerificationResult>
+
+ fun queryPaymentAvailability(): Flow<PaymentAvailability>
+}
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/ProductIds.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/ProductIds.kt
new file mode 100644
index 0000000000..8754968891
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/ProductIds.kt
@@ -0,0 +1,5 @@
+package net.mullvad.mullvadvpn.lib.payment
+
+object ProductIds {
+ const val OneMonth = "one_month"
+}
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt
new file mode 100644
index 0000000000..cee2800677
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentAvailability.kt
@@ -0,0 +1,26 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+sealed interface PaymentAvailability {
+ data object Loading : PaymentAvailability
+
+ data class ProductsAvailable(val products: List<PaymentProduct>) : PaymentAvailability
+
+ data object ProductsUnavailable : PaymentAvailability
+
+ data object NoProductsFounds : PaymentAvailability
+
+ sealed interface Error: PaymentAvailability {
+ data object BillingUnavailable : Error
+
+ data object ServiceUnavailable : Error
+
+ data object FeatureNotSupported : Error
+
+ data object DeveloperError : Error
+
+ data object ItemUnavailable : Error
+
+ data class Other(val exception: Throwable) :
+ Error
+ }
+}
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentProduct.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentProduct.kt
new file mode 100644
index 0000000000..8945453d37
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentProduct.kt
@@ -0,0 +1,7 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+data class PaymentProduct(
+ val productId: ProductId,
+ val price: ProductPrice,
+ val status: PaymentStatus?
+)
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentStatus.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentStatus.kt
new file mode 100644
index 0000000000..37574249a6
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PaymentStatus.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+enum class PaymentStatus {
+ PENDING,
+ VERIFICATION_IN_PROGRESS
+}
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductId.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductId.kt
new file mode 100644
index 0000000000..49a367f7c2
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductId.kt
@@ -0,0 +1,4 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+@JvmInline
+value class ProductId(val value: String)
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductPrice.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductPrice.kt
new file mode 100644
index 0000000000..4939eac3a5
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/ProductPrice.kt
@@ -0,0 +1,4 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+@JvmInline
+value class ProductPrice(val value: String)
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PurchaseResult.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PurchaseResult.kt
new file mode 100644
index 0000000000..f5b89bffe6
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/PurchaseResult.kt
@@ -0,0 +1,34 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+sealed interface PurchaseResult {
+ data object FetchingProducts : PurchaseResult
+
+ data object FetchingObfuscationId : PurchaseResult
+
+ data object BillingFlowStarted : PurchaseResult
+
+ data object VerificationStarted : PurchaseResult
+
+ sealed interface Completed : PurchaseResult {
+ data object Success : Completed
+
+ data object Cancelled : Completed
+
+ // This ends our part of the purchase flow. The rest is handled by Google and the api.
+ data object Pending : Completed
+ }
+
+ sealed interface Error : PurchaseResult {
+ data class NoProductFound(val productId: ProductId) : Error
+
+ data class FetchProductsError(val productId: ProductId, val exception: Throwable?) : Error
+
+ data class TransactionIdError(val productId: ProductId, val exception: Throwable?) : Error
+
+ data class BillingError(val exception: Throwable?) : Error
+
+ data class VerificationError(val exception: Throwable?) : Error
+ }
+
+ fun isTerminatingState(): Boolean = this is Completed || this is Error
+}
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt
new file mode 100644
index 0000000000..725ea0af68
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/model/VerificationResult.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.lib.payment.model
+
+sealed interface VerificationResult {
+ data object FetchingUnfinishedPurchases : VerificationResult
+
+ data object VerificationStarted : VerificationResult
+
+ // No verification was needed as there is no purchases to verify
+ data object NothingToVerify : VerificationResult
+
+ data object Success : VerificationResult
+
+ // Generic error, add more cases as needed
+ sealed interface Error : VerificationResult {
+ data class BillingError(val exception: Throwable?) : Error
+
+ data class VerificationError(val exception: Throwable?) : Error
+ }
+}