summaryrefslogtreecommitdiffhomepage
path: root/android/app/src
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 01:05:49 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 09:34:26 +0100
commit8afe089bb7990328e7d38b8c3133289e36f83203 (patch)
treef760f1d7a2550ba1015c3ae14c0d9a934716e7e4 /android/app/src
parentf95bf57f088b9e7bd5dfb958707853ed63408fd4 (diff)
downloadmullvadvpn-8afe089bb7990328e7d38b8c3133289e36f83203.tar.xz
mullvadvpn-8afe089bb7990328e7d38b8c3133289e36f83203.zip
Add billing payment to out of time screen and view model
Diffstat (limited to 'android/app/src')
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/OutOfTimeScreen.kt46
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/OutOfTimeUiState.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/OutOfTimeFragment.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/OutOfTimeViewModel.kt64
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