diff options
| author | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-11-16 01:05:49 +0100 |
|---|---|---|
| committer | Jonatan Rhodin <jonatan.rhodin@mullvad.net> | 2023-11-16 09:34:26 +0100 |
| commit | 8afe089bb7990328e7d38b8c3133289e36f83203 (patch) | |
| tree | f760f1d7a2550ba1015c3ae14c0d9a934716e7e4 /android/app/src | |
| parent | f95bf57f088b9e7bd5dfb958707853ed63408fd4 (diff) | |
| download | mullvadvpn-8afe089bb7990328e7d38b8c3133289e36f83203.tar.xz mullvadvpn-8afe089bb7990328e7d38b8c3133289e36f83203.zip | |
Add billing payment to out of time screen and view model
Diffstat (limited to 'android/app/src')
5 files changed, 106 insertions, 15 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt index efb07acfa2..a7fd6bae2f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.compose.screen +import android.app.Activity import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column @@ -14,8 +15,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -27,10 +33,14 @@ import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.NegativeButton import net.mullvad.mullvadvpn.compose.button.RedeemVoucherButton import net.mullvad.mullvadvpn.compose.button.SitePaymentButton +import net.mullvad.mullvadvpn.compose.component.PlayPayment import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialog +import net.mullvad.mullvadvpn.compose.dialog.payment.VerificationPendingDialog import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState +import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.theme.AppTheme import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar @@ -95,8 +105,12 @@ fun OutOfTimeScreen( onRedeemVoucherClick: () -> Unit = {}, openConnectScreen: () -> Unit = {}, onSettingsClick: () -> Unit = {}, - onAccountClick: () -> Unit = {} + onAccountClick: () -> Unit = {}, + onPurchaseBillingProductClick: (ProductId, activityProvider: () -> Activity) -> Unit = { _, _ -> + }, + onClosePurchaseResultDialog: (success: Boolean) -> Unit = {} ) { + val context = LocalContext.current val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook() LaunchedEffect(key1 = Unit) { uiSideEffect.collect { uiSideEffect -> @@ -107,6 +121,20 @@ fun OutOfTimeScreen( } } } + + 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() ScaffoldWithTopBarAndDeviceName( topBarColor = @@ -191,6 +219,22 @@ fun OutOfTimeScreen( ) ) } + uiState.billingPaymentState?.let { + PlayPayment( + billingPaymentState = uiState.billingPaymentState, + onPurchaseBillingProductClick = { productId -> + onPurchaseBillingProductClick(productId) { context as Activity } + }, + onInfoClick = { showVerificationPendingDialog = true }, + 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/OutOfTimeUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt index f7794e5a55..0491f80ea0 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt @@ -1,8 +1,11 @@ package net.mullvad.mullvadvpn.compose.state +import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData import net.mullvad.mullvadvpn.model.TunnelState data class OutOfTimeUiState( val tunnelState: TunnelState = TunnelState.Disconnected, - val deviceName: String + val deviceName: String = "", + 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 26912652db..5be527ac0c 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 @@ -133,7 +133,7 @@ val uiModule = module { viewModel { WelcomeViewModel(get(), get(), get(), get()) } viewModel { ReportProblemViewModel(get()) } viewModel { ViewLogsViewModel(get()) } - viewModel { OutOfTimeViewModel(get(), get(), get()) } + viewModel { OutOfTimeViewModel(get(), get(), get(), get()) } } const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt index 53df05c5f3..5a1ae49e1a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt @@ -36,7 +36,9 @@ class OutOfTimeFragment : BaseFragment() { onSettingsClick = ::openSettingsView, onAccountClick = ::openAccountView, openConnectScreen = ::advanceToConnectScreen, - onDisconnectClick = vm::onDisconnectClick + onDisconnectClick = vm::onDisconnectClick, + onPurchaseBillingProductClick = vm::startBillingPayment, + onClosePurchaseResultDialog = vm::onClosePurchaseResultDialog ) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt index b1df2d2225..e570f7a0fe 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt @@ -1,14 +1,15 @@ package net.mullvad.mullvadvpn.viewmodel +import android.app.Activity import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -16,6 +17,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState 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 @@ -24,14 +26,17 @@ 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.ui.serviceconnection.connectionProxy +import net.mullvad.mullvadvpn.usecase.PaymentUseCase 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) class OutOfTimeViewModel( private val accountRepository: AccountRepository, private val serviceConnectionManager: ServiceConnectionManager, private val deviceRepository: DeviceRepository, + private val paymentUseCase: PaymentUseCase, private val pollAccountExpiry: Boolean = true, ) : ViewModel() { @@ -48,21 +53,21 @@ class OutOfTimeViewModel( } } .flatMapLatest { serviceConnection -> - kotlinx.coroutines.flow.combine( + combine( serviceConnection.connectionProxy.tunnelStateFlow(), - deviceRepository.deviceState - ) { tunnelState, deviceState -> + deviceRepository.deviceState, + paymentUseCase.paymentAvailability, + paymentUseCase.purchaseResult + ) { tunnelState, deviceState, paymentAvailability, purchaseResult -> OutOfTimeUiState( tunnelState = tunnelState, deviceName = deviceState.deviceName() ?: "", + billingPaymentState = paymentAvailability?.toPaymentState(), + paymentDialogData = purchaseResult?.toPaymentDialogData() ) } } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - OutOfTimeUiState(deviceName = "") - ) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState()) init { viewModelScope.launch { @@ -78,10 +83,12 @@ class OutOfTimeViewModel( } viewModelScope.launch { while (pollAccountExpiry) { - accountRepository.fetchAccountExpiry() + updateAccountExpiry() delay(ACCOUNT_EXPIRY_POLL_INTERVAL) } } + verifyPurchases() + fetchPaymentAvailability() } private fun ConnectionProxy.tunnelStateFlow(): Flow<TunnelState> = @@ -101,6 +108,41 @@ class OutOfTimeViewModel( viewModelScope.launch { serviceConnectionManager.connectionProxy()?.disconnect() } } + fun startBillingPayment(productId: ProductId, activityProvider: () -> Activity) { + viewModelScope.launch { paymentUseCase.purchaseProduct(productId, activityProvider) } + } + + private fun verifyPurchases() { + viewModelScope.launch { + paymentUseCase.verifyPurchases() + updateAccountExpiry() + } + } + + 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 |
