taler-android

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

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:
Mwallet/src/main/java/net/taler/wallet/accounts/AddAccountComposable.kt | 10+++++-----
Mwallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt | 6++++--
Mwallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt | 9++++-----
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt | 213++++++++++++++++++++++++++++++++++---------------------------------------------
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt | 6+++++-
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt | 148+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt | 3+++
Mwallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt | 12+++++++++++-
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,