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 18, 2023
1 parent 12a47c4 commit 17de06c
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ 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.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -26,10 +28,15 @@ 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.button.VariantButton
import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBarAndDeviceName
import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar
import net.mullvad.mullvadvpn.compose.dialog.PaymentAvailabilityDialog
import net.mullvad.mullvadvpn.compose.dialog.PurchaseResultDialog
import net.mullvad.mullvadvpn.compose.extensions.createOpenAccountPageHook
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.lib.payment.model.PaymentStatus
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.lib.theme.Dimens
import net.mullvad.mullvadvpn.lib.theme.color.AlphaScrollbar
Expand Down Expand Up @@ -94,7 +101,10 @@ fun OutOfTimeScreen(
onRedeemVoucherClick: () -> Unit = {},
openConnectScreen: () -> Unit = {},
onSettingsClick: () -> Unit = {},
onAccountClick: () -> Unit = {}
onAccountClick: () -> Unit = {},
onPurchaseBillingProductClick: (String) -> Unit = {},
onTryFetchProductsAgain: () -> Unit = {},
onTryVerificationAgain: () -> Unit = {}
) {
val openAccountPage = LocalUriHandler.current.createOpenAccountPageHook()
LaunchedEffect(key1 = Unit) {
Expand All @@ -106,6 +116,19 @@ fun OutOfTimeScreen(
}
}
}

uiState.purchaseResult?.let {
PurchaseResultDialog(
purchaseResult = uiState.purchaseResult,
onTryAgain = onTryVerificationAgain
)
}

PaymentAvailabilityDialog(
paymentAvailability = uiState.billingPaymentState,
onTryAgain = onTryFetchProductsAgain
)

val scrollState = rememberScrollState()
ScaffoldWithTopBarAndDeviceName(
topBarColor =
Expand Down Expand Up @@ -189,6 +212,69 @@ fun OutOfTimeScreen(
)
)
}
when (uiState.billingPaymentState) {
is PaymentState.Error -> {
// 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 ->
if (product.status != PaymentStatus.AVAILABLE) {
Text(
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onBackground,
text =
stringResource(
id =
if (
product.status ==
PaymentStatus.VERIFICATION_IN_PROGRESS
) {
R.string.payment_status_verification_in_progress
} else {
R.string.payment_status_pending
}
),
modifier =
Modifier.padding(
start = Dimens.sideMargin,
end = Dimens.sideMargin,
bottom = Dimens.smallPadding
)
)
}
VariantButton(
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
),
isEnabled = product.status == PaymentStatus.AVAILABLE
)
}
}
}
if (showSitePayment) {
SitePaymentButton(
onClick = onSitePaymentClick,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package net.mullvad.mullvadvpn.compose.state

import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.model.TunnelState

data class OutOfTimeUiState(
val tunnelState: TunnelState = TunnelState.Disconnected,
val deviceName: String
val deviceName: String = "",
val billingPaymentState: PaymentState = PaymentState.Loading,
val purchaseResult: PurchaseResult? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ class OutOfTimeFragment : BaseFragment() {
onSettingsClick = ::openSettingsView,
onAccountClick = ::openAccountView,
openConnectScreen = ::advanceToConnectScreen,
onDisconnectClick = vm::onDisconnectClick
onDisconnectClick = vm::onDisconnectClick,
onPurchaseBillingProductClick = vm::startBillingPayment,
onTryFetchProductsAgain = vm::fetchPaymentAvailability,
onTryVerificationAgain = vm::verifyPurchases
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@ package net.mullvad.mullvadvpn.viewmodel

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.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.OutOfTimeUiState
import net.mullvad.mullvadvpn.compose.state.PaymentState
import net.mullvad.mullvadvpn.constant.ACCOUNT_EXPIRY_POLL_INTERVAL
import net.mullvad.mullvadvpn.lib.payment.extensions.toPurchaseResult
import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult
import net.mullvad.mullvadvpn.model.TunnelState
import net.mullvad.mullvadvpn.repository.AccountRepository
import net.mullvad.mullvadvpn.repository.DeviceRepository
Expand All @@ -27,14 +35,17 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.connectionProxy
import net.mullvad.mullvadvpn.util.callbackFlowFromNotifier
import org.joda.time.DateTime

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

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

Expand All @@ -48,21 +59,22 @@ class OutOfTimeViewModel(
}
}
.flatMapLatest { serviceConnection ->
kotlinx.coroutines.flow.combine(
combine(
serviceConnection.connectionProxy.tunnelStateFlow(),
deviceRepository.deviceState
) { tunnelState, deviceState ->
deviceRepository.deviceState,
_paymentAvailability,
_purchaseResult
) { tunnelState, deviceState, paymentAvailability, purchaseResult ->
OutOfTimeUiState(
tunnelState = tunnelState,
deviceName = deviceState.deviceName() ?: "",
billingPaymentState = paymentAvailability?.toPaymentState()
?: PaymentState.NoPayment,
purchaseResult = purchaseResult
)
}
}
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(),
OutOfTimeUiState(deviceName = "")
)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), OutOfTimeUiState())

init {
viewModelScope.launch {
Expand All @@ -82,6 +94,8 @@ class OutOfTimeViewModel(
delay(ACCOUNT_EXPIRY_POLL_INTERVAL)
}
}
verifyPurchases(updatePurchaseResult = false)
fetchPaymentAvailability()
}

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

fun startBillingPayment(productId: String) {
viewModelScope.launch {
try {
paymentRepository?.purchaseBillingProduct(productId)?.collect(_purchaseResult)
} finally {
// Update payment status in case the payment is pending or the verification failed
fetchPaymentAvailability()
}
}
}

fun verifyPurchases(updatePurchaseResult: Boolean = true) {
viewModelScope.launch {
if (updatePurchaseResult) {
paymentRepository
?.verifyPurchases()
?.map(VerificationResult::toPurchaseResult)
?.collect(_purchaseResult)
} else {
paymentRepository?.verifyPurchases()
}
}
}

fun fetchPaymentAvailability() {
viewModelScope.launch {
_paymentAvailability.emit(PaymentAvailability.Loading)
delay(100L) // So that the ui gets a new state in retries
paymentRepository?.queryPaymentAvailability()?.collect(_paymentAvailability)
?: run { _paymentAvailability.emit(PaymentAvailability.ProductsUnavailable) }
}
}

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
PaymentAvailability.Loading -> PaymentState.Loading
}

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

Expand Down

0 comments on commit 17de06c

Please sign in to comment.