Skip to content

Commit

Permalink
Merge branch 'google-play-in-app-purchases-droid-277'
Browse files Browse the repository at this point in the history
  • Loading branch information
Pururun committed Nov 16, 2023
2 parents 67710f3 + c8c896b commit 04c6609
Show file tree
Hide file tree
Showing 98 changed files with 4,108 additions and 100 deletions.
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

0 comments on commit 04c6609

Please sign in to comment.