summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 01:03:32 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 09:34:26 +0100
commit9fefdc3d5a9b7f6815d67db121d73cea90edbff7 (patch)
tree89dbbcfa7e80442bfd1f64b668e50ea742750dd1
parentfc8828f91e2bce52ada92ffb96af4cb69c673368 (diff)
downloadmullvadvpn-9fefdc3d5a9b7f6815d67db121d73cea90edbff7.tar.xz
mullvadvpn-9fefdc3d5a9b7f6815d67db121d73cea90edbff7.zip
Add billing payment to welcome screen and view model
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt74
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt57
5 files changed, 129 insertions, 13 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
index f3c9f9dc7e..d26e8c8265 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/WelcomeScreen.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.compose.screen
+import android.app.Activity
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -36,13 +37,20 @@ import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.RedeemVoucherButton
import net.mullvad.mullvadvpn.compose.button.SitePaymentButton
import net.mullvad.mullvadvpn.compose.component.CopyAnimatedIconButton
+import net.mullvad.mullvadvpn.compose.component.PlayPayment
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.dialog.DeviceNameInfoDialog
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialog
+import net.mullvad.mullvadvpn.compose.dialog.payment.VerificationPendingDialog
+import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.compose.util.createCopyToClipboardHandle
import net.mullvad.mullvadvpn.lib.common.util.groupWithSpaces
import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.ProductPrice
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
@@ -56,13 +64,26 @@ private fun PreviewWelcomeScreen() {
AppTheme {
WelcomeScreen(
showSitePayment = true,
- uiState = WelcomeUiState(accountNumber = "4444555566667777", deviceName = "Happy Mole"),
+ uiState =
+ WelcomeUiState(
+ accountNumber = "4444555566667777",
+ deviceName = "Happy Mole",
+ billingPaymentState =
+ PaymentState.PaymentAvailable(
+ products =
+ listOf(
+ PaymentProduct(ProductId("product"), ProductPrice("$44"), null)
+ )
+ )
+ ),
uiSideEffect = MutableSharedFlow<WelcomeViewModel.UiSideEffect>().asSharedFlow(),
onSitePaymentClick = {},
onRedeemVoucherClick = {},
onSettingsClick = {},
onAccountClick = {},
- openConnectScreen = {}
+ openConnectScreen = {},
+ onPurchaseBillingProductClick = { _, _ -> },
+ onClosePurchaseResultDialog = {}
)
}
}
@@ -76,7 +97,9 @@ fun WelcomeScreen(
onRedeemVoucherClick: () -> Unit,
onSettingsClick: () -> Unit,
onAccountClick: () -> Unit,
- openConnectScreen: () -> Unit
+ openConnectScreen: () -> Unit,
+ onPurchaseBillingProductClick: (productId: ProductId, activityProvider: () -> Activity) -> Unit,
+ onClosePurchaseResultDialog: (success: Boolean) -> Unit
) {
val context = LocalContext.current
LaunchedEffect(Unit) {
@@ -88,6 +111,20 @@ fun WelcomeScreen(
}
}
}
+
+ var showVerificationPendingDialog by remember { mutableStateOf(false) }
+ if (showVerificationPendingDialog) {
+ VerificationPendingDialog(onClose = { showVerificationPendingDialog = false })
+ }
+
+ uiState.paymentDialogData?.let {
+ PaymentDialog(
+ paymentDialogData = uiState.paymentDialogData,
+ retryPurchase = { onPurchaseBillingProductClick(it) { context as Activity } },
+ onCloseDialog = onClosePurchaseResultDialog
+ )
+ }
+
val scrollState = rememberScrollState()
val snackbarHostState = remember { SnackbarHostState() }
@@ -133,7 +170,14 @@ fun WelcomeScreen(
Spacer(modifier = Modifier.weight(1f))
// Payment button area
- PaymentPanel(showSitePayment, onSitePaymentClick, onRedeemVoucherClick)
+ PaymentPanel(
+ showSitePayment = showSitePayment,
+ billingPaymentState = uiState.billingPaymentState,
+ onSitePaymentClick = onSitePaymentClick,
+ onRedeemVoucherClick = onRedeemVoucherClick,
+ onPurchaseBillingProductClick = onPurchaseBillingProductClick,
+ onPaymentInfoClick = { showVerificationPendingDialog = true }
+ )
}
}
}
@@ -264,9 +308,13 @@ fun DeviceNameRow(deviceName: String?) {
@Composable
private fun PaymentPanel(
showSitePayment: Boolean,
+ billingPaymentState: PaymentState?,
onSitePaymentClick: () -> Unit,
- onRedeemVoucherClick: () -> Unit
+ onRedeemVoucherClick: () -> Unit,
+ onPurchaseBillingProductClick: (productId: ProductId, activityProvider: () -> Activity) -> Unit,
+ onPaymentInfoClick: () -> Unit
) {
+ val context = LocalContext.current
Column(
modifier =
Modifier.fillMaxWidth()
@@ -274,6 +322,22 @@ private fun PaymentPanel(
.background(color = MaterialTheme.colorScheme.background)
) {
Spacer(modifier = Modifier.padding(top = Dimens.screenVerticalMargin))
+ billingPaymentState?.let {
+ PlayPayment(
+ billingPaymentState = billingPaymentState,
+ onPurchaseBillingProductClick = { productId ->
+ onPurchaseBillingProductClick(productId) { context as Activity }
+ },
+ onInfoClick = onPaymentInfoClick,
+ modifier =
+ Modifier.padding(
+ start = Dimens.sideMargin,
+ end = Dimens.sideMargin,
+ bottom = Dimens.screenVerticalMargin
+ )
+ .align(Alignment.CenterHorizontally)
+ )
+ }
if (showSitePayment) {
SitePaymentButton(
onClick = onSitePaymentClick,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt
index c6959f23e0..bd1c19e9c9 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/WelcomeUiState.kt
@@ -1,9 +1,12 @@
package net.mullvad.mullvadvpn.compose.state
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData
import net.mullvad.mullvadvpn.model.TunnelState
data class WelcomeUiState(
val tunnelState: TunnelState = TunnelState.Disconnected,
val accountNumber: String? = null,
- val deviceName: String? = null
+ val deviceName: String? = null,
+ val billingPaymentState: PaymentState? = null,
+ val paymentDialogData: PaymentDialogData? = null
)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index 6ed744aef9..26912652db 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -130,7 +130,7 @@ val uiModule = module {
viewModel { SettingsViewModel(get(), get()) }
viewModel { VoucherDialogViewModel(get(), get()) }
viewModel { VpnSettingsViewModel(get(), get(), get(), get(), get()) }
- viewModel { WelcomeViewModel(get(), get(), get()) }
+ viewModel { WelcomeViewModel(get(), get(), get(), get()) }
viewModel { ReportProblemViewModel(get()) }
viewModel { ViewLogsViewModel(get()) }
viewModel { OutOfTimeViewModel(get(), get(), get()) }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt
index d04c5de53a..5c5e0c83f8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/WelcomeFragment.kt
@@ -35,7 +35,9 @@ class WelcomeFragment : BaseFragment() {
onRedeemVoucherClick = ::openRedeemVoucherFragment,
onSettingsClick = ::openSettingsView,
onAccountClick = ::openAccountView,
- openConnectScreen = ::advanceToConnectScreen
+ openConnectScreen = ::advanceToConnectScreen,
+ onPurchaseBillingProductClick = vm::startBillingPayment,
+ onClosePurchaseResultDialog = vm::onClosePurchaseResultDialog
)
}
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
index 6c9b2ea75d..b02a1599a4 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/WelcomeViewModel.kt
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.viewmodel
+import android.app.Activity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.FlowPreview
@@ -18,6 +19,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.state.WelcomeUiState
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.repository.AccountRepository
import net.mullvad.mullvadvpn.repository.DeviceRepository
@@ -25,9 +27,12 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionState
import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.usecase.PaymentUseCase
import net.mullvad.mullvadvpn.util.UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS
import net.mullvad.mullvadvpn.util.addDebounceForUnknownState
import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
+import net.mullvad.mullvadvpn.util.toPaymentDialogData
+import net.mullvad.mullvadvpn.util.toPaymentState
import org.joda.time.DateTime
@OptIn(FlowPreview::class)
@@ -35,9 +40,9 @@ class WelcomeViewModel(
private val accountRepository: AccountRepository,
private val deviceRepository: DeviceRepository,
private val serviceConnectionManager: ServiceConnectionManager,
+ private val paymentUseCase: PaymentUseCase,
private val pollAccountExpiry: Boolean = true
) : ViewModel() {
-
private val _uiSideEffect = MutableSharedFlow<UiSideEffect>(extraBufferCapacity = 1)
val uiSideEffect = _uiSideEffect.asSharedFlow()
@@ -55,12 +60,16 @@ class WelcomeViewModel(
serviceConnection.connectionProxy.tunnelUiStateFlow(),
deviceRepository.deviceState.debounce {
it.addDebounceForUnknownState(UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS)
- }
- ) { tunnelState, deviceState ->
+ },
+ paymentUseCase.paymentAvailability,
+ paymentUseCase.purchaseResult
+ ) { tunnelState, deviceState, paymentAvailability, purchaseResult ->
WelcomeUiState(
tunnelState = tunnelState,
accountNumber = deviceState.token(),
- deviceName = deviceState.deviceName()
+ deviceName = deviceState.deviceName(),
+ billingPaymentState = paymentAvailability?.toPaymentState(),
+ paymentDialogData = purchaseResult?.toPaymentDialogData()
)
}
}
@@ -80,10 +89,12 @@ class WelcomeViewModel(
}
viewModelScope.launch {
while (pollAccountExpiry) {
- accountRepository.fetchAccountExpiry()
+ updateAccountExpiry()
delay(ACCOUNT_EXPIRY_POLL_INTERVAL)
}
}
+ verifyPurchases()
+ fetchPaymentAvailability()
}
private fun ConnectionProxy.tunnelUiStateFlow(): Flow<TunnelState> =
@@ -99,6 +110,42 @@ class WelcomeViewModel(
}
}
+ fun startBillingPayment(productId: ProductId, activityProvider: () -> Activity) {
+ viewModelScope.launch { paymentUseCase.purchaseProduct(productId, activityProvider) }
+ }
+
+ private fun verifyPurchases() {
+ viewModelScope.launch {
+ paymentUseCase.verifyPurchases()
+ updateAccountExpiry()
+ }
+ }
+
+ @OptIn(FlowPreview::class)
+ private fun fetchPaymentAvailability() {
+ viewModelScope.launch { paymentUseCase.queryPaymentAvailability() }
+ }
+
+ fun onClosePurchaseResultDialog(success: Boolean) {
+ // We are closing the dialog without any action, this can happen either if an error occurred
+ // during the purchase or the purchase ended successfully.
+ // In those cases we want to update the both the payment availability and the account
+ // expiry.
+ if (success) {
+ updateAccountExpiry()
+ _uiSideEffect.tryEmit(UiSideEffect.OpenConnectScreen)
+ } else {
+ fetchPaymentAvailability()
+ }
+ viewModelScope.launch {
+ paymentUseCase.resetPurchaseResult() // So that we do not show the dialog again.
+ }
+ }
+
+ private fun updateAccountExpiry() {
+ accountRepository.fetchAccountExpiry()
+ }
+
sealed interface UiSideEffect {
data class OpenAccountView(val token: String) : UiSideEffect