taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

commit fd2df3498764a256342e8eecf5d39868e20df5b9
parent c0f4ebabdcb0e5b2d167db07af46ebd16257c366
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 14 Nov 2024 21:51:57 +0100

[wallet] QC: show available amount for outgoing push/deposit

Diffstat:
Mwallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt | 39++++++++++++++++++++++++++++++++++++---
Mwallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt | 34++++++++++++++++++++++++----------
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt | 26+++++++++++++++++++++++---
Mwallet/src/main/java/net/taler/wallet/peer/PeerManager.kt | 41++++++++++++++++++++++++++++++++++++++---
Mwallet/src/main/res/values/strings.xml | 2++
5 files changed, 123 insertions(+), 19 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt @@ -55,8 +55,10 @@ class DepositManager( } suspend fun checkDepositFees(paytoUri: String, amount: Amount): CheckDepositResult { - var response: CheckDepositResult = CheckDepositResult.None - + val max = getMaxDepositAmount(amount.currency, paytoUri) + var response: CheckDepositResult = CheckDepositResult.None( + maxDepositAmountEffective = max?.effectiveAmount, + ) api.request("checkDeposit", CheckDepositResponse.serializer()) { put("depositPaytoUri", paytoUri) put("amount", amount.toJSONString()) @@ -67,6 +69,7 @@ class DepositManager( kycSoftLimit = it.kycSoftLimit, kycHardLimit = it.kycHardLimit, kycExchanges = it.kycExchanges, + maxDepositAmountEffective = max?.effectiveAmount, ) }.onError { error -> Log.e(TAG, "Error prepareDeposit $error") @@ -83,6 +86,7 @@ class DepositManager( response = CheckDepositResult.InsufficientBalance( maxAmountEffective = maxAmountEffective, maxAmountRaw = maxAmountRaw, + maxDepositAmountEffective = max?.effectiveAmount, ) } } @@ -91,6 +95,23 @@ class DepositManager( return response } + private suspend fun getMaxDepositAmount( + currency: String, + depositPaytoUri: String?, + ): GetMaxDepositAmountResponse? { + var response: GetMaxDepositAmountResponse? = null + api.request("getMaxDepositAmount", GetMaxDepositAmountResponse.serializer()) { + depositPaytoUri?.let { put("depositPaytoUri", it) } + put("currency", currency) + }.onError { error -> + Log.e(TAG, "Error getMaxDepositAmount $error") + }.onSuccess { + response = it + } + + return response + } + fun makeDeposit(amount: Amount, paytoUri: String) { mDepositState.value = DepositState.MakingDeposit @@ -174,11 +195,16 @@ data class CheckDepositResponse( @Serializable sealed class CheckDepositResult { - data object None: CheckDepositResult() + abstract val maxDepositAmountEffective: Amount? + + data class None( + override val maxDepositAmountEffective: Amount? = null + ): CheckDepositResult() data class InsufficientBalance( val maxAmountEffective: Amount?, val maxAmountRaw: Amount?, + override val maxDepositAmountEffective: Amount? ): CheckDepositResult() data class Success( @@ -187,10 +213,17 @@ sealed class CheckDepositResult { val kycSoftLimit: Amount? = null, val kycHardLimit: Amount? = null, val kycExchanges: List<String>? = null, + override val maxDepositAmountEffective: Amount? ): CheckDepositResult() } @Serializable +data class GetMaxDepositAmountResponse( + val effectiveAmount: Amount, + val rawAmount: Amount, +) + +@Serializable data class CreateDepositGroupResponse( val depositGroupId: String, val transactionId: String, diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt @@ -84,7 +84,7 @@ fun MakeDepositComposable( ) { // Amount/currency stuff // TODO: use scopeInfo instead of currency! - var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None) } + var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None()) } var amount by remember { mutableStateOf(Amount.zero(defaultCurrency ?: currencies[0])) } val currencySpec = remember (amount) { getCurrencySpec(amount.currency) } @@ -124,19 +124,13 @@ fun MakeDepositComposable( } amount.useDebounce { - if (paytoUri != null) { + if (paytoUri != null && !formError) { checkResult = checkDeposit(amount, paytoUri) } } paytoUri.useDebounce { - if (paytoUri != null) { - checkResult = checkDeposit(amount, paytoUri) - } - } - - LaunchedEffect(Unit) { - if (paytoUri != null) { + if (paytoUri != null && !formError) { checkResult = checkDeposit(amount, paytoUri) } } @@ -197,6 +191,22 @@ fun MakeDepositComposable( else -> {} } + AnimatedVisibility(checkResult.maxDepositAmountEffective != null) { + checkResult.maxDepositAmountEffective?.let { + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + text = stringResource( + R.string.send_deposit_max_amount, + it.withSpec(currencySpec), + ), + ) + } + } + AmountCurrencyField( modifier = Modifier .padding(horizontal = 16.dp) @@ -211,7 +221,10 @@ fun MakeDepositComposable( supportingText = { val res = checkResult if (res is CheckDepositResult.InsufficientBalance && res.maxAmountEffective != null) { - Text(stringResource(R.string.payment_balance_insufficient_max, res.maxAmountEffective)) + Text(stringResource( + R.string.payment_balance_insufficient_max, + res.maxAmountEffective.withSpec(currencySpec), + )) } } ) @@ -335,6 +348,7 @@ fun PreviewMakeDepositComposable() { checkDeposit = { _, _ -> CheckDepositResult.Success( totalDepositCost = Amount.fromJSONString("KUDOS:10"), effectiveDepositAmount = Amount.fromJSONString("KUDOS:12"), + maxDepositAmountEffective = Amount.fromJSONString("KUDOS:12") ) }, validateIban = { true }, onMakeDeposit = { _, _ -> }, diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt @@ -16,6 +16,7 @@ package net.taler.wallet.peer +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -97,14 +98,30 @@ fun OutgoingPushIntroComposable( ) { var amount by remember { mutableStateOf(Amount.zero(defaultCurrency ?: currencies[0])) } val selectedSpec = remember(amount.currency) { getCurrencySpec(amount.currency) } - var feeResult by remember { mutableStateOf<CheckFeeResult>(None) } + var feeResult by remember { mutableStateOf<CheckFeeResult>(None()) } amount.useDebounce { - feeResult = getFees(it) ?: None + feeResult = getFees(it) ?: None() } LaunchedEffect(Unit) { - feeResult = getFees(amount) ?: None + feeResult = getFees(amount) ?: None() + } + + AnimatedVisibility(feeResult.maxDepositAmountEffective != null) { + feeResult.maxDepositAmountEffective?.let { + Text( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + text = stringResource( + R.string.send_peer_max_amount, + it.withSpec(selectedSpec), + ), + ) + } } AmountCurrencyField( @@ -226,6 +243,7 @@ fun PeerPushComposableCheckingPreview() { getFees = { Success( amountEffective = Amount.fromJSONString("KUDOS:10"), amountRaw = Amount.fromJSONString("KUDOS:12"), + maxDepositAmountEffective = Amount.fromJSONString("KUDOS:12"), ) }, onSend = { _, _, _ -> }, onClose = {}, @@ -248,6 +266,7 @@ fun PeerPushComposableCheckedPreview() { getFees = { Success( amountEffective = Amount.fromJSONString("KUDOS:10"), amountRaw = Amount.fromJSONString("KUDOS:12"), + maxDepositAmountEffective = Amount.fromJSONString("KUDOS:12"), ) }, onSend = { _, _, _ -> }, onClose = {}, @@ -269,6 +288,7 @@ fun PeerPushComposableErrorPreview() { getFees = { Success( amountEffective = Amount.fromJSONString("KUDOS:10"), amountRaw = Amount.fromJSONString("KUDOS:12"), + maxDepositAmountEffective = Amount.fromJSONString("KUDOS:12"), ) }, onSend = { _, _, _ -> }, onClose = {}, diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -24,6 +24,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject @@ -47,19 +48,32 @@ const val MAX_LENGTH_SUBJECT = 100 val DEFAULT_EXPIRY = ExpirationOption.DAYS_1 sealed class CheckFeeResult { - data object None: CheckFeeResult() + abstract val maxDepositAmountEffective: Amount? + + data class None( + override val maxDepositAmountEffective: Amount? = null, + ): CheckFeeResult() data class InsufficientBalance( val maxAmountEffective: Amount?, val maxAmountRaw: Amount?, + override val maxDepositAmountEffective: Amount? = null, ): CheckFeeResult() data class Success( val amountRaw: Amount, val amountEffective: Amount, + override val maxDepositAmountEffective: Amount? = null, ): CheckFeeResult() } +@Serializable +data class GetMaxPeerPushDebitAmountResponse( + val effectiveAmount: Amount, + val rawAmount: Amount, + val exchangeBaseUrl: String? = null, +) + class PeerManager( private val api: WalletBackendApi, private val exchangeManager: ExchangeManager, @@ -129,8 +143,8 @@ class PeerManager( } suspend fun checkPeerPushFees(amount: Amount, exchangeBaseUrl: String? = null): CheckFeeResult { - var response: CheckFeeResult = CheckFeeResult.None - + val max = getMaxPeerPushDebitAmount(amount.currency, exchangeBaseUrl) + var response: CheckFeeResult = CheckFeeResult.None(maxDepositAmountEffective = max?.effectiveAmount) api.request("checkPeerPushDebit", CheckPeerPushDebitResponse.serializer()) { exchangeBaseUrl?.let { put("exchangeBaseUrl", it) } put("amount", amount.toJSONString()) @@ -138,6 +152,7 @@ class PeerManager( response = CheckFeeResult.Success( amountRaw = it.amountRaw, amountEffective = it.amountEffective, + maxDepositAmountEffective = max?.effectiveAmount, ) }.onError { error -> Log.e(TAG, "got checkPeerPushDebit error result $error") @@ -154,6 +169,7 @@ class PeerManager( response = CheckFeeResult.InsufficientBalance( maxAmountEffective = maxAmountEffective, maxAmountRaw = maxAmountRaw, + maxDepositAmountEffective = max?.effectiveAmount, ) } } @@ -162,6 +178,25 @@ class PeerManager( return response } + private suspend fun getMaxPeerPushDebitAmount( + currency: String, + exchangeBaseUrl: String? = null, + restrictScope: ScopeInfo? = null, + ): GetMaxPeerPushDebitAmountResponse? { + var response: GetMaxPeerPushDebitAmountResponse? = null + api.request("getMaxPeerPushDebitAmount", GetMaxPeerPushDebitAmountResponse.serializer()) { + exchangeBaseUrl?.let { put("exchangeBaseUrl", it) } + restrictScope?.let { put("restrictScope", it) } + put("currency", currency) + }.onError { error -> + Log.e(TAG, "got getMaxPeerPushDebitAmount error result $error") + }.onSuccess { + response = it + } + + return response + } + fun initiatePeerPushDebit(amount: Amount, summary: String, expirationHours: Long) { _outgoingPushState.value = OutgoingCreating scope.launch(Dispatchers.IO) { diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -213,6 +213,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="pay_peer_intro">Do you want to pay this request?</string> <string name="pay_peer_title">Pay request</string> <string name="send_deposit_account">Account</string> + <string name="send_deposit_max_amount">Available to deposit: %1$s</string> <string name="send_deposit_bitcoin_address">Bitcoin address</string> <string name="send_deposit_bitcoin_create_button">Transfer Bitcoin</string> <string name="send_deposit_button_label">Deposit</string> @@ -233,6 +234,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card <string name="send_peer_expiration_days">Days</string> <string name="send_peer_expiration_hours">Hours</string> <string name="send_peer_expiration_period">Expires in</string> + <string name="send_peer_max_amount">Available to send: %1$s</string> <string name="send_peer_payment_instruction">To send %1$s, let the payee scan this QR code:</string> <string name="send_peer_purpose">Purpose</string> <string name="send_peer_title">Send money to another wallet</string>