commit 6d3efa869b6889d3b5d825eb998a7c8beb3c397c
parent 47723af929feec09aec983d5f4ae0f6691fb4be6
Author: Iván Ávalos <avalos@disroot.org>
Date: Thu, 29 May 2025 17:21:18 +0200
[wallet] important ToS review fixes
Diffstat:
9 files changed, 176 insertions(+), 154 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountScopeField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountScopeField.kt
@@ -60,7 +60,8 @@ fun AmountScopeField(
supportingText: @Composable (() -> Unit)? = null,
isError: Boolean = false,
readOnly: Boolean = false,
- enabled: Boolean = true,
+ enabledAmount: Boolean = true,
+ enabledScope: Boolean = true,
showShortcuts: Boolean = false,
onShortcutSelected: ((amount: AmountScope) -> Unit)? = null,
) {
@@ -78,41 +79,42 @@ fun AmountScopeField(
))
},
initialScope = amount.scope,
- readOnly = readOnly || !enabled,
+ readOnly = readOnly || !enabledScope,
)
}
- AmountInputFieldBase(
- modifier = Modifier
- .fillMaxWidth(),
- amount = amount.amount,
- onAmountChanged = {
- onAmountChanged(amount.copy(amount = it))
- },
- label = label,
- isError = isError,
- supportingText = supportingText,
- readOnly = readOnly,
- enabled = enabled,
- showSymbol = true,
- )
-
- if (showShortcuts) {
- val currency = amount.amount.currency
- AmountInputShortcuts(
- // TODO: currency-appropriate presets
- amounts = listOf(
- Amount.fromString(currency, "50").withSpec(amount.amount.spec),
- Amount.fromString(currency, "25").withSpec(amount.amount.spec),
- Amount.fromString(currency, "10").withSpec(amount.amount.spec),
- Amount.fromString(currency, "5").withSpec(amount.amount.spec),
- ),
- onSelected = { shortcut ->
- onShortcutSelected?.let {
- it(amount.copy(amount = shortcut))
- }
+ if (enabledAmount) {
+ AmountInputFieldBase(
+ modifier = Modifier
+ .fillMaxWidth(),
+ amount = amount.amount,
+ onAmountChanged = {
+ onAmountChanged(amount.copy(amount = it))
},
+ label = label,
+ isError = isError,
+ supportingText = supportingText,
+ readOnly = readOnly,
+ showSymbol = true,
)
+
+ if (showShortcuts) {
+ val currency = amount.amount.currency
+ AmountInputShortcuts(
+ // TODO: currency-appropriate presets
+ amounts = listOf(
+ Amount.fromString(currency, "50").withSpec(amount.amount.spec),
+ Amount.fromString(currency, "25").withSpec(amount.amount.spec),
+ Amount.fromString(currency, "10").withSpec(amount.amount.spec),
+ Amount.fromString(currency, "5").withSpec(amount.amount.spec),
+ ),
+ onSelected = { shortcut ->
+ onShortcutSelected?.let {
+ it(amount.copy(amount = shortcut))
+ }
+ },
+ )
+ }
}
}
}
@@ -234,7 +236,6 @@ fun AmountInputFieldPreview() {
label = { Text("Amount to withdraw") },
isError = false,
readOnly = false,
- enabled = true,
showShortcuts = true,
onShortcutSelected = { amount = it },
)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
@@ -71,48 +71,11 @@ fun OutgoingPullComposable(
defaultScope: ScopeInfo?,
scopes: List<ScopeInfo>,
getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
- checkPeerPullCredit: suspend (amount: AmountScope) -> CheckPeerPullCreditResult?,
+ checkPeerPullCredit: suspend (amount: AmountScope, loading: Boolean) -> CheckPeerPullCreditResult?,
onCreateInvoice: (amount: AmountScope, subject: String, hours: Long, exchangeBaseUrl: String) -> Unit,
onTosAccept: (exchangeBaseUrl: String) -> Unit,
onClose: () -> Unit,
) {
- when(state) {
- is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> PeerCreatingComposable()
- is OutgoingIntro, is OutgoingChecked -> OutgoingPullIntroComposable(
- defaultScope = defaultScope,
- scopes = scopes,
- getCurrencySpec = getCurrencySpec,
- checkPeerPullCredit = checkPeerPullCredit,
- onCreateInvoice = onCreateInvoice,
- onTosAccept = onTosAccept,
- )
- is OutgoingError -> PeerErrorComposable(state, onClose)
- }
-}
-
-@Composable
-fun PeerCreatingComposable() {
- Box(
- modifier = Modifier
- .fillMaxSize(),
- ) {
- CircularProgressIndicator(
- modifier = Modifier
- .padding(32.dp)
- .align(Center),
- )
- }
-}
-
-@Composable
-fun OutgoingPullIntroComposable(
- defaultScope: ScopeInfo?,
- scopes: List<ScopeInfo>,
- getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
- checkPeerPullCredit: suspend (amount: AmountScope) -> CheckPeerPullCreditResult?,
- onCreateInvoice: (amount: AmountScope, subject: String, hours: Long, exchangeBaseUrl: String) -> Unit,
- onTosAccept: (exchangeBaseUrl: String) -> Unit,
-) {
var subject by rememberSaveable { mutableStateOf("") }
var amount by remember {
val scope = defaultScope ?: scopes[0]
@@ -126,12 +89,21 @@ fun OutgoingPullIntroComposable(
var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) }
var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) }
- amount.useDebounce {
- checkResult = checkPeerPullCredit(it)
+ val tosReview = checkResult != null && checkResult?.tosStatus != ExchangeTosStatus.Accepted
+
+ amount.amount.useDebounce {
+ checkResult = checkPeerPullCredit(amount, false)
+ }
+
+ LaunchedEffect(amount.scope) {
+ checkResult = checkPeerPullCredit(amount, true)
}
- LaunchedEffect(Unit) {
- checkResult = checkPeerPullCredit(amount)
+ if (state is OutgoingChecking ||
+ state is OutgoingCreating ||
+ state is OutgoingResponse) {
+ PeerCreatingComposable()
+ return
}
Column(
@@ -153,71 +125,94 @@ fun OutgoingPullIntroComposable(
amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)),
scopes = scopes,
readOnly = false,
+ enabledAmount = !tosReview,
onAmountChanged = { amount = it },
isError = amount.amount.isZero(),
label = { Text(stringResource(R.string.amount_receive)) },
)
- OutlinedTextField(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .fillMaxWidth(),
- singleLine = true,
- value = subject,
- onValueChange = { input ->
- if (input.length <= MAX_LENGTH_SUBJECT)
- subject = input.replace('\n', ' ')
- },
- isError = subject.isBlank(),
- label = {
- Text(
- stringResource(R.string.send_peer_purpose),
- color = if (subject.isBlank()) {
- MaterialTheme.colorScheme.error
- } else Color.Unspecified,
- )
- },
- supportingText = {
- Text(stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT))
- },
- )
+ if (state is OutgoingError) {
+ PeerErrorComposable(state, onClose)
+ return@Column
+ }
- if (res != null) {
- if (res.amountEffective > res.amountRaw) {
- val fee = res.amountEffective - res.amountRaw
- Text(
- modifier = Modifier.padding(vertical = 16.dp),
- text = stringResource(
- id = R.string.payment_fee,
- fee.withSpec(selectedSpec)
- ),
- softWrap = false,
- color = MaterialTheme.colorScheme.error,
- )
+ if (tosReview) {
+ Text(
+ modifier = Modifier.padding(16.dp),
+ text = stringResource(R.string.receive_peer_review_terms)
+ )
+ } else {
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth(),
+ singleLine = true,
+ value = subject,
+ onValueChange = { input ->
+ if (input.length <= MAX_LENGTH_SUBJECT)
+ subject = input.replace('\n', ' ')
+ },
+ isError = subject.isBlank(),
+ label = {
+ Text(
+ stringResource(R.string.send_peer_purpose),
+ color = if (subject.isBlank()) {
+ MaterialTheme.colorScheme.error
+ } else Color.Unspecified,
+ )
+ },
+ supportingText = {
+ Text(
+ stringResource(
+ R.string.char_count,
+ subject.length,
+ MAX_LENGTH_SUBJECT
+ )
+ )
+ },
+ )
+
+ if (res != null) {
+ if (res.amountEffective > res.amountRaw) {
+ val fee = res.amountEffective - res.amountRaw
+ Text(
+ modifier = Modifier.padding(vertical = 16.dp),
+ text = stringResource(
+ id = R.string.payment_fee,
+ fee.withSpec(selectedSpec)
+ ),
+ softWrap = false,
+ color = MaterialTheme.colorScheme.error,
+ )
+ }
}
- }
- checkResult?.exchangeBaseUrl?.let { exchangeBaseUrl ->
- TransactionInfoComposable(
- label = stringResource(id = R.string.withdraw_exchange),
- info = cleanExchange(exchangeBaseUrl),
+ Text(
+ modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
+ text = stringResource(R.string.send_peer_expiration_period),
+ style = MaterialTheme.typography.bodyMedium,
)
- }
- Text(
- modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp),
- text = stringResource(R.string.send_peer_expiration_period),
- style = MaterialTheme.typography.bodyMedium,
- )
+ ExpirationComposable(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .padding(top = 8.dp, bottom = 16.dp),
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
+ }
- ExpirationComposable(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .padding(top = 8.dp, bottom = 16.dp),
- option = option,
- hours = hours,
- onOptionChange = { option = it }
- ) { hours = it }
+ // only show provider for global scope,
+ // otherwise it's already in scope selector
+ if (amount.scope is ScopeInfo.Global) {
+ checkResult?.exchangeBaseUrl?.let { exchangeBaseUrl ->
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_exchange),
+ info = cleanExchange(exchangeBaseUrl),
+ )
+ }
+ }
BottomInsetsSpacer()
}
@@ -226,7 +221,7 @@ fun OutgoingPullIntroComposable(
Button(
modifier = Modifier
.systemBarsPaddingBottom(),
- enabled = subject.isNotBlank() && res != null,
+ enabled = tosReview || (res != null && subject.isNotBlank()),
onClick = {
val ex = res?.exchangeBaseUrl ?: error("clickable without exchange")
if (res.tosStatus == ExchangeTosStatus.Accepted) {
@@ -250,6 +245,20 @@ fun OutgoingPullIntroComposable(
}
@Composable
+fun PeerCreatingComposable() {
+ Box(
+ modifier = Modifier
+ .fillMaxSize(),
+ ) {
+ CircularProgressIndicator(
+ modifier = Modifier
+ .padding(32.dp)
+ .align(Center),
+ )
+ }
+}
+
+@Composable
fun PeerErrorComposable(state: OutgoingError, onClose: () -> Unit) {
Column(
modifier = Modifier
@@ -292,7 +301,7 @@ fun PeerPullComposableCreatingPreview() {
ScopeInfo.Global("CHF"),
),
getCurrencySpec = { null },
- checkPeerPullCredit = { null },
+ checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
onTosAccept = {},
onClose = {},
@@ -313,7 +322,7 @@ fun PeerPullComposableCheckingPreview() {
ScopeInfo.Global("CHF"),
),
getCurrencySpec = { null },
- checkPeerPullCredit = { null },
+ checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
onTosAccept = {},
onClose = {},
@@ -336,7 +345,7 @@ fun PeerPullComposableCheckedPreview() {
ScopeInfo.Global("CHF"),
),
getCurrencySpec = { null },
- checkPeerPullCredit = { null },
+ checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
onTosAccept = {},
onClose = {},
@@ -359,7 +368,7 @@ fun PeerPullComposableErrorPreview() {
ScopeInfo.Global("CHF"),
),
getCurrencySpec = { null },
- checkPeerPullCredit = { null },
+ checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
onTosAccept = {},
onClose = {},
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt
@@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
@@ -58,15 +59,15 @@ class OutgoingPullFragment : Fragment() {
state = state,
onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice,
onTosAccept = this@OutgoingPullFragment::onTosAccept,
- defaultScope = selectedScope,
+ defaultScope = remember { selectedScope },
scopes = balanceManager.getScopes(),
getCurrencySpec = balanceManager::getSpecForScopeInfo,
- checkPeerPullCredit = {
- exchangeManager.findExchange(it.scope)?.let { ex ->
- peerManager.checkPeerPullCredit(it.amount,
- exchangeBaseUrl = ex.exchangeBaseUrl,
- scopeInfo = it.scope)
- }
+ checkPeerPullCredit = { amount, loading ->
+ transactionManager.selectScope(amount.scope)
+ peerManager.checkPeerPullCredit(amount.amount,
+ scopeInfo = amount.scope,
+ loading = loading,
+ )
},
onClose = {
findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
@@ -225,7 +225,9 @@ fun OutgoingPushIntroComposable(
onOptionChange = { option = it }
) { hours = it }
- AnimatedVisibility(feeResult is Success) {
+ // only show provider for global scope,
+ // otherwise it's already in scope selector
+ AnimatedVisibility(feeResult is Success && amount.scope is ScopeInfo.Global) {
(feeResult as? Success)?.let {
Column(
modifier = Modifier.padding(bottom = 8.dp),
diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@@ -99,15 +100,18 @@ class PeerManager(
suspend fun checkPeerPullCredit(
amount: Amount,
- exchangeBaseUrl: String? = null,
- scopeInfo: ScopeInfo? = null,
+ scopeInfo: ScopeInfo,
+ loading: Boolean = false,
): CheckPeerPullCreditResult? {
var response: CheckPeerPullCreditResult? = null
- val exchangeItem = exchangeManager.findExchange(amount.currency) ?: return null
+ val exchangeItem = exchangeManager.findExchange(scopeInfo) ?: return null
+
+ if (loading) {
+ _outgoingPullState.value = OutgoingChecking
+ }
api.request("checkPeerPullCredit", CheckPeerPullCreditResponse.serializer()) {
- exchangeBaseUrl?.let { put("exchangeBaseUrl", it) }
- scopeInfo?.let { put("restrictScope", JSONObject(BackendManager.json.encodeToString(scopeInfo))) }
+ put("restrictScope", JSONObject(BackendManager.json.encodeToString(scopeInfo)))
put("amount", amount.toJSONString())
}.onSuccess {
response = CheckPeerPullCreditResult(
@@ -120,6 +124,10 @@ class PeerManager(
Log.e(TAG, "got checkPeerPullCredit error result $error")
}
+ if (loading) {
+ _outgoingPullState.value = OutgoingIntro
+ }
+
return response
}
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt
@@ -198,7 +198,7 @@ class TransactionManager(
fun selectScope(
scopeInfo: ScopeInfo?,
stateFilter: TransactionStateFilter? = null,
- ) = scope.launch {
+ ) {
mSelectedScope.value = scopeInfo
mStateFilter.value = stateFilter
}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt
@@ -126,12 +126,7 @@ fun WithdrawalShowInfo(
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- if (status.status == TosReviewRequired) {
- Text(
- modifier = Modifier.padding(22.dp),
- text = stringResource(R.string.withdraw_review_terms),
- )
- } else if (status.isCashAcceptor) {
+ if (status.isCashAcceptor) {
WarningLabel(
label = stringResource(R.string.withdraw_cash_acceptor),
modifier = Modifier
@@ -149,6 +144,7 @@ fun WithdrawalShowInfo(
amount = selectedAmount.amount.withSpec(spec)),
scopes = scopes,
editableScope = editableScope,
+ enabledAmount = status.status != TosReviewRequired,
onAmountChanged = { amount ->
selectedAmount = if (amount.scope != status.scopeInfo) {
// if amount changes, reset to zero!
@@ -172,6 +168,11 @@ fun WithdrawalShowInfo(
}
)
+ if (status.status == TosReviewRequired) Text(
+ modifier = Modifier.padding(22.dp),
+ text = stringResource(R.string.withdraw_review_terms),
+ )
+
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/manual/ManualWithdrawSuccessFragment.kt
@@ -20,7 +20,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -225,6 +225,7 @@ GNU Taler is immune against many types of fraud, such as phishing of credit card
<string name="receive_peer_payment_instruction">Scan this QR code to pay %1$s</string>
<string name="receive_peer_payment_intro">Do you want to receive this payment?</string>
<string name="receive_peer_payment_title">Receive payment</string>
+ <string name="receive_peer_review_terms">You must first accept the payment service\'s terms of service before you can request electronic cash.</string>
<string name="receive_peer_title">Request money</string>
<!-- P2P send -->