diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-11-16 01:03:32 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-11-16 09:34:26 +0100 |
| commit | 9fefdc3d5a9b7f6815d67db121d73cea90edbff7 (patch) | |
| tree | 89dbbcfa7e80442bfd1f64b668e50ea742750dd1 /android | |
| parent | fc8828f91e2bce52ada92ffb96af4cb69c673368 (diff) | |
| download | mullvadvpn-9fefdc3d5a9b7f6815d67db121d73cea90edbff7.tar.xz mullvadvpn-9fefdc3d5a9b7f6815d67db121d73cea90edbff7.zip | |
Add billing payment to welcome screen and view model
Diffstat (limited to 'android')
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 |
