commit 374d9f1ac6908d63a3da2b22818a876d0e16575d
parent df2c70169a6dab48ee5b684588f698034fa01366
Author: Iván Ávalos <avalos@disroot.org>
Date: Thu, 16 May 2024 19:44:28 -0600
[wallet] implement `checkPayForTemplate' request
bug 0008854
Diffstat:
7 files changed, 163 insertions(+), 85 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt
@@ -53,6 +53,7 @@ fun AmountInputField(
keyboardActions: KeyboardActions = KeyboardActions.Default,
decimalFormatSymbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols,
numberOfDecimals: Int = DEFAULT_INPUT_DECIMALS,
+ readOnly: Boolean = false,
) {
var amountInput by remember { mutableStateOf(value) }
@@ -77,6 +78,7 @@ fun AmountInputField(
}
},
modifier = modifier,
+ readOnly = readOnly,
textStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace),
label = label,
supportingText = supportingText,
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt
@@ -34,42 +34,27 @@ import net.taler.wallet.R
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
-sealed class AmountFieldStatus {
- object FixedAmount : AmountFieldStatus()
- class Default(
- val amountStr: String? = null,
- val currency: String? = null,
- ) : AmountFieldStatus()
-
- object Invalid : AmountFieldStatus()
-}
-
@Composable
fun PayTemplateComposable(
- defaultSummary: String?,
- amountStatus: AmountFieldStatus,
currencies: List<String>,
payStatus: PayStatus,
onCreateAmount: (String, String) -> AmountResult,
- onSubmit: (summary: String?, amount: Amount?) -> Unit,
+ onSubmit: (params: TemplateParams) -> 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.amount_invalid))
- } else if (currencies.isEmpty()) {
+ if (currencies.isEmpty()) {
PayTemplateError(stringResource(R.string.payment_balance_insufficient))
} else when (val p = payStatus) {
- is PayStatus.None -> PayTemplateOrderComposable(
+ is PayStatus.Checked -> PayTemplateOrderComposable(
currencies = currencies,
- defaultSummary = defaultSummary,
- amountStatus = amountStatus,
+ templateDetails = p.details,
onCreateAmount = onCreateAmount,
onError = onError,
onSubmit = onSubmit,
)
- is PayStatus.Loading -> PayTemplateLoading()
+ is PayStatus.None, is PayStatus.Loading -> PayTemplateLoading()
is PayStatus.AlreadyPaid -> PayTemplateError(stringResource(R.string.payment_already_paid))
is PayStatus.InsufficientBalance -> PayTemplateError(stringResource(R.string.payment_balance_insufficient))
is PayStatus.Pending -> {
@@ -109,14 +94,12 @@ fun PayTemplateLoading() {
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 = { _, _ -> },
+ onSubmit = { _ -> },
onError = { _ -> },
)
}
@@ -127,8 +110,6 @@ fun PayTemplateLoadingPreview() {
fun PayTemplateInsufficientBalancePreview() {
TalerSurface {
PayTemplateComposable(
- defaultSummary = "Donation",
- amountStatus = AmountFieldStatus.Default("20", "ARS"),
payStatus = PayStatus.InsufficientBalance(
ContractTerms(
"test",
@@ -140,7 +121,7 @@ fun PayTemplateInsufficientBalancePreview() {
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
},
- onSubmit = { _, _ -> },
+ onSubmit = { _ -> },
onError = { _ -> },
)
}
@@ -151,14 +132,12 @@ fun PayTemplateInsufficientBalancePreview() {
fun PayTemplateAlreadyPaidPreview() {
TalerSurface {
PayTemplateComposable(
- defaultSummary = "Donation",
- amountStatus = AmountFieldStatus.Default("20", "ARS"),
payStatus = PayStatus.AlreadyPaid(transactionId = "transactionId"),
currencies = listOf("KUDOS", "ARS"),
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
},
- onSubmit = { _, _ -> },
+ onSubmit = { _ -> },
onError = { _ -> },
)
}
@@ -170,14 +149,12 @@ fun PayTemplateAlreadyPaidPreview() {
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 = { _, _ -> },
+ onSubmit = { _ -> },
onError = { _ -> },
)
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt
@@ -0,0 +1,74 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2024 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 <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.payment
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.taler.common.Amount
+import net.taler.common.RelativeTime
+
+@Serializable
+data class TemplateContractDetails(
+ val summary: String? = null,
+ val currency: String? = null,
+ val amount: Amount? = null,
+ @SerialName("minimum_age")
+ val minimumAge: Int,
+ @SerialName("pay_duration")
+ val payDuration: RelativeTime,
+)
+
+@Serializable
+data class TemplateContractDetailsDefaults(
+ val summary: String? = null,
+ val currency: String? = null,
+ /**
+ * Amount *or* a plain currency string.
+ */
+ val amount: String? = null,
+ @SerialName("minimum_age")
+ val minimumAge: Int? = null,
+)
+
+fun TemplateContractDetailsDefaults?.isNullOrEmpty() =
+ this == null || (summary == null
+ && currency == null
+ && amount == null
+ && minimumAge == null)
+
+@Serializable
+class WalletTemplateDetails(
+ @SerialName("template_contract")
+ val templateContract: TemplateContractDetails,
+ @SerialName("editable_defaults")
+ val editableDefaults: TemplateContractDetailsDefaults? = null,
+ @SerialName("required_currency")
+ val requiredCurrency: String? = null,
+)
+
+@Serializable
+data class TemplateParams(
+ val amount: Amount? = null,
+ val summary: String? = null,
+) {
+ companion object {
+ fun fromTemplateDetails(details: WalletTemplateDetails) = TemplateParams(
+ amount = details.templateContract.amount,
+ summary = details.templateContract.summary,
+ )
+ }
+}
+\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt
@@ -26,7 +26,6 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.asFlow
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
@@ -48,10 +47,6 @@ class PayTemplateFragment : Fragment() {
uriString = arguments?.getString("uri") ?: error("no amount passed")
uri = Uri.parse(uriString)
- val defaultSummary = uri.getQueryParameter("summary")
- val defaultAmount = uri.getQueryParameter("amount")
- val amountFieldStatus = getAmountFieldStatus(defaultAmount)
-
val payStatusFlow = model.paymentManager.payStatus.asFlow()
return ComposeView(requireContext()).apply {
@@ -60,8 +55,6 @@ class PayTemplateFragment : Fragment() {
TalerSurface {
PayTemplateComposable(
currencies = model.getCurrencies(),
- defaultSummary = defaultSummary,
- amountStatus = amountFieldStatus,
payStatus = payStatus.value,
onCreateAmount = model::createAmount,
onSubmit = this@PayTemplateFragment::createOrder,
@@ -74,9 +67,7 @@ class PayTemplateFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- if (uri.queryParameterNames?.isEmpty() == true) {
- createOrder(null, null)
- }
+ checkTemplate()
model.paymentManager.payStatus.observe(viewLifecycleOwner) { payStatus ->
when (payStatus) {
@@ -88,28 +79,20 @@ class PayTemplateFragment : Fragment() {
showError(payStatus.error)
}
+ is PayStatus.Checked -> if (payStatus.details.editableDefaults.isNullOrEmpty()) {
+ createOrder(TemplateParams.fromTemplateDetails(payStatus.details))
+ }
+
else -> {}
}
}
}
- 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 checkTemplate() {
+ model.paymentManager.checkPayForTemplate(uriString)
}
- private fun createOrder(summary: String?, amount: Amount?) {
- model.paymentManager.preparePayForTemplate(uriString, summary, amount)
+ private fun createOrder(params: TemplateParams) {
+ model.paymentManager.preparePayForTemplate(uriString, params)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
@@ -34,6 +34,7 @@ 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.RelativeTime
import net.taler.wallet.AmountResult
import net.taler.wallet.R
import net.taler.wallet.compose.AmountInputField
@@ -43,17 +44,24 @@ import net.taler.wallet.deposit.CurrencyDropdown
@Composable
fun PayTemplateOrderComposable(
currencies: List<String>, // assumed to have size > 0
- defaultSummary: String? = null,
- amountStatus: AmountFieldStatus,
+ templateDetails: WalletTemplateDetails,
onCreateAmount: (String, String) -> AmountResult,
onError: (msgRes: Int) -> Unit,
- onSubmit: (summary: String?, amount: Amount?) -> Unit,
+ onSubmit: (params: TemplateParams) -> Unit,
) {
- val amountDefault = amountStatus as? AmountFieldStatus.Default
+ val defaultSummary = templateDetails.editableDefaults?.summary
+ ?: templateDetails.templateContract.summary
+ // TODO: also handle “plain currency string”
+ val defaultAmount = templateDetails.editableDefaults?.amount?.let {
+ Amount.fromJSONString(it).amountStr
+ } ?: templateDetails.templateContract.amount?.amountStr
+ // TODO: also take into account `requiredCurrency'
+ val defaultCurrency = templateDetails.editableDefaults?.currency
+ ?: templateDetails.templateContract.currency
var summary by remember { mutableStateOf(defaultSummary) }
- var currency by remember { mutableStateOf(amountDefault?.currency ?: currencies[0]) }
- var amount by remember { mutableStateOf(amountDefault?.amountStr ?: "0") }
+ var currency by remember { mutableStateOf(defaultCurrency ?: currencies[0]) }
+ var amount by remember { mutableStateOf(defaultAmount ?: "0") }
Column(horizontalAlignment = End) {
if (defaultSummary != null) OutlinedTextField(
@@ -64,29 +72,36 @@ fun PayTemplateOrderComposable(
isError = summary.isNullOrBlank(),
onValueChange = { summary = it },
singleLine = true,
+ readOnly = templateDetails.editableDefaults?.summary == null,
label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) },
)
- if (amountDefault != null) AmountField(
+
+ if (defaultAmount != null || defaultCurrency != null) AmountField(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
amount = amount,
currency = currency,
currencies = currencies,
- fixedCurrency = (amountStatus as? AmountFieldStatus.Default)?.currency != null,
+ readOnlyCurrency = templateDetails.editableDefaults?.currency == null,
+ readOnlyAmount = templateDetails.editableDefaults?.amount == null,
onAmountChosen = { a, c ->
amount = a
currency = c
},
)
+
Button(
modifier = Modifier.padding(16.dp),
- enabled = defaultSummary == null || !summary.isNullOrBlank(),
+ enabled = templateDetails.editableDefaults?.summary == 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.amount_invalid)
- is AmountResult.Success -> onSubmit(summary, res.amount)
+ is AmountResult.Success -> onSubmit(TemplateParams(
+ summary = summary,
+ amount = res.amount,
+ ))
}
},
) {
@@ -99,9 +114,10 @@ fun PayTemplateOrderComposable(
private fun AmountField(
modifier: Modifier = Modifier,
currencies: List<String>,
- fixedCurrency: Boolean,
amount: String,
currency: String,
+ readOnlyAmount: Boolean = false,
+ readOnlyCurrency: Boolean = false,
onAmountChosen: (amount: String, currency: String) -> Unit,
) {
Row(
@@ -113,30 +129,42 @@ private fun AmountField(
.weight(1f),
value = amount,
onValueChange = { onAmountChosen(it, currency) },
- label = { Text(stringResource(R.string.amount_send)) }
+ label = { Text(stringResource(R.string.amount_send)) },
+ readOnly = readOnlyAmount,
)
+
CurrencyDropdown(
modifier = Modifier.weight(1f),
initialCurrency = currency,
currencies = currencies,
onCurrencyChanged = { onAmountChosen(amount, it) },
- readOnly = fixedCurrency,
+ readOnly = readOnlyCurrency,
)
}
}
+val defaultTemplateDetails = WalletTemplateDetails(
+ templateContract = TemplateContractDetails(
+ minimumAge = 18,
+ payDuration = RelativeTime.forever(),
+ ),
+ editableDefaults = TemplateContractDetailsDefaults(
+ summary = "Donation",
+ amount = "KUDOS:10.0",
+ ),
+)
+
@Preview
@Composable
fun PayTemplateDefaultPreview() {
TalerSurface {
PayTemplateOrderComposable(
- defaultSummary = "Donation",
- amountStatus = AmountFieldStatus.Default("20", "ARS"),
+ templateDetails = defaultTemplateDetails,
currencies = listOf("KUDOS", "ARS"),
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
},
- onSubmit = { _, _ -> },
+ onSubmit = { _ -> },
onError = { },
)
}
@@ -147,13 +175,12 @@ fun PayTemplateDefaultPreview() {
fun PayTemplateFixedAmountPreview() {
TalerSurface {
PayTemplateOrderComposable(
- defaultSummary = "default summary",
- amountStatus = AmountFieldStatus.FixedAmount,
+ templateDetails = defaultTemplateDetails,
currencies = listOf("KUDOS", "ARS"),
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
},
- onSubmit = { _, _ -> },
+ onSubmit = { _ -> },
onError = { },
)
}
@@ -164,13 +191,12 @@ fun PayTemplateFixedAmountPreview() {
fun PayTemplateBlankSubjectPreview() {
TalerSurface {
PayTemplateOrderComposable(
- defaultSummary = "",
- amountStatus = AmountFieldStatus.FixedAmount,
+ templateDetails = defaultTemplateDetails,
currencies = listOf("KUDOS", "ARS"),
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
},
- onSubmit = { _, _ -> },
+ 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
@@ -22,9 +22,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import kotlinx.serialization.encodeToString
import net.taler.common.Amount
import net.taler.common.ContractTerms
import net.taler.wallet.TAG
+import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.payment.PayStatus.AlreadyPaid
@@ -46,6 +48,10 @@ sealed class PayStatus {
val amountEffective: Amount,
) : PayStatus()
+ data class Checked(
+ val details: WalletTemplateDetails,
+ ) : PayStatus()
+
data class InsufficientBalance(
val contractTerms: ContractTerms,
val amountRaw: Amount,
@@ -113,14 +119,22 @@ class PaymentManager(
}
}
- fun preparePayForTemplate(url: String, summary: String?, amount: Amount?) = scope.launch {
+ fun checkPayForTemplate(url: String) = scope.launch {
+ mPayStatus.value = PayStatus.Loading
+ api.request("checkPayForTemplate", WalletTemplateDetails.serializer()) {
+ put("talerPayTemplateUri", url)
+ }.onError {
+ handleError("checkPayForTemplate", it)
+ }.onSuccess { response ->
+ mPayStatus.value = PayStatus.Checked(response)
+ }
+ }
+
+ fun preparePayForTemplate(url: String, params: TemplateParams) = scope.launch {
mPayStatus.value = PayStatus.Loading
api.request("preparePayForTemplate", PreparePayResponse.serializer()) {
put("talerPayTemplateUri", url)
- put("templateParams", JSONObject().apply {
- summary?.let { put("summary", it) }
- amount?.let { put("amount", it.toJSONString()) }
- })
+ put("templateParams", JSONObject(BackendManager.json.encodeToString(params)))
}.onError {
handleError("preparePayForTemplate", it)
}.onSuccess { response ->
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PromptPaymentFragment.kt
@@ -98,6 +98,7 @@ class PromptPaymentFragment : Fragment(), ProductImageClickListener {
private fun onPaymentStatusChanged(payStatus: PayStatus?) {
when (payStatus) {
null -> {}
+ is PayStatus.Checked -> {} // does not apply
is PayStatus.Prepared -> {
showLoading(false)
val fees = payStatus.amountEffective - payStatus.amountRaw