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:
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>