commit 7c55f7a84115f0f5333af84165d47c50dfd3c614
parent 0e4ae9635d12ca36a791cf54d9686fb07047e769
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 22 Jul 2026 23:41:18 +0200
[wallet] template fixes and improvements
Diffstat:
6 files changed, 75 insertions(+), 115 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
@@ -16,22 +16,22 @@
package net.taler.wallet.payment
-import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment.Companion.Center
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
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.common.CurrencySpecification
-import net.taler.common.Merchant
import net.taler.wallet.main.AmountResult
import net.taler.wallet.R
import net.taler.wallet.compose.LoadingScreen
@@ -46,8 +46,8 @@ fun PayTemplateComposable(
onCreateAmount: (String, String) -> AmountResult,
onSubmit: (params: TemplateParams) -> Unit,
onError: (msg: String) -> Unit,
+ onRetry: (() -> Unit)? = null,
) {
- // If wallet is empty, there's no way the user can pay something
if (currencies.isEmpty()) {
PayTemplateError(stringResource(R.string.payment_balance_insufficient))
} else when (val p = payStatus) {
@@ -69,39 +69,37 @@ fun PayTemplateComposable(
)
}
}
-
is PayStatus.None, is PayStatus.Loading -> PayTemplateLoading()
- is PayStatus.AlreadyPaid -> PayTemplateError(stringResource(R.string.payment_already_paid))
- is PayStatus.InsufficientBalance -> {
- var errorMsg = stringResource(R.string.payment_balance_insufficient)
- p.balanceDetails.causeHint?.stringResId()?.let {
- errorMsg += "\n\n"
- errorMsg += stringResource(it)
- }
- PayTemplateError(errorMsg)
- }
- is PayStatus.Pending -> {
- val error = p.error
- PayTemplateError(if (error != null) {
- stringResource(R.string.payment_error, error.userFacingMsg)
+ is PayStatus.Pending -> PayTemplateError(
+ message = if (p.error != null) {
+ stringResource(R.string.payment_error, p.error.userFacingMsg)
} else {
stringResource(R.string.payment_template_error)
- })
- }
- is PayStatus.Prepared -> {} // handled in fragment, will redirect
- is PayStatus.Success -> {} // handled by other UI flow, no need for content here
- is PayStatus.Choices -> {} // only applies to regular payments
+ },
+ onRetry = onRetry,
+ )
+ is PayStatus.Prepared -> PayTemplateLoading()
+
+ // not emitted by template flow
+ is PayStatus.AlreadyPaid,
+ is PayStatus.InsufficientBalance,
+ is PayStatus.Success,
+ is PayStatus.Choices -> {}
}
}
@Composable
-fun PayTemplateError(message: String) {
- Box(
+fun PayTemplateError(
+ message: String,
+ onRetry: (() -> Unit)? = null,
+) {
+ Column(
modifier = Modifier
.padding(16.dp)
.fillMaxSize()
.systemBarsPaddingBottom(),
- contentAlignment = Center,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
) {
Text(
text = message,
@@ -109,6 +107,15 @@ fun PayTemplateError(message: String) {
color = MaterialTheme.colorScheme.error,
textAlign = TextAlign.Center,
)
+
+ if (onRetry != null) {
+ Button(
+ modifier = Modifier.padding(top = 24.dp),
+ onClick = onRetry,
+ ) {
+ Text(stringResource(R.string.transactions_retry))
+ }
+ }
}
}
@@ -136,65 +143,6 @@ fun PayTemplateLoadingPreview() {
@Preview
@Composable
-fun PayTemplateInsufficientBalancePreview() {
- TalerSurface {
- PayTemplateComposable(
- payStatus = PayStatus.InsufficientBalance(
- "txn:3409F039F09",
- ContractTerms.V0(
- "test",
- amount = Amount.zero("TESTKUDOS"),
- products = emptyList(),
- orderId = "xxxxx",
- merchantBaseUrl = "https://backend.test.taler.net/",
- merchant = Merchant(name = "Test Backend"),
- maxFee = Amount.zero("TESTKUDOS")
- ),
- Amount.zero("TESTKUDOS"),
- PaymentInsufficientBalanceDetails(
- amountRequested = Amount.fromJSONString("TESTKUDOS:1"),
- causeHint = InsufficientBalanceHint.MerchantDepositInsufficient,
- balanceAvailable = Amount.fromJSONString("TESTKUDOS:1"),
- balanceMaterial = Amount.fromJSONString("TESTKUDOS:1"),
- balanceAgeAcceptable = Amount.fromJSONString("TESTKUDOS:1"),
- balanceReceiverAcceptable = Amount.fromJSONString("TESTKUDOS:0"),
- balanceReceiverDepositable = Amount.fromJSONString("TESTKUDOS:0"),
- balanceExchangeDepositable = Amount.fromJSONString("TESTKUDOS:1"),
- maxEffectiveSpendAmount = Amount.fromJSONString("TESTKUDOS:1"),
- perExchange = emptyMap(),
- )
- ),
- currencies = listOf("KUDOS", "ARS"),
- onCreateAmount = { text, currency ->
- AmountResult.Success(amount = Amount.fromString(currency, text))
- },
- onSubmit = { _ -> },
- onError = { _ -> },
- getCurrencySpec = { null },
- )
- }
-}
-
-@Preview(widthDp = 300)
-@Composable
-fun PayTemplateAlreadyPaidPreview() {
- TalerSurface {
- PayTemplateComposable(
- payStatus = PayStatus.AlreadyPaid(transactionId = "transactionId"),
- currencies = listOf("KUDOS", "ARS"),
- onCreateAmount = { text, currency ->
- AmountResult.Success(amount = Amount.fromString(currency, text))
- },
- onSubmit = { _ -> },
- onError = { _ -> },
- getCurrencySpec = { null },
- )
- }
-}
-
-
-@Preview
-@Composable
fun PayTemplateNoCurrenciesPreview() {
TalerSurface {
PayTemplateComposable(
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt
@@ -117,10 +117,9 @@ class WalletTemplateDetails(
|| 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,
+ amount = if (isAmountEditable()) null else templateContract.amount,
+ summary = if (isSummaryEditable()) null else templateContract.summary,
)
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt
@@ -143,6 +143,27 @@ val defaultTemplateDetails = WalletTemplateDetails(
),
)
+val fixedAmountDetails = WalletTemplateDetails(
+ templateContract = TemplateContractDetails(
+ templateType = TemplateType.FixedOrder,
+ summary = "Pizza Margherita",
+ amount = Amount.fromJSONString("KUDOS:10.0"),
+ minimumAge = 18,
+ payDuration = RelativeTime.forever(),
+ ),
+)
+
+val blankSubjectDetails = WalletTemplateDetails(
+ templateContract = TemplateContractDetails(
+ templateType = TemplateType.FixedOrder,
+ minimumAge = 18,
+ payDuration = RelativeTime.forever(),
+ ),
+ editableDefaults = TemplateContractDetailsDefaults(
+ amount = Amount.fromJSONString("KUDOS:10.0"),
+ ),
+)
+
@Preview
@Composable
fun PayTemplateDefaultPreview() {
@@ -165,7 +186,7 @@ fun PayTemplateDefaultPreview() {
fun PayTemplateFixedAmountPreview() {
TalerSurface {
PayTemplateOrderComposable(
- templateDetails = defaultTemplateDetails,
+ templateDetails = fixedAmountDetails,
usableCurrencies = listOf("KUDOS", "ARS"),
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
@@ -182,7 +203,7 @@ fun PayTemplateFixedAmountPreview() {
fun PayTemplateBlankSubjectPreview() {
TalerSurface {
PayTemplateOrderComposable(
- templateDetails = defaultTemplateDetails,
+ templateDetails = blankSubjectDetails,
usableCurrencies = listOf("KUDOS", "ARS"),
onCreateAmount = { text, currency ->
AmountResult.Success(amount = Amount.fromString(currency, text))
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt
@@ -24,10 +24,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.lifecycle.asFlow
import kotlinx.coroutines.launch
import net.taler.wallet.NavigateCallback
import net.taler.wallet.R
@@ -38,7 +40,6 @@ import net.taler.wallet.compose.ErrorComposable
import net.taler.wallet.compose.GlobalScaffold
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
-import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.main.MainViewModel
@Composable
@@ -54,7 +55,7 @@ fun PayTemplateScreen(
val exchangeManager = model.exchangeManager
val transactionManager = model.transactionManager
- val payStatus by paymentManager.payStatus.asFlow().collectAsStateLifecycleAware(PayStatus.None)
+ val payStatus by paymentManager.payStatus.observeAsState(PayStatus.None)
val balanceState by balanceManager.state.observeAsState(BalanceState.None)
val devMode by model.devMode.observeAsState(false)
val scope = rememberCoroutineScope()
@@ -68,12 +69,14 @@ fun PayTemplateScreen(
}
}
- LaunchedEffect(Unit) {
+ var retryTrigger by remember { mutableIntStateOf(0) }
+
+ LaunchedEffect(retryTrigger) {
balanceManager.loadAssets()
paymentManager.checkPayForTemplate(uri)
}
- LaunchedEffect(payStatus) {
+ LaunchedEffect(payStatus, retryTrigger) {
when (val s = payStatus) {
is PayStatus.Prepared -> {
if (transactionManager.selectTransaction(s.transactionId)) {
@@ -119,6 +122,7 @@ fun PayTemplateScreen(
onError = { errorMsg ->
onShowError(TalerErrorInfo.makeCustomError(errorMsg))
},
+ onRetry = { retryTrigger++ },
getCurrencySpec = exchangeManager::getSpecForCurrency,
)
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -36,11 +36,6 @@ import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.donau.DonauInfo
import net.taler.wallet.donau.GetDonauResponse
import net.taler.wallet.exchanges.ExchangeManager
-import net.taler.wallet.payment.PayStatus.AlreadyPaid
-import net.taler.wallet.payment.PayStatus.InsufficientBalance
-import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse
-import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse
-import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse
import org.json.JSONObject
import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail
import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail.PaymentPossible
@@ -50,7 +45,6 @@ sealed class PayStatus {
data object Loading : PayStatus()
data class Prepared(
val transactionId: String,
- val contractTerms: ContractTerms,
) : PayStatus()
data class Choices(
@@ -258,7 +252,11 @@ class PaymentManager(
handleError("checkPayForTemplate", it)
}.onSuccess { response ->
if (response.templateDetails.templateContract.templateType == TemplateType.Paivana) {
- scope.launch { preparePayForTemplate(url, TemplateParams()) }
+ scope.launch {
+ preparePayForTemplate(url, TemplateParams())?.let { transactionId ->
+ mPayStatus.value = PayStatus.Prepared(transactionId = transactionId)
+ }
+ }
} else {
mPayStatus.value = PayStatus.Checked(
details = response.templateDetails,
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt
@@ -45,12 +45,7 @@ sealed class PreparePayResponse {
data class PaymentPossibleResponse(
val transactionId: String,
val contractTerms: ContractTerms,
- ) : PreparePayResponse() {
- fun toPayStatusPrepared() = PayStatus.Prepared(
- contractTerms = contractTerms,
- transactionId = transactionId,
- )
- }
+ ) : PreparePayResponse()
@Serializable
@SerialName("insufficient-balance")
@@ -79,12 +74,7 @@ sealed class PreparePayResponse {
data class ChoiceSelection(
val transactionId: String,
val contractTerms: ContractTerms,
- ) : PreparePayResponse() {
- fun toPayStatusPrepared() = PayStatus.Prepared(
- contractTerms = contractTerms,
- transactionId = transactionId,
- )
- }
+ ) : PreparePayResponse()
}
@Serializable