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 301ee649b168..6564114acbb8 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 @@ -69,7 +69,7 @@ fun ChangelogDialog(changesList: List, version: String, onDismiss: () -> shape = MaterialTheme.shapes.small ) { Text( - text = stringResource(R.string.changes_dialog_dismiss_button), + text = stringResource(R.string.got_it), fontSize = 18.sp ) } 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 6f22d65fb139..b53f207cbcf1 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 @@ -89,7 +89,7 @@ fun InfoDialog(message: String, additionalInfo: String? = null, onDismiss: () -> shape = MaterialTheme.shapes.small ) { Text( - text = stringResource(R.string.changes_dialog_dismiss_button), + text = stringResource(R.string.got_it), fontSize = dimensionResource(id = R.dimen.text_medium_plus).value.sp, ) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentBillingErrorDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentBillingErrorDialog.kt new file mode 100644 index 000000000000..e49300e7ce81 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentBillingErrorDialog.kt @@ -0,0 +1,26 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.mullvad.mullvadvpn.R + +@Preview +@Composable +fun PreviewPaymentBillingErrorDialog() { + PaymentBillingErrorDialog(onTryAgain = {}, onClose = {}) +} + +@Composable +fun PaymentBillingErrorDialog(onTryAgain: () -> Unit, onClose: () -> Unit) { + PaymentDialog( + title = stringResource(id = R.string.payment_billing_error_dialog_title), + message = stringResource(id = R.string.payment_billing_error_dialog_message), + icon = R.drawable.icon_fail, + onConfirmClick = onClose, + confirmText = stringResource(id = R.string.cancel), + onDismissRequest = onClose, + dismissText = stringResource(id = R.string.try_again), + onDismissClick = onTryAgain + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentCompletedDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentCompletedDialog.kt new file mode 100644 index 000000000000..6dedbe7afdc8 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentCompletedDialog.kt @@ -0,0 +1,24 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.mullvad.mullvadvpn.R + +@Preview +@Composable +fun PreviewPaymentCompletedDialog() { + PaymentCompletedDialog {} +} + +@Composable +fun PaymentCompletedDialog(onClose: () -> Unit) { + PaymentDialog( + title = stringResource(id = R.string.payment_completed_dialog_title), + message = stringResource(id = R.string.payment_completed_dialog_message), + icon = R.drawable.icon_success, + onConfirmClick = onClose, + confirmText = stringResource(id = R.string.got_it), + onDismissRequest = onClose + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentDialog.kt new file mode 100644 index 000000000000..753ca8c9e353 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentDialog.kt @@ -0,0 +1,88 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.ActionButton +import net.mullvad.mullvadvpn.lib.theme.AlphaDescription +import net.mullvad.mullvadvpn.lib.theme.Dimens + +@Preview +@Composable +private fun PreviewPaymentDialog() { + PaymentDialog( + title = "Payment was unsuccessful", + message = "We were unable to verify your paymenmt, please try again", + icon = R.drawable.icon_fail, + onConfirmClick = {}, + confirmText = "Cancel", + onDismissRequest = {}, + dismissText = "Try again", + onDismissClick = {}, + ) +} + +@Composable +fun PaymentDialog( + title: String, + message: String, + icon: Int, + onConfirmClick: () -> Unit, + confirmText: String, + dismissText: String? = null, + onDismissClick: () -> Unit = {}, + onDismissRequest: () -> Unit +) { + AlertDialog( + title = { + Column { + Icon( + modifier = Modifier.fillMaxWidth().height(Dimens.iconHeight), + painter = painterResource(id = icon), + contentDescription = "" + ) + Text(text = title, style = MaterialTheme.typography.headlineSmall) + } + }, + text = { Text(text = message, style = MaterialTheme.typography.bodySmall) }, + containerColor = MaterialTheme.colorScheme.background, + titleContentColor = MaterialTheme.colorScheme.onBackground, + textContentColor = MaterialTheme.colorScheme.onBackground.copy(alpha = AlphaDescription), + onDismissRequest = onDismissRequest, + dismissButton = { + dismissText?.let { + ActionButton( + text = dismissText, + onClick = onDismissClick, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ) + ) + } + }, + confirmButton = { + ActionButton( + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + text = confirmText, + onClick = onConfirmClick + ) + } + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentVerificationErrorDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentVerificationErrorDialog.kt new file mode 100644 index 000000000000..d9d3310e4397 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/PaymentVerificationErrorDialog.kt @@ -0,0 +1,26 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.mullvad.mullvadvpn.R + +@Preview +@Composable +fun PreviewPaymentVerificationErrorDialog() { + PaymentVerificationErrorDialog(onTryAgain = {}, onClose = {}) +} + +@Composable +fun PaymentVerificationErrorDialog(onTryAgain: () -> Unit, onClose: () -> Unit) { + PaymentDialog( + title = stringResource(id = R.string.payment_verification_error_dialog_title), + message = stringResource(id = R.string.payment_verification_error_dialog_message), + icon = R.drawable.icon_fail, + onConfirmClick = onClose, + confirmText = stringResource(id = R.string.cancel), + onDismissRequest = onClose, + dismissText = stringResource(id = R.string.try_again), + onDismissClick = onTryAgain + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt index cf8af6fc0736..d5a715d5a755 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/AccountScreen.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer 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.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -26,7 +28,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import me.onebone.toolbar.ScrollStrategy import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState -import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.button.ActionButton import net.mullvad.mullvadvpn.compose.component.CollapsingToolbarScaffold @@ -35,10 +36,15 @@ import net.mullvad.mullvadvpn.compose.component.CopyableObfuscationView import net.mullvad.mullvadvpn.compose.component.InformationView import net.mullvad.mullvadvpn.compose.component.MissingPolicy import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.compose.dialog.PaymentBillingErrorDialog +import net.mullvad.mullvadvpn.compose.dialog.PaymentCompletedDialog +import net.mullvad.mullvadvpn.compose.dialog.PaymentVerificationErrorDialog +import net.mullvad.mullvadvpn.compose.state.AccountDialogState import net.mullvad.mullvadvpn.compose.state.AccountUiState -import net.mullvad.mullvadvpn.lib.common.constant.BuildTypes +import net.mullvad.mullvadvpn.compose.state.PaymentState import net.mullvad.mullvadvpn.lib.common.util.capitalizeFirstCharOfEachWord import net.mullvad.mullvadvpn.lib.common.util.openAccountPageInBrowser +import net.mullvad.mullvadvpn.lib.payment.PaymentProduct import net.mullvad.mullvadvpn.lib.theme.Dimens import net.mullvad.mullvadvpn.util.toExpiryDateString import net.mullvad.mullvadvpn.viewmodel.AccountViewModel @@ -52,7 +58,12 @@ private fun PreviewAccountScreen() { AccountUiState( deviceName = "Test Name", accountNumber = "1234123412341234", - accountExpiry = null + accountExpiry = null, + webPaymentAvailable = true, + billingPaymentState = + PaymentState.PaymentAvailable( + listOf(PaymentProduct("productId", price = "34 SEK")) + ) ), viewActions = MutableSharedFlow().asSharedFlow(), enterTransitionEndAction = MutableSharedFlow() @@ -68,6 +79,9 @@ fun AccountScreen( onRedeemVoucherClick: () -> Unit = {}, onManageAccountClick: () -> Unit = {}, onLogoutClick: () -> Unit = {}, + onPurchaseBillingProductClick: (productId: String) -> Unit = {}, + onDialogClose: () -> Unit = {}, + onTryVerificationAgain: () -> Unit = {}, onBackClick: () -> Unit = {} ) { val context = LocalContext.current @@ -79,6 +93,32 @@ fun AccountScreen( LaunchedEffect(Unit) { enterTransitionEndAction.collect { systemUiController.setStatusBarColor(backgroundColor) } } + LaunchedEffect(Unit) { + viewActions.collect { viewAction -> + if (viewAction is AccountViewModel.ViewAction.OpenAccountManagementPageInBrowser) { + context.openAccountPageInBrowser(viewAction.token) + } + } + } + + when (uiState.dialogState) { + AccountDialogState.NoDialog -> { + // Show nothing + } + AccountDialogState.PurchaseComplete -> { + PaymentCompletedDialog(onClose = onDialogClose) + } + AccountDialogState.PurchaseError -> { + PaymentBillingErrorDialog(onTryAgain = {}, onClose = onDialogClose) + } + AccountDialogState.VerificationError -> { + PaymentVerificationErrorDialog( + onTryAgain = onTryVerificationAgain, + onClose = onDialogClose + ) + } + } + CollapsingToolbarScaffold( backgroundColor = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize(), @@ -101,14 +141,6 @@ fun AccountScreen( ) }, ) { - LaunchedEffect(Unit) { - viewActions.collect { viewAction -> - if (viewAction is AccountViewModel.ViewAction.OpenAccountManagementPageInBrowser) { - context.openAccountPageInBrowser(viewAction.token) - } - } - } - val scrollState = rememberScrollState() Column( @@ -158,7 +190,53 @@ fun AccountScreen( Spacer(modifier = Modifier.weight(1f)) - if (BuildConfig.BUILD_TYPE != BuildTypes.RELEASE) { + when (uiState.billingPaymentState) { + PaymentState.BillingError, + PaymentState.GenericError -> { + // We should show some kind of dialog error + } + 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 -> + ActionButton( + 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 + ), + colors = + ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onPrimary, + containerColor = MaterialTheme.colorScheme.surface + ) + ) + } + } + } + + if (uiState.webPaymentAvailable) { ActionButton( text = stringResource(id = R.string.manage_account), onClick = onManageAccountClick, 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 7785ca35949d..10d6f2016819 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 @@ -37,6 +37,7 @@ import org.apache.commons.validator.routines.InetAddressValidator import org.koin.android.ext.koin.androidApplication import org.koin.android.ext.koin.androidContext import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.parameter.parametersOf import org.koin.core.qualifier.named import org.koin.dsl.module import org.koin.dsl.onClose @@ -75,8 +76,17 @@ val uiModule = module { single { ChangelogDataProvider(get()) } + single { (activity: Activity) -> + PaymentRepository( + activity = activity, + showWebPayment = BuildConfig.BUILD_TYPE != BuildTypes.RELEASE + ) + } + // View models - viewModel { AccountViewModel(get(), get(), get(), get()) } + viewModel { (activity: Activity) -> + AccountViewModel(get(), get(), get() { parametersOf(activity) }, get()) + } viewModel { ChangelogViewModel(get(), BuildConfig.VERSION_CODE, BuildConfig.ALWAYS_SHOW_CHANGELOG) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt index 39013f11a868..538a729dab4d 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/AccountFragment.kt @@ -16,9 +16,10 @@ import net.mullvad.mullvadvpn.ui.StatusBarPainter import net.mullvad.mullvadvpn.ui.extension.requireMainActivity import net.mullvad.mullvadvpn.viewmodel.AccountViewModel import org.koin.androidx.viewmodel.ext.android.viewModel +import org.koin.core.parameter.parametersOf class AccountFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter { - private val vm by viewModel() + private val vm by viewModel { parametersOf(requireActivity()) } @OptIn(ExperimentalMaterial3Api::class) override fun onCreateView( @@ -36,7 +37,10 @@ class AccountFragment : BaseFragment(), StatusBarPainter, NavigationBarPainter { enterTransitionEndAction = vm.enterTransitionEndAction, onRedeemVoucherClick = { openRedeemVoucherFragment() }, onManageAccountClick = vm::onManageAccountClick, - onLogoutClick = vm::onLogoutClick + onLogoutClick = vm::onLogoutClick, + onPurchaseBillingProductClick = vm::startBillingPayment, + onDialogClose = vm::closeDialog, + onTryVerificationAgain = vm::verifyPurchases ) { activity?.onBackPressed() } diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt index 7920b7b7105b..c95f1511abba 100644 --- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingRepository.kt @@ -237,6 +237,6 @@ class BillingRepository(private val activity: Activity) { } companion object { - private const val PRODUCT_ID = "test" + private const val PRODUCT_ID = "one_month" } } 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 5f21789469f2..1207d2a51e6d 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 @@ -15,9 +15,9 @@ import net.mullvad.mullvadvpn.lib.billing.model.QueryPurchasesResult class PaymentRepository( activity: Activity, - private val billingRepository: BillingRepository = BillingRepository(activity = activity), private val showWebPayment: Boolean ) { + private val billingRepository: BillingRepository = BillingRepository(activity = activity) private val _billingPurchaseEvents = billingRepository.purchaseEvents.map { event -> when (event) { diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index ac979ed2106c..d0e1b209b3bd 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -148,7 +148,6 @@ Show account number Failed to remove device Changes in this version: - Got it! Always-on VPN assigned to other app %s before using Mullvad VPN.]]> @@ -216,4 +215,12 @@ Remove custom port Valid ranges: %s Enter port + Add 30 days time (%s) + Time was successfully added + 30 days was added to your account. + Got it! + Payment was unsuccessful + We were unable to verify your payment, please try again. + Google Play services not available + Unable to connect to Google Play services. Please make sure you have the latest version of Google Play services. diff --git a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt index aeee6593da6f..cd2015dc6a0f 100644 --- a/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt +++ b/android/lib/theme/src/main/kotlin/net/mullvad/mullvadvpn/lib/theme/dimensions/Dimensions.kt @@ -20,6 +20,7 @@ data class Dimensions( val countryRowPadding: Dp = 18.dp, val customPortBoxMinWidth: Dp = 80.dp, val expandableCellChevronSize: Dp = 30.dp, + val iconHeight: Dp = 44.dp, val indentedCellStartPadding: Dp = 38.dp, val infoButtonVerticalPadding: Dp = 13.dp, val listIconSize: Dp = 24.dp,