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 19, 2023
1 parent 48f1fb5 commit 064c12c
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.button.NegativeButton
import net.mullvad.mullvadvpn.compose.button.PlayPaymentButton
import net.mullvad.mullvadvpn.compose.button.RedeemVoucherButton
import net.mullvad.mullvadvpn.compose.button.SitePaymentButton
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.lib.theme.AppTheme
Expand Down Expand Up @@ -94,7 +97,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 +112,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 +208,17 @@ fun OutOfTimeScreen(
)
)
}
PlayPaymentButton(
billingPaymentState = uiState.billingPaymentState,
onPurchaseBillingProductClick = onPurchaseBillingProductClick,
modifier =
Modifier.padding(
start = Dimens.sideMargin,
end = Dimens.sideMargin,
bottom = Dimens.screenVerticalMargin
)
.align(Alignment.CenterHorizontally)
)
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import kotlin.test.assertIs
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runTest
import net.mullvad.mullvadvpn.PaymentProvider
import net.mullvad.mullvadvpn.compose.state.OutOfTimeUiState
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.model.AccountExpiry
Expand Down Expand Up @@ -53,6 +54,7 @@ class OutOfTimeViewModelTest {
private val mockAccountRepository: AccountRepository = mockk()
private val mockDeviceRepository: DeviceRepository = mockk()
private val mockServiceConnectionManager: ServiceConnectionManager = mockk()
private val mockPaymentProvider: PaymentProvider = mockk(relaxed = true)

private lateinit var viewModel: OutOfTimeViewModel

Expand All @@ -75,6 +77,7 @@ class OutOfTimeViewModelTest {
accountRepository = mockAccountRepository,
serviceConnectionManager = mockServiceConnectionManager,
deviceRepository = mockDeviceRepository,
paymentProvider = mockPaymentProvider,
pollAccountExpiry = false
)
}
Expand Down

0 comments on commit 064c12c

Please sign in to comment.