Skip to content

Commit

Permalink
Add billing payment to out of time screen and view model
Browse files Browse the repository at this point in the history
  • Loading branch information
Pururun committed Oct 3, 2023
1 parent 3431d85 commit e2f5d71
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -30,8 +32,13 @@ import net.mullvad.mullvadvpn.compose.button.RedeemVoucherButton
import net.mullvad.mullvadvpn.compose.button.SitePaymentButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.dialog.PaymentBillingErrorDialog
import net.mullvad.mullvadvpn.compose.dialog.PaymentCompletedDialog
import net.mullvad.mullvadvpn.compose.dialog.PaymentVerificationErrorDialog
import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook
import net.mullvad.mullvadvpn.compose.state.OutOfTimeDialogState
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.lib.theme.AlphaTopBar
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
Expand Down Expand Up @@ -93,7 +100,11 @@ fun OutOfTimeScreen(
onRedeemVoucherClick: () -> Unit = {},
openConnectScreen: () -> Unit = {},
onSettingsClick: () -> Unit = {},
onAccountClick: () -> Unit = {}
onAccountClick: () -> Unit = {},
onPurchaseBillingProductClick: (String) -> Unit = {},
onDialogClose: () -> Unit = {},
onTryFetchProductsAgain: () -> Unit = {},
onTryVerificationAgain: () -> Unit = {}
) {
val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
LaunchedEffect(key1 = Unit) {
Expand All @@ -105,6 +116,34 @@ fun OutOfTimeScreen(
}
}
}

when (uiState.dialogState) {
OutOfTimeDialogState.NoDialog -> {
// Show nothing
}
OutOfTimeDialogState.PurchaseComplete -> {
PaymentCompletedDialog(onClose = onDialogClose)
}
OutOfTimeDialogState.BillingError -> {
PaymentBillingErrorDialog(
onTryAgain = {
onDialogClose()
onTryFetchProductsAgain()
},
onClose = onDialogClose
)
}
OutOfTimeDialogState.VerificationError -> {
PaymentVerificationErrorDialog(
onTryAgain = {
onDialogClose()
onTryVerificationAgain()
},
onClose = onDialogClose
)
}
}

val scrollState = rememberScrollState()
ScaffoldWithTopBar(
topBarColor =
Expand Down Expand Up @@ -190,6 +229,50 @@ fun OutOfTimeScreen(
)
)
}
when (uiState.billingPaymentState) {
PaymentState.BillingError,
PaymentState.GenericError -> {
// We show some kind of dialog error at the top
}
PaymentState.Loading -> {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onBackground,
modifier =
Modifier.padding(
start = Dimens.sideMargin,
end = Dimens.sideMargin,
bottom = Dimens.screenVerticalMargin
)
.size(
width = Dimens.progressIndicatorSize,
height = Dimens.progressIndicatorSize
)
.align(Alignment.CenterHorizontally)
)
}
PaymentState.NoPayment -> {
// Show nothing
}
is PaymentState.PaymentAvailable -> {
uiState.billingPaymentState.products.forEach { product ->
ActionButton(
text = stringResource(id = R.string.add_30_days_time_x, product.price),
onClick = { onPurchaseBillingProductClick(product.productId) },
modifier =
Modifier.padding(
start = Dimens.sideMargin,
end = Dimens.sideMargin,
bottom = Dimens.screenVerticalMargin
),
colors =
ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.surface
)
)
}
}
}
if (showSitePayment) {
SitePaymentButton(
onClick = onSitePaymentClick,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.mullvad.mullvadvpn.compose.state

sealed interface OutOfTimeDialogState {
data object NoDialog: OutOfTimeDialogState

data object VerificationError: OutOfTimeDialogState

data object BillingError: OutOfTimeDialogState

data object PurchaseComplete: OutOfTimeDialogState
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@ package net.mullvad.mullvadvpn.compose.state

import net.mullvad.mullvadvpn.model.TunnelState

data class OutOfTimeUiState(val tunnelState: TunnelState = TunnelState.Disconnected)
data class OutOfTimeUiState(
val tunnelState: TunnelState = TunnelState.Disconnected,
val billingPaymentState: PaymentState = PaymentState.Loading,
val dialogState: OutOfTimeDialogState = OutOfTimeDialogState.NoDialog
)
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ val uiModule = module {
viewModel { SettingsViewModel(get(), get()) }
viewModel { VpnSettingsViewModel(get(), get(), get(), get()) }
viewModel { WelcomeViewModel(get(), get(), get(), get()) }
viewModel { OutOfTimeViewModel(get(), get()) }
viewModel { OutOfTimeViewModel(get(), get(), get()) }
}

const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ class OutOfTimeFragment : BaseFragment() {
onSettingsClick = ::openSettingsView,
onAccountClick = ::openAccountView,
openConnectScreen = ::advanceToConnectScreen,
onDisconnectClick = vm::onDisconnectClick
onDisconnectClick = vm::onDisconnectClick,
onPurchaseBillingProductClick = vm::startBillingPayment,
onDialogClose = vm::closeDialog,
onTryFetchProductsAgain = vm::fetchPaymentAvailability,
onTryVerificationAgain = vm::verifyPurchases
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
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
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.PaymentProvider
import net.mullvad.mullvadvpn.compose.state.OutOfTimeDialogState
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.repository.AccountRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ConnectionProxy
Expand All @@ -25,15 +31,20 @@ 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.util.callbackFlowFromNotifier
import net.mullvad.mullvadvpn.util.combine
import org.joda.time.DateTime

@OptIn(FlowPreview::class)
class OutOfTimeViewModel(
private val accountRepository: AccountRepository,
private val serviceConnectionManager: ServiceConnectionManager,
paymentProvider: PaymentProvider,
private val pollAccountExpiry: Boolean = true
) : ViewModel() {
private val paymentRepository = paymentProvider.paymentRepository

private val _dialogState = MutableStateFlow<OutOfTimeDialogState>(OutOfTimeDialogState.NoDialog)
private val _paymentAvailability = MutableStateFlow<PaymentAvailability?>(null)
private val _uiSideEffect = MutableSharedFlow<UiSideEffect>(extraBufferCapacity = 1)
val uiSideEffect = _uiSideEffect.asSharedFlow()

Expand All @@ -47,9 +58,19 @@ class OutOfTimeViewModel(
}
}
.flatMapLatest { serviceConnection ->
serviceConnection.connectionProxy.tunnelStateFlow()
combine(
serviceConnection.connectionProxy.tunnelStateFlow(),
_paymentAvailability,
_dialogState
) { tunnelState, paymentAvailability, dialogState ->
OutOfTimeUiState(
tunnelState = tunnelState,
billingPaymentState =
paymentAvailability?.toPaymentState() ?: PaymentState.NoPayment,
dialogState = dialogState
)
}
}
.map { tunnelState -> OutOfTimeUiState(tunnelState = tunnelState) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState())

init {
Expand All @@ -70,6 +91,28 @@ class OutOfTimeViewModel(
delay(ACCOUNT_EXPIRY_POLL_INTERVAL)
}
}
viewModelScope.launch {
paymentRepository?.purchaseResult?.collectLatest { result ->
when (result) {
PurchaseResult.PurchaseCancelled -> {
// Do nothing
}
PurchaseResult.PurchaseCompleted -> {
// Show completed dialog
_dialogState.tryEmit(OutOfTimeDialogState.PurchaseComplete)
}
PurchaseResult.PurchaseError -> {
// Do nothing, errors that we get from here should be shown by google
}
PurchaseResult.VerificationError -> {
// Show verification error
_dialogState.tryEmit(OutOfTimeDialogState.VerificationError)
}
}
}
}
verifyPurchases()
fetchPaymentAvailability()
}

private fun ConnectionProxy.tunnelStateFlow(): Flow<TunnelState> =
Expand All @@ -89,6 +132,42 @@ class OutOfTimeViewModel(
viewModelScope.launch { serviceConnectionManager.connectionProxy()?.disconnect() }
}

fun startBillingPayment(productId: String) {
viewModelScope.launch { paymentRepository?.purchaseBillingProduct(productId) }
}

fun closeDialog() {
viewModelScope.launch { _dialogState.tryEmit(OutOfTimeDialogState.NoDialog) }
}

fun verifyPurchases() {
viewModelScope.launch { paymentRepository?.verifyPurchases() }
}

fun fetchPaymentAvailability() {
viewModelScope.launch {
val result =
paymentRepository?.queryPaymentAvailability()
?: PaymentAvailability.ProductsUnavailable
_paymentAvailability.tryEmit(result)
if (
result is PaymentAvailability.Error.BillingUnavailable ||
result is PaymentAvailability.Error.ServiceUnavailable
) {
_dialogState.tryEmit(OutOfTimeDialogState.BillingError)
}
}
}

private fun PaymentAvailability.toPaymentState(): PaymentState =
when (this) {
PaymentAvailability.Error.ServiceUnavailable,
PaymentAvailability.Error.BillingUnavailable -> PaymentState.Error.BillingError
is PaymentAvailability.Error.Other -> PaymentState.Error.GenericError
is PaymentAvailability.ProductsAvailable -> PaymentState.PaymentAvailable(products)
PaymentAvailability.ProductsUnavailable -> PaymentState.NoPayment
}

sealed interface UiSideEffect {
data class OpenAccountView(val token: String) : UiSideEffect

Expand Down

0 comments on commit e2f5d71

Please sign in to comment.