diff --git a/android/lib/billing/build.gradle.kts b/android/lib/billing/build.gradle.kts index 46554cf956ad..d9b4c4dc686a 100644 --- a/android/lib/billing/build.gradle.kts +++ b/android/lib/billing/build.gradle.kts @@ -62,6 +62,9 @@ dependencies { // Management service implementation(projects.lib.daemonGrpc) + // Logger + implementation(libs.kermit) + // Test dependencies testRuntimeOnly(Dependencies.junitJupiterEngine) 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 index 540a8ad929de..39cc584a5772 100644 --- 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 @@ -2,8 +2,12 @@ package net.mullvad.mullvadvpn.lib.billing import android.app.Activity import arrow.core.Either +import arrow.core.flatMap +import arrow.core.left import arrow.core.raise.either import arrow.core.raise.ensure +import arrow.core.right +import co.touchlab.kermit.Logger import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.Purchase import kotlinx.coroutines.flow.Flow @@ -19,6 +23,7 @@ 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.model.PlayPurchase +import net.mullvad.mullvadvpn.lib.model.PlayPurchaseInitError import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken import net.mullvad.mullvadvpn.lib.payment.PaymentRepository import net.mullvad.mullvadvpn.lib.payment.ProductIds @@ -78,7 +83,7 @@ class BillingPaymentRepository( // Get transaction id emit(PurchaseResult.FetchingObfuscationId) val obfuscatedId: PlayPurchasePaymentToken = - initialisePurchase() + initializePurchase() .fold( { emit(PurchaseResult.Error.TransactionIdError(productId, null)) @@ -148,7 +153,15 @@ class BillingPaymentRepository( .bind() } - private suspend fun initialisePurchase() = playPurchaseRepository.initializePlayPurchase() + private suspend fun initializePurchase() = + playPurchaseRepository.initializePlayPurchase().flatMap { + if (it.value.isNotEmpty()) { + it.right() + } else { + Logger.e("PlayPurchasePaymentToken is empty") + PlayPurchaseInitError.OtherError.left() + } + } private suspend fun verifyPurchase(purchase: Purchase) = playPurchaseRepository.verifyPlayPurchase( 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 index 77eaea03a63f..2e9f956d072f 100644 --- 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 @@ -2,6 +2,7 @@ package net.mullvad.mullvadvpn.lib.billing import android.app.Activity import android.content.Context +import co.touchlab.kermit.Logger import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingClientStateListener @@ -124,6 +125,11 @@ class BillingRepository(context: Context) { return try { ensureConnected() + if (obfuscatedId.isEmpty()) { + Logger.e("Obfuscated id is empty") + return BillingResult.newBuilder().setResponseCode(BillingResponseCode.ERROR).build() + } + val productDetailsParamsList = listOf( BillingFlowParams.ProductDetailsParams.newBuilder() diff --git a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt index 48618feb2bef..d04c40029eda 100644 --- a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt +++ b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt @@ -183,6 +183,32 @@ class BillingPaymentRepositoryTest { } } + @Test + fun `purchaseProduct should return TransactionIdError when PlayPurchasePaymentToken is empty`() = + runTest { + // Arrange + val mockProductId = ProductId("MOCK") + val mockProductDetailsResult = mockk() + val mockProductDetails: ProductDetails = mockk() + every { mockProductDetails.productId } returns mockProductId.value + every { mockProductDetailsResult.billingResult.responseCode } returns + BillingResponseCode.OK + every { mockProductDetailsResult.productDetailsList } returns listOf(mockProductDetails) + coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns + mockProductDetailsResult + coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns + PlayPurchasePaymentToken("").right() + + // Act, Assert + paymentRepository.purchaseProduct(mockProductId, mockk()).test { + assertIs(awaitItem()) + assertIs(awaitItem()) + val result = awaitItem() + assertIs(result) + awaitComplete() + } + } + @Test fun `purchaseProduct should return BillingError on billing unavailable from startPurchaseFlow`() = runTest { diff --git a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt index 3313e2d91e93..bce573a6b6e6 100644 --- a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt +++ b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepositoryTest.kt @@ -205,6 +205,27 @@ class BillingRepositoryTest { assertEquals(mockBillingResult, result) } + @Test + fun `starting purchase flow with empty transaction id should return error`() = runTest { + // Arrange + val transactionId = "" + val mockProductDetails: ProductDetails = mockk(relaxed = true) + val mockActivityProvider: () -> Activity = mockk() + every { mockBillingClient.isReady } returns true + every { mockBillingClient.connectionState } returns BillingClient.ConnectionState.CONNECTED + + // Act + val result = + billingRepository.startPurchaseFlow( + mockProductDetails, + transactionId, + mockActivityProvider, + ) + + // Assert + assertEquals(BillingResponseCode.ERROR, result.responseCode) + } + @Test fun `when billing client query purchases returns OK query purchases should return OK`() = runTest {