commit ecd9979460e7776ea12e83d0339be44a3cae9e6c
parent acf50017c5b4d63fa5ce0339280b88a52ef34bcc
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 26 Nov 2025 08:43:16 +0100
[wallet] p2p flow UI/UX improvements
Diffstat:
8 files changed, 208 insertions(+), 199 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AddAccountComposable.kt b/wallet/src/main/java/net/taler/wallet/accounts/AddAccountComposable.kt
@@ -57,9 +57,8 @@ import net.taler.wallet.accounts.PaytoUriIban
import net.taler.wallet.accounts.PaytoUriTalerBank
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.ErrorComposable
import net.taler.wallet.compose.WarningLabel
-import net.taler.wallet.peer.OutgoingError
-import net.taler.wallet.peer.PeerErrorComposable
import net.taler.wallet.useDebounce
@Composable
@@ -305,11 +304,12 @@ fun AddAccountErrorComposable(
message: String,
onClose: () -> Unit,
) {
- PeerErrorComposable(
- state = OutgoingError(info = TalerErrorInfo(
+ ErrorComposable(
+ error = TalerErrorInfo(
message = message,
code = TalerErrorCode.UNKNOWN,
- )),
+ ),
+ devMode = false,
onClose = onClose,
)
}
diff --git a/wallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt
@@ -16,6 +16,7 @@
package net.taler.wallet.compose
+import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -24,6 +25,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.ErrorOutline
@@ -41,7 +43,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.taler.wallet.BottomInsetsSpacer
import net.taler.wallet.R
@@ -57,7 +58,8 @@ fun ErrorComposable(
Column(
modifier = Modifier
.padding(16.dp)
- .fillMaxSize(),
+ .fillMaxSize()
+ .horizontalScroll(rememberScrollState()),
horizontalAlignment = CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt
@@ -34,8 +34,7 @@ import net.taler.wallet.accounts.BankAccountRow
import net.taler.wallet.accounts.KnownBankAccountInfo
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
-import net.taler.wallet.peer.OutgoingError
-import net.taler.wallet.peer.PeerErrorComposable
+import net.taler.wallet.compose.ErrorComposable
@Composable
fun MakeDepositComposable(
@@ -85,12 +84,12 @@ fun MakeDepositErrorComposable(
message: String,
onClose: () -> Unit,
) {
- PeerErrorComposable(
- state = OutgoingError(info = TalerErrorInfo(
+ ErrorComposable(
+ error = TalerErrorInfo(
message = message,
code = TalerErrorCode.UNKNOWN,
- )
),
+ devMode = false,
onClose = onClose,
)
}
\ No newline at end of file
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
@@ -16,8 +16,7 @@
package net.taler.wallet.peer
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -27,8 +26,6 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
@@ -40,14 +37,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
@@ -64,6 +58,8 @@ import net.taler.wallet.cleanExchange
import net.taler.wallet.compose.AmountScope
import net.taler.wallet.compose.AmountScopeField
import net.taler.wallet.compose.BottomButtonBox
+import net.taler.wallet.compose.ErrorComposable
+import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.exchanges.ExchangeTosStatus
import net.taler.wallet.systemBarsPaddingBottom
@@ -76,6 +72,7 @@ fun OutgoingPullComposable(
state: OutgoingState,
defaultScope: ScopeInfo?,
scopes: List<ScopeInfo>,
+ devMode: Boolean,
getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
checkPeerPullCredit: suspend (amount: AmountScope, loading: Boolean) -> CheckPeerPullCreditResult?,
onCreateInvoice: (amount: AmountScope, subject: String, hours: Long, exchangeBaseUrl: String) -> Unit,
@@ -98,22 +95,24 @@ fun OutgoingPullComposable(
val tosReview = checkResult != null && checkResult?.tosStatus != ExchangeTosStatus.Accepted
amount.amount.useDebounce {
- checkResult = checkPeerPullCredit(amount, false)
- }
-
- LaunchedEffect(amount.scope) {
- checkResult = checkPeerPullCredit(amount, true)
+ if (!amount.amount.isZero()) {
+ checkResult = checkPeerPullCredit(amount, false)
+ }
}
if (state is OutgoingChecking ||
state is OutgoingCreating ||
state is OutgoingResponse) {
- PeerCreatingComposable()
+ LoadingScreen()
return
}
- val focusManager = LocalFocusManager.current
- val focusRequester = remember { FocusRequester() }
+ val amountFocusRequester = remember { FocusRequester() }
+ val subjectFocusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(Unit) {
+ amountFocusRequester.requestFocus()
+ }
Column(
Modifier
@@ -127,28 +126,32 @@ fun OutgoingPullComposable(
.verticalScroll(rememberScrollState()),
horizontalAlignment = CenterHorizontally,
) {
+ var shortcutSelected by remember { mutableStateOf(false) }
AmountScopeField(
modifier = Modifier
.padding(16.dp)
- .fillMaxWidth(),
+ .fillMaxWidth()
+ .focusRequester(amountFocusRequester),
amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)),
scopes = scopes,
readOnly = false,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
enabledAmount = !tosReview,
showShortcuts = true,
- onAmountChanged = { amount = it },
+ onAmountChanged = {
+ amount = it
+ shortcutSelected = false
+ },
onShortcutSelected = {
amount = it
- focusManager.moveFocus(FocusDirection.Next)
- focusRequester.requestFocus()
+ shortcutSelected = true
},
isError = amount.amount.isZero(),
label = { Text(stringResource(R.string.amount_receive)) },
)
if (state is OutgoingError) {
- PeerErrorComposable(state, onClose)
+ ErrorComposable(state.info, devMode, onClose)
return@Column
}
@@ -157,67 +160,74 @@ fun OutgoingPullComposable(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.receive_peer_review_terms)
)
- } else {
- OutlinedTextField(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .fillMaxWidth()
- .focusRequester(focusRequester),
- 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
+ } else AnimatedVisibility(!amount.amount.isZero()) {
+ Column {
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ .focusRequester(subjectFocusRequester),
+ 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,
- )
+ 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,
+ )
+ }
}
- }
- 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 }
+ }
+
+ LaunchedEffect(Unit) {
+ // do not steal focus when manually typing amount
+ if (shortcutSelected) subjectFocusRequester.requestFocus()
+ }
}
// only show provider for global scope,
@@ -238,7 +248,7 @@ fun OutgoingPullComposable(
Button(
modifier = Modifier
.systemBarsPaddingBottom(),
- enabled = tosReview || (res != null && subject.isNotBlank()),
+ enabled = tosReview || (res != null && !amount.amount.isZero() && subject.isNotBlank()),
onClick = {
val ex = res?.exchangeBaseUrl ?: error("clickable without exchange")
if (res.tosStatus == ExchangeTosStatus.Accepted) {
@@ -260,51 +270,6 @@ fun OutgoingPullComposable(
}
}
}
-
-@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
- .padding(16.dp)
- .fillMaxSize(),
- horizontalAlignment = CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- ) {
- Text(
- color = MaterialTheme.colorScheme.error,
- style = MaterialTheme.typography.bodyLarge,
- text = state.info.userFacingMsg,
- )
-
- Button(
- modifier = Modifier.padding(16.dp),
- onClick = onClose,
- colors = ButtonDefaults.buttonColors(
- containerColor = MaterialTheme.colorScheme.error,
- contentColor = MaterialTheme.colorScheme.onError,
- ),
- ) {
- Text(text = stringResource(R.string.close))
- }
-
- BottomInsetsSpacer()
- }
-}
-
@Preview
@Composable
fun PeerPullComposableCreatingPreview() {
@@ -317,6 +282,7 @@ fun PeerPullComposableCreatingPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
@@ -338,6 +304,7 @@ fun PeerPullComposableCheckingPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
@@ -361,6 +328,7 @@ fun PeerPullComposableCheckedPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
@@ -384,6 +352,7 @@ fun PeerPullComposableErrorPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
checkPeerPullCredit = { _, _ -> null },
onCreateInvoice = { _, _, _, _ -> },
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.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.core.os.bundleOf
@@ -56,16 +57,19 @@ class OutgoingPullFragment : Fragment() {
TalerSurface {
val state by peerManager.pullState.collectAsStateLifecycleAware()
val viewMode by model.viewMode.collectAsStateLifecycleAware()
+ val devMode by model.devMode.observeAsState()
OutgoingPullComposable(
state = state,
onCreateInvoice = this@OutgoingPullFragment::onCreateInvoice,
onTosAccept = this@OutgoingPullFragment::onTosAccept,
defaultScope = remember { (viewMode as? ViewMode.Transactions)?.selectedScope },
scopes = balanceManager.getScopes(),
+ devMode = devMode == true,
getCurrencySpec = exchangeManager::getSpecForScopeInfo,
checkPeerPullCredit = { amount, loading ->
model.selectScope(amount.scope)
- peerManager.checkPeerPullCredit(amount.amount,
+ peerManager.checkPeerPullCredit(
+ amount.amount,
scopeInfo = amount.scope,
loading = loading,
)
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
@@ -39,11 +39,9 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
@@ -60,6 +58,8 @@ import net.taler.wallet.cleanExchange
import net.taler.wallet.compose.AmountScope
import net.taler.wallet.compose.AmountScopeField
import net.taler.wallet.compose.BottomButtonBox
+import net.taler.wallet.compose.ErrorComposable
+import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.exchanges.ExchangeTosStatus
import net.taler.wallet.payment.stringResId
@@ -76,13 +76,14 @@ fun OutgoingPushComposable(
state: OutgoingState,
defaultScope: ScopeInfo?,
scopes: List<ScopeInfo>,
+ devMode: Boolean,
getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
getFees: suspend (amount: AmountScope) -> CheckFeeResult?,
onSend: (amount: AmountScope, summary: String, hours: Long) -> Unit,
onClose: () -> Unit,
) {
when(state) {
- is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> PeerCreatingComposable()
+ is OutgoingChecking, is OutgoingCreating, is OutgoingResponse -> LoadingScreen()
is OutgoingIntro, is OutgoingChecked -> OutgoingPushIntroComposable(
defaultScope = defaultScope,
scopes = scopes,
@@ -90,7 +91,7 @@ fun OutgoingPushComposable(
getFees = getFees,
onSend = onSend,
)
- is OutgoingError -> PeerErrorComposable(state, onClose)
+ is OutgoingError -> ErrorComposable(state.info, devMode, onClose)
}
}
@@ -115,16 +116,18 @@ fun OutgoingPushIntroComposable(
var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) }
amount.useDebounce {
- feeResult = getFees(it) ?: None()
+ if (!amount.amount.isZero()) {
+ feeResult = getFees(it) ?: None()
+ }
}
+ val amountFocusRequester = remember { FocusRequester() }
+ val subjectFocusRequester = remember { FocusRequester() }
+
LaunchedEffect(Unit) {
- feeResult = getFees(amount) ?: None()
+ amountFocusRequester.requestFocus()
}
- val focusManager = LocalFocusManager.current
- val focusRequester = remember { FocusRequester() }
-
Column(
Modifier
.fillMaxSize()
@@ -156,20 +159,24 @@ fun OutgoingPushIntroComposable(
}
}
+ var shortcutSelected by remember { mutableStateOf(false) }
AmountScopeField(
modifier = Modifier
.padding(horizontal = 16.dp)
- .fillMaxWidth(),
+ .fillMaxWidth()
+ .focusRequester(amountFocusRequester),
amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)),
scopes = scopes,
readOnly = false,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
showShortcuts = true,
- onAmountChanged = { amount = it },
+ onAmountChanged = {
+ amount = it
+ shortcutSelected = false
+ },
onShortcutSelected = {
amount = it
- focusManager.moveFocus(FocusDirection.Next)
- focusRequester.requestFocus()
+ shortcutSelected = true
},
label = { Text(stringResource(R.string.amount_send)) },
isError = amount.amount.isZero() || feeResult is InsufficientBalance,
@@ -201,61 +208,72 @@ fun OutgoingPushIntroComposable(
}
)
- OutlinedTextField(
- modifier = Modifier
- .padding(horizontal = 16.dp)
- .fillMaxWidth()
- .focusRequester(focusRequester),
- 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,
+ AnimatedVisibility(feeResult is Success && !amount.amount.isZero()) {
+ Column(
+ modifier = Modifier.padding(bottom = 8.dp),
+ horizontalAlignment = CenterHorizontally,
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ .focusRequester(subjectFocusRequester),
+ 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
+ )
+ )
+ },
)
- },
- supportingText = {
- Text(stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT))
- },
- )
- Text(
- modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp),
- text = stringResource(R.string.send_peer_expiration_period),
- style = MaterialTheme.typography.bodyMedium,
- )
+ Text(
+ modifier = Modifier.padding(top = 8.dp, start = 16.dp, end = 16.dp),
+ text = stringResource(R.string.send_peer_expiration_period),
+ style = MaterialTheme.typography.bodyMedium,
+ )
- ExpirationComposable(
- modifier = Modifier.padding(
- vertical = 8.dp,
- horizontal = 16.dp,
- ),
- option = option,
- hours = hours,
- onOptionChange = { option = it }
- ) { hours = it }
+ ExpirationComposable(
+ modifier = Modifier.padding(
+ vertical = 8.dp,
+ horizontal = 16.dp,
+ ),
+ option = option,
+ hours = hours,
+ onOptionChange = { option = it }
+ ) { hours = it }
- // 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),
- horizontalAlignment = CenterHorizontally,
- ) {
- TransactionInfoComposable(
- label = stringResource(id = R.string.withdraw_exchange),
- info = cleanExchange(it.exchangeBaseUrl),
- )
+ (feeResult as? Success)?.let {
+ if (amount.scope is ScopeInfo.Global) {
+ TransactionInfoComposable(
+ label = stringResource(id = R.string.withdraw_exchange),
+ info = cleanExchange(it.exchangeBaseUrl),
+ )
+ }
}
}
+
+ LaunchedEffect(Unit) {
+ // do not steal focus when manually typing amount
+ if (shortcutSelected) subjectFocusRequester.requestFocus()
+ }
}
BottomInsetsSpacer()
@@ -264,7 +282,7 @@ fun OutgoingPushIntroComposable(
BottomButtonBox(Modifier.fillMaxWidth()) {
Button(
modifier = Modifier.systemBarsPaddingBottom(),
- enabled = feeResult is Success && subject.isNotBlank(),
+ enabled = feeResult is Success && !amount.amount.isZero() && subject.isNotBlank(),
onClick = { onSend(amount, subject, hours) },
) {
Text(text = stringResource(R.string.send_peer_create_button))
@@ -285,6 +303,7 @@ fun PeerPushComposableCreatingPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
getFees = { Success(
amountEffective = Amount.fromJSONString("KUDOS:10"),
@@ -310,6 +329,7 @@ fun PeerPushComposableCheckingPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
getFees = { Success(
amountEffective = Amount.fromJSONString("KUDOS:10"),
@@ -332,6 +352,7 @@ fun PeerPushComposableCheckedPreview() {
val state = OutgoingChecked(amountRaw, amountEffective, "https://exchange.demo.taler.net", ExchangeTosStatus.Accepted)
OutgoingPushComposable(
state = state,
+ devMode = true,
getCurrencySpec = { null },
defaultScope = ScopeInfo.Exchange("KUDOS", "https://exchange.demo.taler.net/"),
scopes = listOf(
@@ -365,6 +386,7 @@ fun PeerPushComposableErrorPreview() {
ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"),
ScopeInfo.Global("CHF"),
),
+ devMode = true,
getCurrencySpec = { null },
getFees = { Success(
amountEffective = Amount.fromJSONString("KUDOS:10"),
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt
@@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
@@ -68,10 +69,12 @@ class OutgoingPushFragment : Fragment() {
TalerSurface {
val state = peerManager.pushState.collectAsStateLifecycleAware().value
val viewMode by model.viewMode.collectAsStateLifecycleAware()
+ val devMode by model.devMode.observeAsState()
OutgoingPushComposable(
state = state,
defaultScope = remember { (viewMode as? ViewMode.Transactions)?.selectedScope },
scopes = balanceManager.getScopes(),
+ devMode = devMode == true,
getCurrencySpec = exchangeManager::getSpecForScopeInfo,
getFees = {
model.selectScope(it.scope)
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt
@@ -35,12 +35,15 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
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
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -110,6 +113,12 @@ fun WithdrawalShowInfo(
}
}
+ val focusRequester = remember { FocusRequester() }
+
+ LaunchedEffect(Unit) {
+ focusRequester.requestFocus()
+ }
+
Column(
Modifier
.fillMaxSize()
@@ -153,7 +162,8 @@ fun WithdrawalShowInfo(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
- .fillMaxWidth(),
+ .fillMaxWidth()
+ .focusRequester(focusRequester),
amount = selectedAmount.copy(
amount = selectedAmount.amount.withSpec(spec)),
scopes = scopes,