From 32af23d0469d9c5a51448808c6585e598b60e3a5 Mon Sep 17 00:00:00 2001 From: Jonatan Rhodin Date: Mon, 18 Sep 2023 16:53:05 +0200 Subject: [PATCH] Add billing payments to AccountViewModel --- .../compose/state/AccountDialogState.kt | 11 +++ .../compose/state/AccountUiState.kt | 10 ++- .../mullvadvpn/compose/state/PaymentState.kt | 15 ++++ .../net/mullvad/mullvadvpn/di/UiModule.kt | 5 +- .../mullvadvpn/viewmodel/AccountViewModel.kt | 86 ++++++++++++++++--- .../lib/payment/PaymentRepository.kt | 4 +- 6 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountDialogState.kt create mode 100644 android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/PaymentState.kt diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountDialogState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountDialogState.kt new file mode 100644 index 000000000000..b79c203cbf2a --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountDialogState.kt @@ -0,0 +1,11 @@ +package net.mullvad.mullvadvpn.compose.state + +sealed interface AccountDialogState { + data object NoDialog: AccountDialogState + + data object VerificationError: AccountDialogState + + data object PurchaseError: AccountDialogState + + data object PurchaseComplete: AccountDialogState +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt index a9527955711f..5a61d3d02630 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/AccountUiState.kt @@ -3,7 +3,11 @@ package net.mullvad.mullvadvpn.compose.state import org.joda.time.DateTime data class AccountUiState( - val deviceName: String, - val accountNumber: String, - val accountExpiry: DateTime? + val deviceName: String = "", + val accountNumber: String = "", + val accountExpiry: DateTime? = null, + val webPaymentAvailable: Boolean = false, + val billingPaymentState: PaymentState = PaymentState.Loading, + val purchaseLoading: Boolean = false, + val dialogState: AccountDialogState = AccountDialogState.NoDialog ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/PaymentState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/PaymentState.kt new file mode 100644 index 000000000000..e1ebd694bda4 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/PaymentState.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.compose.state + +import net.mullvad.mullvadvpn.lib.payment.PaymentProduct + +sealed interface PaymentState { + data object Loading : PaymentState + + data object NoPayment : PaymentState + + data object GenericError : PaymentState + + data object BillingError : PaymentState + + data class PaymentAvailable(val products: List): PaymentState +} 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 05e7548b9f32..7785ca35949d 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 @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.di +import android.app.Activity import android.content.Context import android.content.SharedPreferences import android.content.pm.PackageManager @@ -8,7 +9,9 @@ import kotlinx.coroutines.Dispatchers import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.applist.ApplicationsIconManager import net.mullvad.mullvadvpn.applist.ApplicationsProvider +import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher +import net.mullvad.mullvadvpn.lib.payment.PaymentRepository import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.repository.DeviceRepository @@ -73,7 +76,7 @@ val uiModule = module { single { ChangelogDataProvider(get()) } // View models - viewModel { AccountViewModel(get(), get(), get()) } + viewModel { AccountViewModel(get(), get(), get(), get()) } viewModel { ChangelogViewModel(get(), BuildConfig.VERSION_CODE, BuildConfig.ALWAYS_SHOW_CHANGELOG) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt index e359deed2e3f..dee8d3604b3f 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt @@ -3,13 +3,20 @@ package net.mullvad.mullvadvpn.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch +import net.mullvad.mullvadvpn.compose.state.AccountDialogState import net.mullvad.mullvadvpn.compose.state.AccountUiState +import net.mullvad.mullvadvpn.compose.state.PaymentState +import net.mullvad.mullvadvpn.lib.payment.BillingPaymentAvailability +import net.mullvad.mullvadvpn.lib.payment.PaymentRepository +import net.mullvad.mullvadvpn.lib.payment.PurchaseResult import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager @@ -18,34 +25,45 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache class AccountViewModel( private var accountRepository: AccountRepository, private var serviceConnectionManager: ServiceConnectionManager, + private val paymentRepository: PaymentRepository, deviceRepository: DeviceRepository ) : ViewModel() { + init { + viewModelScope.launch { + paymentRepository.verifyPurchases() + } + } + + private val _dialogState = MutableStateFlow(AccountDialogState.NoDialog) + private val _purchaseLoading = MutableStateFlow(false) private val _viewActions = MutableSharedFlow(extraBufferCapacity = 1) private val _enterTransitionEndAction = MutableSharedFlow() val viewActions = _viewActions.asSharedFlow() private val vmState: StateFlow = - combine(deviceRepository.deviceState, accountRepository.accountExpiryState) { - deviceState, - accountExpiry -> + combine( + deviceRepository.deviceState, + accountRepository.accountExpiryState, + paymentRepository.productsFlow(), + _purchaseLoading, + _dialogState + ) { deviceState, accountExpiry, paymentAvailability, purchaseLoading, dialogState -> AccountUiState( deviceName = deviceState.deviceName() ?: "", accountNumber = deviceState.token() ?: "", - accountExpiry = accountExpiry.date() + accountExpiry = accountExpiry.date(), + webPaymentAvailable = paymentAvailability?.webPaymentAvailable ?: false, + billingPaymentState = + paymentAvailability?.billingPaymentAvailability?.toPaymentState() + ?: PaymentState.Loading, + purchaseLoading = purchaseLoading, + dialogState = dialogState ) } - .stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - AccountUiState(deviceName = "", accountNumber = "", accountExpiry = null) - ) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), AccountUiState()) val uiState = - vmState.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(), - AccountUiState(deviceName = "", accountNumber = "", accountExpiry = null) - ) + vmState.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), AccountUiState()) val enterTransitionEndAction = _enterTransitionEndAction.asSharedFlow() @@ -67,6 +85,46 @@ class AccountViewModel( viewModelScope.launch { _enterTransitionEndAction.emit(Unit) } } + fun startBillingPayment(productId: String) { + viewModelScope.launch { + _purchaseLoading.tryEmit(true) + val result = paymentRepository.purchaseBillingProduct(productId) + _purchaseLoading.tryEmit(false) + when (result) { + PurchaseResult.PurchaseCancelled -> { + // Do nothing + } + PurchaseResult.PurchaseCompleted -> { + // Show purchase completed dialog + _dialogState.tryEmit(AccountDialogState.PurchaseComplete) + } + PurchaseResult.PurchaseError -> { + // Show purchase error dialog + _dialogState.tryEmit(AccountDialogState.PurchaseError) + } + PurchaseResult.VerificationError -> { + // Show verification error dialog + _dialogState.tryEmit(AccountDialogState.VerificationError) + } + } + } + } + + private fun PaymentRepository.productsFlow() = callbackFlow { + this.trySend(null) + this.trySend(this@productsFlow.queryAvailablePaymentTypes()) + } + + private fun BillingPaymentAvailability.toPaymentState(): PaymentState = + when (this) { + BillingPaymentAvailability.Error.ServiceUnavailable, + BillingPaymentAvailability.Error.BillingUnavailable -> PaymentState.BillingError + is BillingPaymentAvailability.Error.Other -> PaymentState.GenericError + is BillingPaymentAvailability.ProductsAvailable -> + PaymentState.PaymentAvailable(products) + BillingPaymentAvailability.ProductsUnavailable -> PaymentState.NoPayment + } + sealed class ViewAction { data class OpenAccountManagementPageInBrowser(val token: String) : ViewAction() } diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt index c25d219f9ac2..d873dfaa6835 100644 --- a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt +++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentRepository.kt @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.lib.payment +import android.app.Activity import kotlinx.coroutines.flow.singleOrNull import net.mullvad.mullvadvpn.lib.billing.BillingRepository import net.mullvad.mullvadvpn.lib.billing.model.BillingProduct @@ -10,7 +11,8 @@ import net.mullvad.mullvadvpn.lib.billing.model.QueryProductResult import net.mullvad.mullvadvpn.lib.billing.model.QueryPurchasesResult class PaymentRepository( - private val billingRepository: BillingRepository, + activity: Activity, + private val billingRepository: BillingRepository = BillingRepository(activity = activity), private val showWebPayment: Boolean ) { suspend fun queryAvailablePaymentTypes(): PaymentAvailability =