summaryrefslogtreecommitdiffhomepage
path: root/android
diff options
context:
space:
mode:
authorJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 02:02:30 +0100
committerJonatan Rhodin <jonatan.rhodin@mullvad.net>2023-11-16 09:34:26 +0100
commit91a030a18450c5b7bc531544c54df0445746f1fa (patch)
tree2fc905270638fe32476544bfcbaa9f9cd3079535 /android
parent868e35cb9a814fea6c313eb93849ab426326be53 (diff)
downloadmullvadvpn-91a030a18450c5b7bc531544c54df0445746f1fa.tar.xz
mullvadvpn-91a030a18450c5b7bc531544c54df0445746f1fa.zip
Add payment support to AccountViewModel
Diffstat (limited to 'android')
-rw-r--r--android/app/build.gradle.kts3
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt5
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/InfoDialog.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialogData.kt26
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/PaymentState.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt22
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt19
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PurchaseResultExtensions.kt78
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/AccountViewModel.kt79
-rw-r--r--android/app/src/oss/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt6
-rw-r--r--android/app/src/play/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt14
-rw-r--r--android/buildSrc/src/main/kotlin/Extensions.kt3
-rw-r--r--android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentProvider.kt3
-rw-r--r--android/lib/resource/src/main/res/values/strings.xml16
16 files changed, 278 insertions, 23 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 3da373a368..a3035f0a12 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -317,6 +317,9 @@ dependencies {
implementation(project(Dependencies.Mullvad.themeLib))
implementation(project(Dependencies.Mullvad.paymentLib))
+ // Play implementation
+ playImplementation(project(Dependencies.Mullvad.billingLib))
+
implementation(Dependencies.androidMaterial)
implementation(Dependencies.commonsValidator)
implementation(Dependencies.AndroidX.appcompat)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt
index 89af2eafe9..9ce21c6bac 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ChangelogDialog.kt
@@ -50,10 +50,7 @@ fun ChangelogDialog(changesList: List<String>, version: String, onDismiss: () ->
}
},
confirmButton = {
- PrimaryButton(
- text = stringResource(R.string.changes_dialog_dismiss_button),
- onClick = onDismiss
- )
+ PrimaryButton(text = stringResource(R.string.got_it), onClick = onDismiss)
},
containerColor = MaterialTheme.colorScheme.background,
titleContentColor = MaterialTheme.colorScheme.onBackground
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/InfoDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/InfoDialog.kt
index ad12932405..d032a9fa8e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/InfoDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/InfoDialog.kt
@@ -88,7 +88,7 @@ fun InfoDialog(message: String, additionalInfo: String? = null, onDismiss: () ->
confirmButton = {
PrimaryButton(
modifier = Modifier.wrapContentHeight().fillMaxWidth(),
- text = stringResource(R.string.changes_dialog_dismiss_button),
+ text = stringResource(R.string.got_it),
onClick = onDismiss,
)
},
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt
index 14afdbcf24..c5b619a9cd 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/RedeemVoucherDialog.kt
@@ -120,7 +120,7 @@ fun RedeemVoucherDialog(
stringResource(
id =
if (uiState.voucherViewModelState is VoucherDialogState.Success)
- R.string.changes_dialog_dismiss_button
+ R.string.got_it
else R.string.cancel
),
onClick = {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialogData.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialogData.kt
new file mode 100644
index 0000000000..9876964610
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/payment/PaymentDialogData.kt
@@ -0,0 +1,26 @@
+package net.mullvad.mullvadvpn.compose.dialog.payment
+
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+
+data class PaymentDialogData(
+ val title: Int? = null,
+ val message: Int? = null,
+ val icon: PaymentDialogIcon? = null,
+ val confirmAction: PaymentDialogAction? = null,
+ val dismissAction: PaymentDialogAction? = null,
+ val closeOnDismiss: Boolean = true,
+ val successfulPayment: Boolean = false
+)
+
+sealed class PaymentDialogAction(val message: Int) {
+ data object Close : PaymentDialogAction(R.string.got_it)
+
+ data class RetryPurchase(val productId: ProductId) : PaymentDialogAction(R.string.try_again)
+}
+
+enum class PaymentDialogIcon {
+ SUCCESS,
+ FAIL,
+ LOADING
+}
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 0000000000..60f8d5864f
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/PaymentState.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.compose.state
+
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct
+
+sealed interface PaymentState {
+ data object Loading : PaymentState
+
+ data object NoPayment : PaymentState
+
+ data object NoProductsFounds : PaymentState
+
+ data class PaymentAvailable(val products: List<PaymentProduct>) : PaymentState
+
+ sealed interface Error : PaymentState {
+ data object Generic : Error
+
+ data object Billing : Error
+ }
+}
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 de8801557a..6ed744aef9 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
@@ -12,6 +12,7 @@ import net.mullvad.mullvadvpn.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher
import net.mullvad.mullvadvpn.lib.ipc.MessageHandler
+import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
import net.mullvad.mullvadvpn.repository.AccountRepository
import net.mullvad.mullvadvpn.repository.ChangelogRepository
import net.mullvad.mullvadvpn.repository.DeviceRepository
@@ -22,7 +23,10 @@ import net.mullvad.mullvadvpn.ui.serviceconnection.RelayListListener
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling
import net.mullvad.mullvadvpn.usecase.AccountExpiryNotificationUseCase
+import net.mullvad.mullvadvpn.usecase.EmptyPaymentUseCase
import net.mullvad.mullvadvpn.usecase.NewDeviceNotificationUseCase
+import net.mullvad.mullvadvpn.usecase.PaymentUseCase
+import net.mullvad.mullvadvpn.usecase.PlayPaymentUseCase
import net.mullvad.mullvadvpn.usecase.PortRangeUseCase
import net.mullvad.mullvadvpn.usecase.RelayListUseCase
import net.mullvad.mullvadvpn.usecase.TunnelStateNotificationUseCase
@@ -100,8 +104,20 @@ val uiModule = module {
single { RelayListListener(get()) }
+ // Will be resolved using from either of the two PaymentModule.kt classes.
+ single { PaymentProvider(get()) }
+
+ single<PaymentUseCase> {
+ val paymentRepository = get<PaymentProvider>().paymentRepository
+ if (paymentRepository != null) {
+ PlayPaymentUseCase(paymentRepository = paymentRepository)
+ } else {
+ EmptyPaymentUseCase()
+ }
+ }
+
// View models
- viewModel { AccountViewModel(get(), get(), get()) }
+ viewModel { AccountViewModel(get(), get(), get(), get()) }
viewModel {
ChangelogViewModel(get(), BuildConfig.VERSION_CODE, BuildConfig.ALWAYS_SHOW_CHANGELOG)
}
@@ -114,10 +130,10 @@ val uiModule = module {
viewModel { SettingsViewModel(get(), get()) }
viewModel { VoucherDialogViewModel(get(), get()) }
viewModel { VpnSettingsViewModel(get(), get(), get(), get(), get()) }
- viewModel { WelcomeViewModel(get(), get(), get(), get()) }
+ viewModel { WelcomeViewModel(get(), get(), get()) }
viewModel { ReportProblemViewModel(get()) }
viewModel { ViewLogsViewModel(get()) }
- viewModel { OutOfTimeViewModel(get(), get(), get(), get()) }
+ viewModel { OutOfTimeViewModel(get(), get(), get()) }
}
const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
index 98b0c0576c..f299b8c956 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt
@@ -28,6 +28,7 @@ import kotlinx.coroutines.withTimeoutOrNull
import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.dialog.ChangelogDialog
+import net.mullvad.mullvadvpn.di.paymentModule
import net.mullvad.mullvadvpn.di.uiModule
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.isNotificationPermissionGranted
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration
@@ -54,6 +55,7 @@ import net.mullvad.mullvadvpn.viewmodel.ChangelogDialogUiState
import net.mullvad.mullvadvpn.viewmodel.ChangelogViewModel
import org.koin.android.ext.android.getKoin
import org.koin.core.context.loadKoinModules
+import org.koin.dsl.bind
open class MainActivity : FragmentActivity() {
private val requestNotificationPermissionLauncher =
@@ -78,7 +80,7 @@ open class MainActivity : FragmentActivity() {
private var currentDeviceState: DeviceState? = null
override fun onCreate(savedInstanceState: Bundle?) {
- loadKoinModules(uiModule)
+ loadKoinModules(listOf(uiModule, paymentModule))
getKoin().apply {
accountRepository = get()
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt
new file mode 100644
index 0000000000..6a69a807f1
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PaymentAvailabilityExtensions.kt
@@ -0,0 +1,19 @@
+package net.mullvad.mullvadvpn.util
+
+import net.mullvad.mullvadvpn.compose.state.PaymentState
+import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability
+
+fun PaymentAvailability.toPaymentState(): PaymentState =
+ when (this) {
+ PaymentAvailability.Error.ServiceUnavailable,
+ PaymentAvailability.Error.BillingUnavailable -> PaymentState.Error.Billing
+ is PaymentAvailability.Error.Other -> PaymentState.Error.Generic
+ is PaymentAvailability.ProductsAvailable -> PaymentState.PaymentAvailable(products)
+ PaymentAvailability.ProductsUnavailable -> PaymentState.NoPayment
+ PaymentAvailability.NoProductsFounds -> PaymentState.NoProductsFounds
+ PaymentAvailability.Loading -> PaymentState.Loading
+ // Unrecoverable error states
+ PaymentAvailability.Error.DeveloperError,
+ PaymentAvailability.Error.FeatureNotSupported,
+ PaymentAvailability.Error.ItemUnavailable -> PaymentState.NoPayment
+ }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PurchaseResultExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PurchaseResultExtensions.kt
new file mode 100644
index 0000000000..bf6dbec35e
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PurchaseResultExtensions.kt
@@ -0,0 +1,78 @@
+package net.mullvad.mullvadvpn.util
+
+import net.mullvad.mullvadvpn.R
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogAction
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogIcon
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
+import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult
+
+fun PurchaseResult.toPaymentDialogData(): PaymentDialogData? =
+ when (this) {
+ // Idle states
+ PurchaseResult.Completed.Cancelled,
+ PurchaseResult.BillingFlowStarted,
+ is PurchaseResult.Error.BillingError -> {
+ // Show nothing
+ null
+ }
+ // Fetching products and obfuscated id loading state
+ PurchaseResult.FetchingProducts,
+ PurchaseResult.FetchingObfuscationId ->
+ PaymentDialogData(
+ title = R.string.loading_connecting,
+ icon = PaymentDialogIcon.LOADING,
+ closeOnDismiss = false
+ )
+ // Verifying loading states
+ PurchaseResult.VerificationStarted ->
+ PaymentDialogData(
+ title = R.string.loading_verifying,
+ icon = PaymentDialogIcon.LOADING,
+ closeOnDismiss = false
+ )
+ // Pending state
+ PurchaseResult.Completed.Pending,
+ is PurchaseResult.Error.VerificationError ->
+ PaymentDialogData(
+ title = R.string.payment_pending_dialog_title,
+ message = R.string.payment_pending_dialog_message,
+ confirmAction = PaymentDialogAction.Close
+ )
+ // Success state
+ PurchaseResult.Completed.Success ->
+ PaymentDialogData(
+ title = R.string.payment_completed_dialog_title,
+ message = R.string.payment_completed_dialog_message,
+ icon = PaymentDialogIcon.SUCCESS,
+ confirmAction = PaymentDialogAction.Close,
+ successfulPayment = true
+ )
+ // Error states
+ is PurchaseResult.Error.TransactionIdError ->
+ PaymentDialogData(
+ title = R.string.payment_obfuscation_id_error_dialog_title,
+ message = R.string.payment_obfuscation_id_error_dialog_message,
+ icon = PaymentDialogIcon.FAIL,
+ confirmAction = PaymentDialogAction.Close,
+ dismissAction = PaymentDialogAction.RetryPurchase(productId = this.productId),
+ )
+ is PurchaseResult.Error.FetchProductsError,
+ is PurchaseResult.Error.NoProductFound -> {
+ PaymentDialogData(
+ title = R.string.payment_billing_error_dialog_title,
+ message = R.string.payment_billing_error_dialog_message,
+ icon = PaymentDialogIcon.FAIL,
+ confirmAction = PaymentDialogAction.Close,
+ dismissAction =
+ PaymentDialogAction.RetryPurchase(
+ productId =
+ when (this) {
+ is PurchaseResult.Error.FetchProductsError -> this.productId
+ is PurchaseResult.Error.NoProductFound -> this.productId
+ else -> ProductId("")
+ }
+ ),
+ )
+ }
+ }
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 fb3e3d6393..5f72167499 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
@@ -1,39 +1,54 @@
package net.mullvad.mullvadvpn.viewmodel
+import android.app.Activity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import net.mullvad.mullvadvpn.compose.dialog.payment.PaymentDialogData
+import net.mullvad.mullvadvpn.compose.state.PaymentState
+import net.mullvad.mullvadvpn.lib.payment.model.ProductId
import net.mullvad.mullvadvpn.model.AccountExpiry
import net.mullvad.mullvadvpn.model.DeviceState
import net.mullvad.mullvadvpn.repository.AccountRepository
import net.mullvad.mullvadvpn.repository.DeviceRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.ui.serviceconnection.authTokenCache
+import net.mullvad.mullvadvpn.usecase.PaymentUseCase
+import net.mullvad.mullvadvpn.util.toPaymentDialogData
+import net.mullvad.mullvadvpn.util.toPaymentState
import org.joda.time.DateTime
class AccountViewModel(
- private var accountRepository: AccountRepository,
- private var serviceConnectionManager: ServiceConnectionManager,
+ private val accountRepository: AccountRepository,
+ private val serviceConnectionManager: ServiceConnectionManager,
+ private val paymentUseCase: PaymentUseCase,
deviceRepository: DeviceRepository
) : ViewModel() {
private val _uiSideEffect = MutableSharedFlow<UiSideEffect>(extraBufferCapacity = 1)
private val _enterTransitionEndAction = MutableSharedFlow<Unit>()
+
val uiSideEffect = _uiSideEffect.asSharedFlow()
- val uiState =
- combine(deviceRepository.deviceState, accountRepository.accountExpiryState) {
- deviceState,
- accountExpiry ->
+ val uiState: StateFlow<AccountUiState> =
+ combine(
+ deviceRepository.deviceState,
+ accountRepository.accountExpiryState,
+ paymentUseCase.purchaseResult,
+ paymentUseCase.paymentAvailability
+ ) { deviceState, accountExpiry, purchaseResult, paymentAvailability ->
AccountUiState(
- deviceName = deviceState.deviceName(),
- accountNumber = deviceState.token(),
- accountExpiry = accountExpiry.date()
+ deviceName = deviceState.deviceName() ?: "",
+ accountNumber = deviceState.token() ?: "",
+ accountExpiry = accountExpiry.date(),
+ paymentDialogData = purchaseResult?.toPaymentDialogData(),
+ billingPaymentState = paymentAvailability?.toPaymentState()
)
}
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), AccountUiState.default())
@@ -42,7 +57,9 @@ class AccountViewModel(
val enterTransitionEndAction = _enterTransitionEndAction.asSharedFlow()
init {
- accountRepository.fetchAccountExpiry()
+ updateAccountExpiry()
+ verifyPurchases()
+ fetchPaymentAvailability()
}
fun onManageAccountClick() {
@@ -63,6 +80,40 @@ class AccountViewModel(
viewModelScope.launch { _enterTransitionEndAction.emit(Unit) }
}
+ 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()
+ } else {
+ fetchPaymentAvailability()
+ }
+ viewModelScope.launch {
+ paymentUseCase.resetPurchaseResult() // So that we do not show the dialog again.
+ }
+ }
+
+ private fun updateAccountExpiry() {
+ accountRepository.fetchAccountExpiry()
+ }
+
sealed class UiSideEffect {
data class OpenAccountManagementPageInBrowser(val token: String) : UiSideEffect()
}
@@ -71,14 +122,18 @@ class AccountViewModel(
data class AccountUiState(
val deviceName: String?,
val accountNumber: String?,
- val accountExpiry: DateTime?
+ val accountExpiry: DateTime?,
+ val billingPaymentState: PaymentState? = null,
+ val paymentDialogData: PaymentDialogData? = null
) {
companion object {
fun default() =
AccountUiState(
deviceName = DeviceState.Unknown.deviceName(),
accountNumber = DeviceState.Unknown.token(),
- accountExpiry = AccountExpiry.Missing.date()
+ accountExpiry = AccountExpiry.Missing.date(),
+ billingPaymentState = PaymentState.Loading,
+ paymentDialogData = null,
)
}
}
diff --git a/android/app/src/oss/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt b/android/app/src/oss/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt
new file mode 100644
index 0000000000..cb5cb649a6
--- /dev/null
+++ b/android/app/src/oss/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt
@@ -0,0 +1,6 @@
+package net.mullvad.mullvadvpn.di
+
+import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
+import org.koin.dsl.module
+
+val paymentModule = module { single { PaymentProvider(null) } }
diff --git a/android/app/src/play/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt b/android/app/src/play/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt
new file mode 100644
index 0000000000..82738b5246
--- /dev/null
+++ b/android/app/src/play/kotlin/net/mullvad/mullvadvpn/di/PaymentModule.kt
@@ -0,0 +1,14 @@
+package net.mullvad.mullvadvpn.di
+
+import net.mullvad.mullvadvpn.lib.billing.BillingPaymentRepository
+import net.mullvad.mullvadvpn.lib.billing.BillingRepository
+import net.mullvad.mullvadvpn.lib.billing.PlayPurchaseRepository
+import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
+import org.koin.android.ext.koin.androidContext
+import org.koin.dsl.module
+
+val paymentModule = module {
+ single { BillingRepository(androidContext()) }
+ single { PaymentProvider(BillingPaymentRepository(get(), get())) }
+ single { PlayPurchaseRepository(get()) }
+}
diff --git a/android/buildSrc/src/main/kotlin/Extensions.kt b/android/buildSrc/src/main/kotlin/Extensions.kt
index 8659cb8ecb..0115aa9f30 100644
--- a/android/buildSrc/src/main/kotlin/Extensions.kt
+++ b/android/buildSrc/src/main/kotlin/Extensions.kt
@@ -13,3 +13,6 @@ fun String.isNonStableVersion(): Boolean {
fun DependencyHandler.`leakCanaryImplementation`(dependencyNotation: Any): Dependency? =
add("leakCanaryImplementation", dependencyNotation)
+
+fun DependencyHandler.`playImplementation`(dependencyNotation: Any): Dependency? =
+ add("playImplementation", dependencyNotation)
diff --git a/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentProvider.kt b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentProvider.kt
new file mode 100644
index 0000000000..431b406dc0
--- /dev/null
+++ b/android/lib/payment/src/main/kotlin/net/mullvad/mullvadvpn/lib/payment/PaymentProvider.kt
@@ -0,0 +1,3 @@
+package net.mullvad.mullvadvpn.lib.payment
+
+@JvmInline value class PaymentProvider(val paymentRepository: PaymentRepository?)
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index a769d91bcb..f3b0b0d157 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -151,7 +151,6 @@
<string name="show_account_number">Show account number</string>
<string name="failed_to_remove_device">Failed to remove device</string>
<string name="changes_dialog_subtitle">Changes in this version:</string>
- <string name="changes_dialog_dismiss_button">Got it!</string>
<string name="always_on_vpn_error_notification_title">Always-on VPN assigned to other app</string>
<string name="always_on_vpn_error_notification_content">
<![CDATA[Unable to start tunnel connection. Please disable Always-on VPN for <b>%s</b> before using Mullvad VPN.]]>
@@ -229,4 +228,19 @@
<string name="less_than_one_day">less than one day</string>
<string name="top_bar_time_left">Time left: %s</string>
<string name="top_bar_device_name">Device name: %s</string>
+ <string name="add_30_days_time_x">Add 30 days time (%s)</string>
+ <string name="add_30_days_time">Add 30 days time</string>
+ <string name="payment_completed_dialog_title">Time was successfully added</string>
+ <string name="payment_completed_dialog_message">30 days was added to your account.</string>
+ <string name="got_it">Got it!</string>
+ <string name="payment_billing_error_dialog_title">Google Play unavailable</string>
+ <string name="payment_billing_error_dialog_message">We were unable to start the payment process, please make sure you have the latest version of Google Play.</string>
+ <string name="payment_obfuscation_id_error_dialog_title">Mullvad services unavailable</string>
+ <string name="payment_obfuscation_id_error_dialog_message">We were unable to start the payment process, please try again later.</string>
+ <string name="payment_status_pending">Google Play payment pending</string>
+ <string name="payment_status_verification_in_progress">Verifying purchase</string>
+ <string name="payment_pending_dialog_title">Verifying purchase</string>
+ <string name="payment_pending_dialog_message">We are currently verifying your purchase, this might take some time. Your time will be added if the verification is successful.</string>
+ <string name="loading_connecting">Connecting...</string>
+ <string name="loading_verifying">Verifying purchase...</string>
</resources>