From 559bfbe2f722144e669ff5810dee3c9e41f9e06e Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 20 Sep 2023 15:14:30 +0200 Subject: [wallet] simplify pay templates --- .../taler/wallet/payment/PayTemplateComposable.kt | 211 ++++++++------------- .../taler/wallet/payment/PayTemplateFragment.kt | 65 ++++--- .../wallet/payment/PayTemplateOrderComposable.kt | 179 +++++++++++++++++ .../net/taler/wallet/payment/PaymentManager.kt | 9 +- 4 files changed, 295 insertions(+), 169 deletions(-) create mode 100644 wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt (limited to 'wallet/src/main/java/net/taler') diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt index 59a088d..815f463 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt @@ -17,151 +17,62 @@ package net.taler.wallet.payment import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable -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.Companion.Center -import androidx.compose.ui.Alignment.Companion.End import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import net.taler.common.Amount +import net.taler.common.ContractTerms import net.taler.wallet.AmountResult import net.taler.wallet.R -import net.taler.wallet.compose.AmountInputField import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.deposit.CurrencyDropdown sealed class AmountFieldStatus { - object FixedAmount: AmountFieldStatus() + object FixedAmount : AmountFieldStatus() class Default( val amountStr: String? = null, val currency: String? = null, - ): AmountFieldStatus() - object Invalid: AmountFieldStatus() + ) : AmountFieldStatus() + + object Invalid : AmountFieldStatus() } @Composable fun PayTemplateComposable( - summary: String?, + defaultSummary: String?, amountStatus: AmountFieldStatus, currencies: List, payStatus: PayStatus, onCreateAmount: (String, String) -> AmountResult, - onSubmit: (Map) -> Unit, + onSubmit: (summary: String?, amount: Amount?) -> Unit, onError: (resId: Int) -> Unit, ) { - // If wallet is empty, there's no way the user can pay something if (amountStatus is AmountFieldStatus.Invalid) { PayTemplateError(stringResource(R.string.receive_amount_invalid)) - } else if (payStatus is PayStatus.InsufficientBalance || currencies.isEmpty()) { + } else if (currencies.isEmpty()) { PayTemplateError(stringResource(R.string.payment_balance_insufficient)) } else when (payStatus) { - is PayStatus.None -> PayTemplateDefault( + is PayStatus.None -> PayTemplateOrderComposable( currencies = currencies, - summary = summary, + defaultSummary = defaultSummary, amountStatus = amountStatus, onCreateAmount = onCreateAmount, onError = onError, - onSubmit = { s, a -> - onSubmit(mutableMapOf().apply { - s?.let { put("summary", it) } - a?.let { put("amount", it.toJSONString()) } - }) - } + onSubmit = onSubmit, ) + is PayStatus.Loading -> PayTemplateLoading() is PayStatus.AlreadyPaid -> PayTemplateError(stringResource(R.string.payment_already_paid)) - - // TODO we should handle the other cases or explain why we don't handle them - else -> {} - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun PayTemplateDefault( - currencies: List, - summary: String? = null, - amountStatus: AmountFieldStatus, - onCreateAmount: (String, String) -> AmountResult, - onError: (msgRes: Int) -> Unit, - onSubmit: (summary: String?, amount: Amount?) -> Unit, -) { - val amountDefault = amountStatus as? AmountFieldStatus.Default - - var localSummary by remember { mutableStateOf(summary) } - var localAmount by remember { mutableStateOf( - amountDefault?.let { s -> s.amountStr ?: "0" } - ) } - var localCurrency by remember { mutableStateOf( - amountDefault?.let { s -> s.currency ?: currencies[0] } - ) } - - Column(horizontalAlignment = End) { - localSummary?.let { summary -> - OutlinedTextField( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - value = summary, - isError = summary.isBlank(), - onValueChange = { localSummary = it }, - singleLine = true, - label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) }, - ) - } - - localAmount?.let { amount -> - localCurrency?.let { currency -> - AmountField( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - amount = amount, - currency = currency, - currencies = currencies, - fixedCurrency = (amountStatus as? AmountFieldStatus.Default)?.currency != null, - onAmountChosen = { a, c -> - localAmount = a - localCurrency = c - }, - ) - } - } - - Button( - modifier = Modifier.padding(16.dp), - enabled = localSummary == null || localSummary!!.isNotBlank(), - onClick = { - localAmount?.let { amount -> - localCurrency?.let { currency -> - when (val res = onCreateAmount(amount, currency)) { - is AmountResult.InsufficientBalance -> onError(R.string.payment_balance_insufficient) - is AmountResult.InvalidAmount -> onError(R.string.receive_amount_invalid) - is AmountResult.Success -> onSubmit(summary, res.amount) - } - } - } - }, - ) { - Text(stringResource(R.string.payment_create_order)) - } + is PayStatus.InsufficientBalance -> PayTemplateError(stringResource(R.string.payment_balance_insufficient)) + is PayStatus.Error -> {} // handled in fragment will show bottom sheet FIXME white page? + is PayStatus.Prepared -> {} // handled in fragment, will redirect + is PayStatus.Success -> {} // handled by other UI flow, no need for content here } } @@ -189,51 +100,81 @@ fun PayTemplateLoading() { } } +@Preview @Composable -private fun AmountField( - modifier: Modifier = Modifier, - currencies: List, - fixedCurrency: Boolean, - amount: String, - currency: String, - onAmountChosen: (amount: String, currency: String) -> Unit, -) { - Row( - modifier = modifier, - ) { - AmountInputField( - modifier = Modifier - .padding(end = 16.dp) - .weight(1f), - value = amount, - onValueChange = { onAmountChosen(it, currency) }, - label = { Text(stringResource(R.string.send_amount)) } +fun PayTemplateLoadingPreview() { + TalerSurface { + PayTemplateComposable( + defaultSummary = "Donation", + amountStatus = AmountFieldStatus.Default("20", "ARS"), + payStatus = PayStatus.Loading, + currencies = listOf("KUDOS", "ARS"), + onCreateAmount = { text, currency -> + AmountResult.Success(amount = Amount.fromString(currency, text)) + }, + onSubmit = { _, _ -> }, + onError = { _ -> }, ) - CurrencyDropdown( - modifier = Modifier.weight(1f), - initialCurrency = currency, - currencies = currencies, - onCurrencyChanged = { onAmountChosen(amount, it) }, - readOnly = fixedCurrency, + } +} + +@Preview +@Composable +fun PayTemplateInsufficientBalancePreview() { + TalerSurface { + PayTemplateComposable( + defaultSummary = "Donation", + amountStatus = AmountFieldStatus.Default("20", "ARS"), + payStatus = PayStatus.InsufficientBalance( + ContractTerms( + "test", + amount = Amount.zero("TESTKUDOS"), + products = emptyList() + ), Amount.zero("TESTKUDOS") + ), + currencies = listOf("KUDOS", "ARS"), + onCreateAmount = { text, currency -> + AmountResult.Success(amount = Amount.fromString(currency, text)) + }, + onSubmit = { _, _ -> }, + onError = { _ -> }, ) } } @Preview @Composable -fun PayTemplateComposablePreview() { +fun PayTemplateAlreadyPaidPreview() { TalerSurface { PayTemplateComposable( - summary = "Donation", - amountStatus = AmountFieldStatus.Default("20", "ARS"), + defaultSummary = "Donation", + amountStatus = AmountFieldStatus.Default("20", "ARS"), + payStatus = PayStatus.AlreadyPaid, currencies = listOf("KUDOS", "ARS"), - // TODO create previews for other states + onCreateAmount = { text, currency -> + AmountResult.Success(amount = Amount.fromString(currency, text)) + }, + onSubmit = { _, _ -> }, + onError = { _ -> }, + ) + } +} + + +@Preview +@Composable +fun PayTemplateNoCurrenciesPreview() { + TalerSurface { + PayTemplateComposable( + defaultSummary = "Donation", + amountStatus = AmountFieldStatus.Default("20", "ARS"), payStatus = PayStatus.None, + currencies = emptyList(), onCreateAmount = { text, currency -> AmountResult.Success(amount = Amount.fromString(currency, text)) }, - onSubmit = { }, - onError = { }, + onSubmit = { _, _ -> }, + onError = { _ -> }, ) } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt index 01160ec..c902d65 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt @@ -21,18 +21,18 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.asFlow import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController +import net.taler.common.Amount import net.taler.common.showError import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.compose.TalerSurface +import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.showError class PayTemplateFragment : Fragment() { @@ -49,37 +49,23 @@ class PayTemplateFragment : Fragment() { uriString = arguments?.getString("uri") ?: error("no amount passed") uri = Uri.parse(uriString) - val queryParams = uri.queryParameterNames + val defaultSummary = uri.getQueryParameter("summary") + val defaultAmount = uri.getQueryParameter("amount") + val amountFieldStatus = getAmountFieldStatus(defaultAmount) - val summary = if ("summary" in queryParams) - uri.getQueryParameter("summary")!! else null - - val amountStatus = if ("amount" in queryParams) { - val amount = uri.getQueryParameter("amount")!! - val parts = if (amount.isEmpty()) emptyList() else amount.split(":") - when (parts.size) { - 0 -> AmountFieldStatus.Default() - 1 -> AmountFieldStatus.Default(currency = parts[0]) - 2 -> AmountFieldStatus.Default(parts[1], parts[0]) - else -> AmountFieldStatus.Invalid - } - } else AmountFieldStatus.FixedAmount + val payStatusFlow = model.paymentManager.payStatus.asFlow() return ComposeView(requireContext()).apply { setContent { - val payStatus by model.paymentManager.payStatus - .asFlow() - .collectAsState(initial = PayStatus.None) + val payStatus = payStatusFlow.collectAsStateLifecycleAware(initial = PayStatus.None) TalerSurface { PayTemplateComposable( currencies = model.getCurrencies(), - summary = summary, - amountStatus = amountStatus, - payStatus = payStatus, - onCreateAmount = { text, currency -> - model.createAmount(text, currency) - }, - onSubmit = { createOrder(it) }, + defaultSummary = defaultSummary, + amountStatus = amountFieldStatus, + payStatus = payStatus.value, + onCreateAmount = model::createAmount, + onSubmit = this@PayTemplateFragment::createOrder, onError = { this@PayTemplateFragment.showError(it) }, ) } @@ -90,7 +76,7 @@ class PayTemplateFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (uri.queryParameterNames?.isEmpty() == true) { - createOrder(emptyMap()) + createOrder(null, null) } model.paymentManager.payStatus.observe(viewLifecycleOwner) { payStatus -> @@ -99,9 +85,9 @@ class PayTemplateFragment : Fragment() { val navOptions = NavOptions.Builder() .setPopUpTo(R.id.nav_main, true) .build() - findNavController() - .navigate(R.id.action_global_promptPayment, null, navOptions) + findNavController().navigate(R.id.action_global_promptPayment, null, navOptions) } + is PayStatus.Error -> { if (model.devMode.value == true) { showError(payStatus.error) @@ -109,12 +95,29 @@ class PayTemplateFragment : Fragment() { showError(R.string.payment_template_error, payStatus.error.userFacingMsg) } } + else -> {} } } } - private fun createOrder(params: Map) { - model.paymentManager.preparePayForTemplate(uriString, params) + private fun getAmountFieldStatus(defaultAmount: String?): AmountFieldStatus { + return if (defaultAmount == null) { + AmountFieldStatus.FixedAmount + } else if (defaultAmount.isBlank()) { + AmountFieldStatus.Default() + } else { + val parts = defaultAmount.split(":") + when (parts.size) { + 0 -> AmountFieldStatus.Default() + 1 -> AmountFieldStatus.Default(currency = parts[0]) + 2 -> AmountFieldStatus.Default(parts[1], parts[0]) + else -> AmountFieldStatus.Invalid + } + } + } + + private fun createOrder(summary: String?, amount: Amount?) { + model.paymentManager.preparePayForTemplate(uriString, summary, amount) } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt new file mode 100644 index 0000000..1524faf --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt @@ -0,0 +1,179 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.wallet.payment + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.Companion.End +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import net.taler.common.Amount +import net.taler.wallet.AmountResult +import net.taler.wallet.R +import net.taler.wallet.compose.AmountInputField +import net.taler.wallet.compose.TalerSurface +import net.taler.wallet.deposit.CurrencyDropdown + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PayTemplateOrderComposable( + currencies: List, // assumed to have size > 0 + defaultSummary: String? = null, + amountStatus: AmountFieldStatus, + onCreateAmount: (String, String) -> AmountResult, + onError: (msgRes: Int) -> Unit, + onSubmit: (summary: String?, amount: Amount?) -> Unit, +) { + val amountDefault = amountStatus as? AmountFieldStatus.Default + + var summary by remember { mutableStateOf(defaultSummary) } + var currency by remember { mutableStateOf(amountDefault?.currency ?: currencies[0]) } + var amount by remember { mutableStateOf(amountDefault?.amountStr ?: "0") } + + Column(horizontalAlignment = End) { + if (defaultSummary != null) OutlinedTextField( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + value = summary ?: "", + isError = summary.isNullOrBlank(), + onValueChange = { summary = it }, + singleLine = true, + label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) }, + ) + if (amountDefault != null) AmountField( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + amount = amount, + currency = currency, + currencies = currencies, + fixedCurrency = (amountStatus as? AmountFieldStatus.Default)?.currency != null, + onAmountChosen = { a, c -> + amount = a + currency = c + }, + ) + Button( + modifier = Modifier.padding(16.dp), + enabled = defaultSummary == null || !summary.isNullOrBlank(), + onClick = { + when (val res = onCreateAmount(amount, currency)) { + is AmountResult.InsufficientBalance -> onError(R.string.payment_balance_insufficient) + is AmountResult.InvalidAmount -> onError(R.string.receive_amount_invalid) + is AmountResult.Success -> onSubmit(summary, res.amount) + } + }, + ) { + Text(stringResource(R.string.payment_create_order)) + } + } +} + +@Composable +private fun AmountField( + modifier: Modifier = Modifier, + currencies: List, + fixedCurrency: Boolean, + amount: String, + currency: String, + onAmountChosen: (amount: String, currency: String) -> Unit, +) { + Row( + modifier = modifier, + ) { + AmountInputField( + modifier = Modifier + .padding(end = 16.dp) + .weight(1f), + value = amount, + onValueChange = { onAmountChosen(it, currency) }, + label = { Text(stringResource(R.string.send_amount)) } + ) + CurrencyDropdown( + modifier = Modifier.weight(1f), + initialCurrency = currency, + currencies = currencies, + onCurrencyChanged = { onAmountChosen(amount, it) }, + readOnly = fixedCurrency, + ) + } +} + +@Preview +@Composable +fun PayTemplateDefaultPreview() { + TalerSurface { + PayTemplateOrderComposable( + defaultSummary = "Donation", + amountStatus = AmountFieldStatus.Default("20", "ARS"), + currencies = listOf("KUDOS", "ARS"), + onCreateAmount = { text, currency -> + AmountResult.Success(amount = Amount.fromString(currency, text)) + }, + onSubmit = { _, _ -> }, + onError = { }, + ) + } +} + +@Preview +@Composable +fun PayTemplateFixedAmountPreview() { + TalerSurface { + PayTemplateOrderComposable( + defaultSummary = "default summary", + amountStatus = AmountFieldStatus.FixedAmount, + currencies = listOf("KUDOS", "ARS"), + onCreateAmount = { text, currency -> + AmountResult.Success(amount = Amount.fromString(currency, text)) + }, + onSubmit = { _, _ -> }, + onError = { }, + ) + } +} + +@Preview +@Composable +fun PayTemplateBlankSubjectPreview() { + TalerSurface { + PayTemplateOrderComposable( + defaultSummary = "", + amountStatus = AmountFieldStatus.FixedAmount, + currencies = listOf("KUDOS", "ARS"), + onCreateAmount = { text, currency -> + AmountResult.Success(amount = Amount.fromString(currency, text)) + }, + onSubmit = { _, _ -> }, + onError = { }, + ) + } +} diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt index 627c05d..3a3069c 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -79,6 +79,7 @@ class PaymentManager( response.contractTerms, response.amountRaw ) + is AlreadyConfirmedResponse -> AlreadyPaid } } @@ -103,22 +104,24 @@ class PaymentManager( resetPayStatus() } - fun preparePayForTemplate(url: String, params: Map) = scope.launch { + fun preparePayForTemplate(url: String, summary: String?, amount: Amount?) = scope.launch { mPayStatus.value = PayStatus.Loading api.request("preparePayForTemplate", PreparePayResponse.serializer()) { put("talerPayTemplateUri", url) put("templateParams", JSONObject().apply { - params.forEach { put(it.key, it.value) } + summary?.let { put("summary", it) } + amount?.let { put("amount", it.toJSONString()) } }) }.onError { handleError("preparePayForTemplate", it) - }.onSuccess { response -> + }.onSuccess { response -> mPayStatus.value = when (response) { is PaymentPossibleResponse -> response.toPayStatusPrepared() is InsufficientBalanceResponse -> InsufficientBalance( contractTerms = response.contractTerms, amountRaw = response.amountRaw, ) + is AlreadyConfirmedResponse -> AlreadyPaid } } -- cgit v1.2.3