diff options
Diffstat (limited to 'wallet/src/main/java/net/taler/wallet/payment')
7 files changed, 267 insertions, 107 deletions
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 ffa4875..d744183 100644 --- 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,37 @@ 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.receive_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( - currencies = currencies, - defaultSummary = defaultSummary, - amountStatus = amountStatus, - onCreateAmount = onCreateAmount, - onError = onError, - onSubmit = onSubmit, - ) + is PayStatus.Checked -> { + val usableCurrencies = currencies + .intersect(p.supportedCurrencies.toSet()) + .toList() + if (usableCurrencies.isEmpty()) { + // If user doesn't have any supported currency, they can't pay either + PayTemplateError(stringResource(R.string.payment_balance_insufficient)) + } else { + PayTemplateOrderComposable( + usableCurrencies = usableCurrencies, + 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 +104,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 +120,6 @@ fun PayTemplateLoadingPreview() { fun PayTemplateInsufficientBalancePreview() { TalerSurface { PayTemplateComposable( - defaultSummary = "Donation", - amountStatus = AmountFieldStatus.Default("20", "ARS"), payStatus = PayStatus.InsufficientBalance( ContractTerms( "test", @@ -140,7 +131,7 @@ fun PayTemplateInsufficientBalancePreview() { onCreateAmount = { text, currency -> AmountResult.Success(amount = Amount.fromString(currency, text)) }, - onSubmit = { _, _ -> }, + onSubmit = { _ -> }, onError = { _ -> }, ) } @@ -151,14 +142,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 +159,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 new file mode 100644 index 0000000..f292e20 --- /dev/null +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt @@ -0,0 +1,114 @@ +/* + * 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( + /** + * Human-readable summary for the template. + */ + val summary: String? = null, + + /** + * Required currency for payments to the template. The user may specify + * any amount, but it must be in this currency. This parameter is + * optional and should not be present if "amount" is given. + */ + val currency: String? = null, + + /** + * The price is imposed by the merchant and cannot be changed by the + * customer. This parameter is optional. + */ + val amount: Amount? = null, + + /** + * Minimum age buyer must have (in years). Default is 0. + */ + @SerialName("minimum_age") + val minimumAge: Int, + + /** + * The time the customer need to pay before his order will be deleted. It + * is deleted if the customer did not pay and if the duration is over. + */ + @SerialName("pay_duration") + val payDuration: RelativeTime, +) + +@Serializable +data class TemplateContractDetailsDefaults( + val summary: String? = null, + val currency: String? = null, + val amount: Amount? = null, + @SerialName("minimum_age") + val minimumAge: Int? = null, +) + +@Serializable +class WalletTemplateDetails( + /** + * Hard-coded information about the contract terms for this template. + */ + @SerialName("template_contract") + val templateContract: TemplateContractDetails, + + /** + * Key-value pairs matching a subset of the fields from template_contract + * that are user-editable defaults for this template. + */ + @SerialName("editable_defaults") + val editableDefaults: TemplateContractDetailsDefaults? = null, +) { + val defaultSummary get() = editableDefaults?.summary + ?: templateContract.summary + + val defaultAmount get() = editableDefaults?.amount + ?: templateContract.amount + + val defaultCurrency get() = editableDefaults?.currency + ?: templateContract.currency + + fun isSummaryEditable() = templateContract.summary == null + + fun isAmountEditable() = templateContract.amount == null + + fun isCurrencyEditable(usableCurrencies: List<String>) = isAmountEditable() + && templateContract.currency == null + && usableCurrencies.size > 1 + + fun isTemplateEditable(usableCurrencies: List<String>) = isSummaryEditable() + || isAmountEditable() + || isCurrencyEditable(usableCurrencies) + + // NOTE: it is important to nullify non-editable values! + fun toTemplateParams() = TemplateParams( + amount = if(isAmountEditable()) templateContract.amount else null, + summary = if(isSummaryEditable()) templateContract.summary else null, + ) +} + +@Serializable +data class TemplateParams( + val amount: Amount? = null, + val summary: String? = null, +) 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 4eb2c11..51c0bc0 100644 --- 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 @@ -39,6 +38,7 @@ class PayTemplateFragment : Fragment() { private val model: MainViewModel by activityViewModels() private lateinit var uriString: String private lateinit var uri: Uri + private val currencies by lazy { model.getCurrencies() } override fun onCreateView( inflater: LayoutInflater, @@ -48,10 +48,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 { @@ -59,9 +55,7 @@ class PayTemplateFragment : Fragment() { val payStatus = payStatusFlow.collectAsStateLifecycleAware(initial = PayStatus.None) TalerSurface { PayTemplateComposable( - currencies = model.getCurrencies(), - defaultSummary = defaultSummary, - amountStatus = amountFieldStatus, + currencies = currencies, payStatus = payStatus.value, onCreateAmount = model::createAmount, onSubmit = this@PayTemplateFragment::createOrder, @@ -74,9 +68,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 +80,25 @@ class PayTemplateFragment : Fragment() { showError(payStatus.error) } + is PayStatus.Checked -> { + val usableCurrencies = currencies + .intersect(payStatus.supportedCurrencies.toSet()) + .toList() + if (!payStatus.details.isTemplateEditable(usableCurrencies)) { + createOrder(payStatus.details.toTemplateParams()) + } + } + 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 index d6131c7..2febfbb 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt @@ -24,84 +24,118 @@ import androidx.compose.material3.Button import androidx.compose.material3.OutlinedTextField 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.Companion.End +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalSoftwareKeyboardController 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 import net.taler.wallet.compose.TalerSurface import net.taler.wallet.deposit.CurrencyDropdown +@OptIn(ExperimentalComposeUiApi::class) @Composable fun PayTemplateOrderComposable( - currencies: List<String>, // assumed to have size > 0 - defaultSummary: String? = null, - amountStatus: AmountFieldStatus, + usableCurrencies: List<String>, // non-empty intersection between the stored currencies and the ones supported by the merchant + 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.defaultSummary + val defaultAmount = templateDetails.defaultAmount + val defaultCurrency = templateDetails.defaultCurrency - var summary by remember { mutableStateOf(defaultSummary) } - var currency by remember { mutableStateOf(amountDefault?.currency ?: currencies[0]) } - var amount by remember { mutableStateOf(amountDefault?.amountStr ?: "0") } + val summaryFocusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + + var summary by remember { mutableStateOf(defaultSummary ?: "") } + var currency by remember { mutableStateOf(defaultCurrency ?: usableCurrencies[0]) } + var amount by remember { mutableStateOf(defaultAmount?.amountStr ?: "0") } Column(horizontalAlignment = End) { - if (defaultSummary != null) OutlinedTextField( + OutlinedTextField( modifier = Modifier .padding(horizontal = 16.dp) - .fillMaxWidth(), - value = summary ?: "", - isError = summary.isNullOrBlank(), + .fillMaxWidth() + .focusRequester(summaryFocusRequester) + .onFocusChanged { + if (it.isFocused) { + keyboardController?.show() + } + }, + value = summary, + isError = templateDetails.isSummaryEditable() && summary.isBlank(), onValueChange = { summary = it }, singleLine = true, + readOnly = !templateDetails.isSummaryEditable(), label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) }, ) - if (amountDefault != null) AmountField( + + AmountField( modifier = Modifier .padding(16.dp) .fillMaxWidth(), amount = amount, currency = currency, - currencies = currencies, - fixedCurrency = (amountStatus as? AmountFieldStatus.Default)?.currency != null, + currencies = usableCurrencies, + readOnlyCurrency = !templateDetails.isCurrencyEditable(usableCurrencies), + readOnlyAmount = !templateDetails.isAmountEditable(), onAmountChosen = { a, c -> amount = a currency = c }, ) + Button( modifier = Modifier.padding(16.dp), - enabled = defaultSummary == null || !summary.isNullOrBlank(), + enabled = !templateDetails.isSummaryEditable() || summary.isNotBlank(), 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) + is AmountResult.InvalidAmount -> onError(R.string.amount_invalid) + // NOTE: it is important to nullify non-editable values! + is AmountResult.Success -> onSubmit(TemplateParams( + summary = if (templateDetails.isSummaryEditable()) summary else null, + amount = if(templateDetails.isAmountEditable()) res.amount else null, + )) } }, ) { Text(stringResource(R.string.payment_create_order)) } } + + LaunchedEffect(Unit) { + if (templateDetails.isSummaryEditable() + && templateDetails.defaultSummary == null) { + summaryFocusRequester.requestFocus() + } + } } @Composable private fun AmountField( modifier: Modifier = Modifier, currencies: List<String>, - fixedCurrency: Boolean, amount: String, currency: String, + readOnlyAmount: Boolean = true, + readOnlyCurrency: Boolean = true, onAmountChosen: (amount: String, currency: String) -> Unit, ) { Row( @@ -113,30 +147,42 @@ private fun AmountField( .weight(1f), value = amount, onValueChange = { onAmountChosen(it, currency) }, - label = { Text(stringResource(R.string.send_amount)) } + 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 = Amount.fromJSONString("KUDOS:10.0"), + ), +) + @Preview @Composable fun PayTemplateDefaultPreview() { TalerSurface { PayTemplateOrderComposable( - defaultSummary = "Donation", - amountStatus = AmountFieldStatus.Default("20", "ARS"), - currencies = listOf("KUDOS", "ARS"), + templateDetails = defaultTemplateDetails, + usableCurrencies = listOf("KUDOS", "ARS"), onCreateAmount = { text, currency -> AmountResult.Success(amount = Amount.fromString(currency, text)) }, - onSubmit = { _, _ -> }, + onSubmit = { _ -> }, onError = { }, ) } @@ -147,13 +193,12 @@ fun PayTemplateDefaultPreview() { fun PayTemplateFixedAmountPreview() { TalerSurface { PayTemplateOrderComposable( - defaultSummary = "default summary", - amountStatus = AmountFieldStatus.FixedAmount, - currencies = listOf("KUDOS", "ARS"), + templateDetails = defaultTemplateDetails, + usableCurrencies = listOf("KUDOS", "ARS"), onCreateAmount = { text, currency -> AmountResult.Success(amount = Amount.fromString(currency, text)) }, - onSubmit = { _, _ -> }, + onSubmit = { _ -> }, onError = { }, ) } @@ -164,13 +209,12 @@ fun PayTemplateFixedAmountPreview() { fun PayTemplateBlankSubjectPreview() { TalerSurface { PayTemplateOrderComposable( - defaultSummary = "", - amountStatus = AmountFieldStatus.FixedAmount, - currencies = listOf("KUDOS", "ARS"), + templateDetails = defaultTemplateDetails, + usableCurrencies = 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 index 35cd9e6..647c98c 100644 --- 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,12 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +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 @@ -37,8 +40,8 @@ import org.json.JSONObject val REGEX_PRODUCT_IMAGE = Regex("^data:image/(jpeg|png);base64,([A-Za-z0-9+/=]+)$") sealed class PayStatus { - object None : PayStatus() - object Loading : PayStatus() + data object None : PayStatus() + data object Loading : PayStatus() data class Prepared( val contractTerms: ContractTerms, val transactionId: String, @@ -46,6 +49,11 @@ sealed class PayStatus { val amountEffective: Amount, ) : PayStatus() + data class Checked( + val details: WalletTemplateDetails, + val supportedCurrencies: List<String>, + ) : PayStatus() + data class InsufficientBalance( val contractTerms: ContractTerms, val amountRaw: Amount, @@ -65,6 +73,12 @@ sealed class PayStatus { ) : PayStatus() } +@Serializable +data class CheckPayTemplateResponse( + val templateDetails: WalletTemplateDetails, + val supportedCurrencies: List<String>, +) + class PaymentManager( private val api: WalletBackendApi, private val scope: CoroutineScope, @@ -113,14 +127,25 @@ 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", CheckPayTemplateResponse.serializer()) { + put("talerPayTemplateUri", url) + }.onError { + handleError("checkPayForTemplate", it) + }.onSuccess { response -> + mPayStatus.value = PayStatus.Checked( + details = response.templateDetails, + supportedCurrencies = response.supportedCurrencies, + ) + } + } + + 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 index 31c26a0..1995f9d 100644 --- 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, only used for templates is PayStatus.Prepared -> { showLoading(false) val fees = payStatus.amountEffective - payStatus.amountRaw diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt index 0f6d661..beb37d9 100644 --- a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt +++ b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt @@ -82,10 +82,10 @@ fun TransactionPaymentComposable( amountType = AmountType.Neutral, ) - val fee = t.amountEffective - t.amountRaw - if (!fee.isZero()) { + if (t.amountEffective > t.amountRaw) { + val fee = t.amountEffective - t.amountRaw TransactionAmountComposable( - label = stringResource(id = R.string.withdraw_fees), + label = stringResource(id = R.string.amount_fee), amount = fee.withSpec(spec), amountType = AmountType.Negative, ) |