Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Google Play in-app purchases #5125

Merged
merged 22 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cdd0b8e
Add billing client library and billing module
Pururun Nov 15, 2023
c3e17fd
Add BillingRepository that handles calls to billing client
Pururun Nov 15, 2023
4d70ef4
Add tests for BillingRepository
Pururun Nov 15, 2023
5c5c2a9
Add play purchasing android ipc calls
Pururun Nov 15, 2023
ae7471c
Add payment module and billing payment repository
Pururun Nov 15, 2023
81beb5e
Add unit tests for BillingPaymentRepository
Pururun Nov 15, 2023
2a0c1b1
Add PaymentUseCase
Pururun Nov 15, 2023
868e35c
Add unit tests for PlayPaymentUseCase
Pururun Nov 15, 2023
91a030a
Add payment support to AccountViewModel
Pururun Nov 16, 2023
06a950f
Add payment unit tests for AccountViewModel
Pururun Nov 16, 2023
52e2705
Add payment support to account screen
Pururun Nov 16, 2023
fc8828f
Add ui tests for account screen
Pururun Nov 16, 2023
9fefdc3
Add billing payment to welcome screen and view model
Pururun Nov 16, 2023
3455fda
Add payment unit tests for welcome view model
Pururun Nov 16, 2023
f95bf57
Add payment ui tests to Welcome Screen
Pururun Nov 16, 2023
8afe089
Add billing payment to out of time screen and view model
Pururun Nov 16, 2023
5aac263
Add payment unit tests for OutOfTimeViewModel
Pururun Nov 16, 2023
dc3f2b3
Add payment ui tests to OutOfTimeScreen
Pururun Nov 16, 2023
3bf12a6
Update gradle lockfile
Pururun Nov 16, 2023
382111e
Update changelog
Pururun Nov 16, 2023
b9c41d5
Add translations and update messages.pot
Pururun Nov 16, 2023
c8c896b
Fix kotlin formatting
albin-mullvad Nov 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Line wrap the file at 100 chars. Th
- Migrate voucher dialog to compose.
- Add "New Device" in app notification & rework notification system
- Add support for setting per-app language in system settings.
- Add support for in app purchases for versions that are released on Google Play.

#### Linux
- Don't block forwarding of traffic when the split tunnel mark (ct mark) is set.
Expand Down
4 changes: 4 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ dependencies {
implementation(project(Dependencies.Mullvad.resourceLib))
implementation(project(Dependencies.Mullvad.talpidLib))
implementation(project(Dependencies.Mullvad.themeLib))
implementation(project(Dependencies.Mullvad.paymentLib))

// Play implementation
playImplementation(project(Dependencies.Mullvad.billingLib))

implementation(Dependencies.androidMaterial)
implementation(Dependencies.commonsValidator)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
package net.mullvad.mullvadvpn.compose.screen

import android.app.Activity
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import net.mullvad.mullvadvpn.compose.setContentWithTheme
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.test.PLAY_PAYMENT_INFO_ICON_TEST_TAG
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
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.util.toPaymentDialogData
import net.mullvad.mullvadvpn.viewmodel.AccountUiState
import net.mullvad.mullvadvpn.viewmodel.AccountViewModel
import org.junit.Before
import org.junit.Rule
import org.junit.Test

@OptIn(ExperimentalMaterial3Api::class)
class AccountScreenTest {
@get:Rule val composeTestRule = createComposeRule()

Expand All @@ -24,12 +36,12 @@ class AccountScreenTest {
MockKAnnotations.init(this)
}

@OptIn(ExperimentalMaterial3Api::class)
@Test
fun testDefaultState() {
// Arrange
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState(
deviceName = DUMMY_DEVICE_NAME,
Expand All @@ -48,13 +60,13 @@ class AccountScreenTest {
}
}

@OptIn(ExperimentalMaterial3Api::class)
@Test
fun testManageAccountClick() {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState(
deviceName = DUMMY_DEVICE_NAME,
Expand All @@ -74,13 +86,13 @@ class AccountScreenTest {
verify { mockedClickHandler.invoke() }
}

@OptIn(ExperimentalMaterial3Api::class)
@Test
fun testRedeemVoucherClick() {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState(
deviceName = DUMMY_DEVICE_NAME,
Expand All @@ -100,13 +112,13 @@ class AccountScreenTest {
verify { mockedClickHandler.invoke() }
}

@OptIn(ExperimentalMaterial3Api::class)
@Test
fun testLogoutClick() {
// Arrange
val mockedClickHandler: () -> Unit = mockk(relaxed = true)
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState(
deviceName = DUMMY_DEVICE_NAME,
Expand All @@ -126,6 +138,220 @@ class AccountScreenTest {
verify { mockedClickHandler.invoke() }
}

@Test
fun testShowPurchaseCompleteDialog() {
// Arrange
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
paymentDialogData =
PurchaseResult.Completed.Success.toPaymentDialogData()
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Time was successfully added").assertExists()
}

@Test
fun testShowVerificationErrorDialog() {
// Arrange
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
paymentDialogData =
PurchaseResult.Error.VerificationError(null).toPaymentDialogData()
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Verifying purchase").assertExists()
}

@Test
fun testShowFetchProductsErrorDialog() {
// Arrange
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
paymentDialogData =
PurchaseResult.Error.FetchProductsError(ProductId(""), null)
.toPaymentDialogData()
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Google Play unavailable").assertExists()
}

@Test
fun testShowBillingErrorPaymentButton() {
// Arrange
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default().copy(billingPaymentState = PaymentState.Error.Billing),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Add 30 days time").assertExists()
}

@Test
fun testShowBillingPaymentAvailable() {
// Arrange
val mockPaymentProduct: PaymentProduct = mockk()
every { mockPaymentProduct.price } returns ProductPrice("$10")
every { mockPaymentProduct.status } returns null
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
billingPaymentState =
PaymentState.PaymentAvailable(listOf(mockPaymentProduct))
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Add 30 days time ($10)").assertExists()
}

@Test
fun testShowPendingPayment() {
// Arrange
val mockPaymentProduct: PaymentProduct = mockk()
every { mockPaymentProduct.price } returns ProductPrice("$10")
every { mockPaymentProduct.status } returns PaymentStatus.PENDING
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
billingPaymentState =
PaymentState.PaymentAvailable(listOf(mockPaymentProduct))
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Google Play payment pending").assertExists()
}

@Test
fun testShowPendingPaymentInfoDialog() {
// Arrange
val mockPaymentProduct: PaymentProduct = mockk()
every { mockPaymentProduct.price } returns ProductPrice("$10")
every { mockPaymentProduct.status } returns PaymentStatus.PENDING
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
billingPaymentState =
PaymentState.PaymentAvailable(listOf(mockPaymentProduct))
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Act
composeTestRule.onNodeWithTag(PLAY_PAYMENT_INFO_ICON_TEST_TAG).performClick()

// Assert
composeTestRule
.onNodeWithText(
"We are currently verifying your purchase, this might take some time. Your time will be added if the verification is successful."
)
.assertExists()
}

@Test
fun testShowVerificationInProgress() {
// Arrange
val mockPaymentProduct: PaymentProduct = mockk()
every { mockPaymentProduct.price } returns ProductPrice("$10")
every { mockPaymentProduct.status } returns PaymentStatus.VERIFICATION_IN_PROGRESS
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
billingPaymentState =
PaymentState.PaymentAvailable(listOf(mockPaymentProduct))
),
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Assert
composeTestRule.onNodeWithText("Verifying purchase").assertExists()
}

@Test
fun testOnPurchaseBillingProductClick() {
// Arrange
val clickHandler: (ProductId, () -> Activity) -> Unit = mockk(relaxed = true)
val mockPaymentProduct: PaymentProduct = mockk()
every { mockPaymentProduct.price } returns ProductPrice("$10")
every { mockPaymentProduct.productId } returns ProductId("PRODUCT_ID")
every { mockPaymentProduct.status } returns null
composeTestRule.setContentWithTheme {
AccountScreen(
showSitePayment = true,
uiState =
AccountUiState.default()
.copy(
billingPaymentState =
PaymentState.PaymentAvailable(listOf(mockPaymentProduct))
),
onPurchaseBillingProductClick = clickHandler,
uiSideEffect = MutableSharedFlow<AccountViewModel.UiSideEffect>().asSharedFlow(),
enterTransitionEndAction = MutableSharedFlow<Unit>().asSharedFlow()
)
}

// Act
composeTestRule.onNodeWithText("Add 30 days time ($10)").performClick()

// Assert
verify { clickHandler.invoke(ProductId("PRODUCT_ID"), any()) }
}

companion object {
private const val DUMMY_DEVICE_NAME = "fake_name"
private const val DUMMY_ACCOUNT_NUMBER = "fake_number"
Expand Down
Loading
Loading