commit a8c76d7580345e3d4f9492f883df71dc5537a0e2 parent 2b307902d1b77f16a90e095931206d59f33578d4 Author: Iván Ávalos <avalos@disroot.org> Date: Fri, 25 Oct 2024 22:51:56 +0200 [wallet] Refactor amount input into AmountCurrencyField Diffstat:
10 files changed, 247 insertions(+), 331 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt @@ -185,7 +185,6 @@ fun EmptyBalancesComposable() { ) { val context = LocalContext.current - // TODO: render hyperlink! Text( stringResource(R.string.balances_empty_state), textAlign = TextAlign.Center, diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountInputField.kt @@ -17,23 +17,32 @@ package net.taler.wallet.compose import android.os.Build +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.OutlinedTextField 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.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp import net.taler.common.Amount +import net.taler.common.CurrencySpecification +import net.taler.wallet.deposit.CurrencyDropdown +import net.taler.wallet.getAmount import java.text.DecimalFormat import java.text.DecimalFormatSymbols import kotlin.math.max @@ -43,7 +52,60 @@ import kotlin.math.roundToLong const val DEFAULT_INPUT_DECIMALS = 2 @Composable -fun AmountInputField( +fun AmountCurrencyField( + modifier: Modifier = Modifier, + initialAmount: Amount, + initialCurrency: String?, + editableCurrency: Boolean = true, + currencies: List<String>, + onAmountChanged: (amount: Amount) -> Unit, + getCurrencySpec: (currency: String) -> CurrencySpecification?, + label: @Composable (() -> Unit)? = null, + supportingText: @Composable (() -> Unit)? = null, + isError: Boolean = false, + keyboardActions: KeyboardActions = KeyboardActions.Default, + decimalFormatSymbols: DecimalFormatSymbols = DecimalFormat().decimalFormatSymbols, + readOnly: Boolean = false, +) { + var text by remember(initialAmount) { mutableStateOf(initialAmount.amountStr) } + var selectedCurrency by rememberSaveable { mutableStateOf(initialCurrency ?: currencies[0]) } + val selectedSpec: CurrencySpecification? = getCurrencySpec(selectedCurrency) + val amount = remember(selectedCurrency, text) { getAmount(selectedCurrency, text) } + + LaunchedEffect(amount) { + amount?.let { onAmountChanged(amount) } + } + + Row(modifier = modifier) { + AmountInputFieldBase( + modifier = Modifier + .weight(2f, true) + .padding(end = 16.dp), + value = text, + onValueChange = { input -> text = input }, + label = label, + numberOfDecimals = selectedSpec + ?.numFractionalInputDigits + ?: DEFAULT_INPUT_DECIMALS, + isError = isError, + supportingText = supportingText, + keyboardActions = keyboardActions, + decimalFormatSymbols = decimalFormatSymbols, + readOnly = readOnly, + ) + + CurrencyDropdown( + modifier = Modifier.weight(1f), + currencies = currencies, + onCurrencyChanged = { selectedCurrency = it }, + initialCurrency = initialCurrency, + readOnly = !editableCurrency, + ) + } +} + +@Composable +fun AmountInputFieldBase( value: String, onValueChange: (value: String) -> Unit, modifier: Modifier = Modifier, diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt @@ -18,7 +18,6 @@ package net.taler.wallet.deposit import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -51,9 +50,7 @@ import net.taler.common.CurrencySpecification import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo -import net.taler.wallet.compose.AmountInputField -import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS -import net.taler.wallet.getAmount +import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.peer.OutgoingError import net.taler.wallet.peer.PeerErrorComposable import net.taler.wallet.transactions.AmountType.Negative @@ -84,14 +81,11 @@ fun MakeDepositComposable( ) { // Amount/currency stuff // TODO: use scopeInfo instead of currency! - var selectedCurrency by rememberSaveable { mutableStateOf(defaultCurrency ?: currencies[0]) } - val selectedSpec: CurrencySpecification? = getCurrencySpec(selectedCurrency) - var text by rememberSaveable { mutableStateOf("0") } - val amount = remember(selectedCurrency, text) { getAmount(selectedCurrency, text) } var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None) } + var amount by remember { mutableStateOf(Amount.zero(defaultCurrency ?: currencies[0])) } // TODO: make getDepositWireTypes asynchronous! - val depositWireTypes = remember(selectedCurrency) { getDepositWireTypes(selectedCurrency) } + val depositWireTypes = remember(amount.currency) { getDepositWireTypes(amount.currency) } val supportedWireTypes = remember(depositWireTypes) { depositWireTypes?.wireTypes ?: emptyList() } val talerBankHostnames = remember(depositWireTypes) { depositWireTypes?.wireTypeDetails?.flatMap { it.talerBankHostnames }?.distinct() ?: emptyList() } var selectedWireType by remember { mutableStateOf(supportedWireTypes.firstOrNull()) } @@ -111,7 +105,7 @@ fun MakeDepositComposable( } // reset forms and selected wire type when switching currency - DisposableEffect(selectedCurrency) { + DisposableEffect(amount.currency) { selectedWireType = supportedWireTypes.first() formError = true ibanName = presetName ?: "" @@ -124,7 +118,7 @@ fun MakeDepositComposable( // TODO: make checkDeposit asynchronous! amount.useDebounce { - if (amount != null && paytoUri != null) { + if (paytoUri != null) { // TODO: handle insufficient balance! // TODO: handle KYC limits! checkResult = checkDeposit(amount, paytoUri) @@ -132,13 +126,13 @@ fun MakeDepositComposable( } paytoUri.useDebounce { - if (amount != null && paytoUri != null) { + if (paytoUri != null) { checkResult = checkDeposit(amount, paytoUri) } } LaunchedEffect(Unit) { - if (amount != null && paytoUri != null) { + if (paytoUri != null) { checkResult = checkDeposit(amount, paytoUri) } } @@ -160,40 +154,28 @@ fun MakeDepositComposable( ) } - Row(Modifier.padding( - start = 16.dp, - top = 16.dp, - end = 16.dp, - )) { - AmountInputField( - modifier = Modifier - .weight(1f, true) - .padding(end = 16.dp), - value = text, - onValueChange = { input -> - text = input - }, - label = { Text(stringResource(R.string.amount_deposit)) }, - numberOfDecimals = selectedSpec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS, - isError = checkResult is CheckDepositResult.InsufficientBalance, - supportingText = { (checkResult as? CheckDepositResult.InsufficientBalance)?.let { res -> - if (res.maxAmountRaw != null) { - Text(stringResource( - R.string.payment_balance_insufficient_max, - res.maxAmountRaw.withSpec(selectedSpec), - )) - } - } } - ) - - CurrencyDropdown( - modifier = Modifier.weight(1f), - currencies = currencies, - onCurrencyChanged = { selectedCurrency = it }, - initialCurrency = defaultCurrency, - readOnly = false, - ) - } + AmountCurrencyField( + modifier = Modifier + .padding( + top = 16.dp, + start = 16.dp, + end = 16.dp, + ).fillMaxWidth(), + initialAmount = amount, + initialCurrency = defaultCurrency, + onAmountChanged = { amount = it }, + editableCurrency = false, + currencies = currencies, + getCurrencySpec = getCurrencySpec, + isError = checkResult !is CheckDepositResult.Success, + label = { Text(stringResource(R.string.amount_deposit)) }, + supportingText = { + val res = checkResult + if (res is CheckDepositResult.InsufficientBalance && res.maxAmountEffective != null) { + Text(stringResource(R.string.payment_balance_insufficient_max, res.maxAmountEffective)) + } + } + ) when(selectedWireType) { WireType.IBAN -> { @@ -234,8 +216,7 @@ fun MakeDepositComposable( else -> {} } - AnimatedVisibility(visible = amount != null && checkResult is CheckDepositResult.Success) { - if (amount == null) return@AnimatedVisibility + AnimatedVisibility(visible = checkResult is CheckDepositResult.Success) { val res = checkResult as? CheckDepositResult.Success ?: return@AnimatedVisibility Column( @@ -277,7 +258,7 @@ fun MakeDepositComposable( enabled = checkResult is CheckDepositResult.Success && !formError, onClick = { focusManager.clearFocus() - if (paytoUri != null && amount != null) { + if (paytoUri != null) { onMakeDeposit(amount, paytoUri) } }, diff --git a/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/PayToUriFragment.kt @@ -25,10 +25,8 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -40,7 +38,6 @@ import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -48,10 +45,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.Alignment.Companion.Center import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -62,10 +56,11 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import net.taler.common.Amount +import net.taler.common.CurrencySpecification import net.taler.wallet.AmountResult import net.taler.wallet.MainViewModel import net.taler.wallet.R -import net.taler.wallet.compose.AmountInputField +import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.TalerSurface class PayToUriFragment : Fragment() { @@ -103,6 +98,7 @@ class PayToUriFragment : Fragment() { findNavController().navigate( R.id.action_nav_payto_uri_to_nav_deposit, bundle) }, + getCurrencySpec = balanceManager::getSpecForCurrency, ) else Text( text = stringResource(id = R.string.uri_invalid), color = MaterialTheme.colorScheme.error, @@ -123,6 +119,7 @@ class PayToUriFragment : Fragment() { private fun PayToComposable( currencies: List<String>, getAmount: (String, String) -> AmountResult, + getCurrencySpec: (String) -> CurrencySpecification?, onAmountChosen: (Amount) -> Unit, ) { val scrollState = rememberScrollState() @@ -134,42 +131,36 @@ private fun PayToComposable( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), ) { - var amountText by rememberSaveable { mutableStateOf("0") } + var amount by remember { mutableStateOf(Amount.zero(currencies[0])) } var amountError by rememberSaveable { mutableStateOf("") } - var currency by rememberSaveable { mutableStateOf(currencies[0]) } - val focusRequester = remember { FocusRequester() } - AmountInputField( - modifier = Modifier.focusRequester(focusRequester), - value = amountText, - onValueChange = { input -> - amountError = "" - amountText = input - }, - label = { Text(stringResource(R.string.amount_send)) }, - supportingText = { - if (amountError.isNotBlank()) Text(amountError) - }, - isError = amountError.isNotBlank(), - ) - CurrencyDropdown( + + AmountCurrencyField( modifier = Modifier - .fillMaxSize() - .wrapContentSize(Center), + .padding(horizontal = 16.dp) + .fillMaxWidth(), + initialAmount = amount, + initialCurrency = amount.currency, currencies = currencies, - onCurrencyChanged = { c -> currency = c }, + readOnly = false, + onAmountChanged = { amount = it }, + getCurrencySpec = getCurrencySpec, + label = { Text(stringResource(R.string.amount_send)) }, + isError = amountError.isNotBlank(), + supportingText = { + if (amountError.isNotBlank()) { + Text(amountError) + } + } ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } val focusManager = LocalFocusManager.current val errorStrInvalidAmount = stringResource(id = R.string.amount_invalid) val errorStrInsufficientBalance = stringResource(id = R.string.payment_balance_insufficient) Button( modifier = Modifier.padding(16.dp), - enabled = amountText.isNotBlank(), + enabled = !amount.isZero(), onClick = { - when (val amountResult = getAmount(amountText, currency)) { + when (val amountResult = getAmount(amount.amountStr, amount.currency)) { is AmountResult.Success -> { focusManager.clearFocus() onAmountChosen(amountResult.amount) @@ -244,6 +235,7 @@ fun PreviewPayToComposable() { currencies = listOf("KUDOS", "TESTKUDOS", "BTCBITCOIN"), getAmount = { _, _ -> AmountResult.InvalidAmount }, onAmountChosen = {}, + getCurrencySpec = { null } ) } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount import net.taler.common.ContractTerms +import net.taler.common.CurrencySpecification import net.taler.wallet.AmountResult import net.taler.wallet.R import net.taler.wallet.compose.LoadingScreen @@ -38,6 +39,7 @@ import net.taler.wallet.compose.TalerSurface fun PayTemplateComposable( currencies: List<String>, payStatus: PayStatus, + getCurrencySpec: (String) -> CurrencySpecification?, onCreateAmount: (String, String) -> AmountResult, onSubmit: (params: TemplateParams) -> Unit, onError: (resId: Int) -> Unit, @@ -60,6 +62,7 @@ fun PayTemplateComposable( onCreateAmount = onCreateAmount, onError = onError, onSubmit = onSubmit, + getCurrencySpec = getCurrencySpec, ) } } @@ -111,6 +114,7 @@ fun PayTemplateLoadingPreview() { }, onSubmit = { _ -> }, onError = { _ -> }, + getCurrencySpec = { null }, ) } } @@ -133,6 +137,7 @@ fun PayTemplateInsufficientBalancePreview() { }, onSubmit = { _ -> }, onError = { _ -> }, + getCurrencySpec = { null }, ) } } @@ -149,6 +154,7 @@ fun PayTemplateAlreadyPaidPreview() { }, onSubmit = { _ -> }, onError = { _ -> }, + getCurrencySpec = { null }, ) } } @@ -166,6 +172,7 @@ fun PayTemplateNoCurrenciesPreview() { }, onSubmit = { _ -> }, onError = { _ -> }, + getCurrencySpec = { null }, ) } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateFragment.kt @@ -60,6 +60,7 @@ class PayTemplateFragment : Fragment() { onCreateAmount = model::createAmount, onSubmit = this@PayTemplateFragment::createOrder, onError = { this@PayTemplateFragment.showError(it) }, + getCurrencySpec = model.balanceManager::getSpecForCurrency, ) } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt @@ -17,7 +17,6 @@ package net.taler.wallet.payment import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Button @@ -40,12 +39,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount +import net.taler.common.CurrencySpecification import net.taler.common.RelativeTime import net.taler.wallet.AmountResult import net.taler.wallet.R -import net.taler.wallet.compose.AmountInputField +import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.deposit.CurrencyDropdown @OptIn(ExperimentalComposeUiApi::class) @Composable @@ -53,6 +52,7 @@ fun PayTemplateOrderComposable( usableCurrencies: List<String>, // non-empty intersection between the stored currencies and the ones supported by the merchant templateDetails: WalletTemplateDetails, onCreateAmount: (String, String) -> AmountResult, + getCurrencySpec: (String) -> CurrencySpecification?, onError: (msgRes: Int) -> Unit, onSubmit: (params: TemplateParams) -> Unit, ) { @@ -64,8 +64,10 @@ fun PayTemplateOrderComposable( val keyboardController = LocalSoftwareKeyboardController.current var summary by remember { mutableStateOf(defaultSummary ?: "") } - var currency by remember { mutableStateOf(defaultCurrency ?: usableCurrencies[0]) } - var amount by remember { mutableStateOf(defaultAmount?.amountStr ?: "0") } + var amount by remember { + val currency = defaultCurrency ?: usableCurrencies[0] + mutableStateOf(defaultAmount?.withCurrency(currency) ?: Amount.zero(currency)) + } Column(horizontalAlignment = End) { OutlinedTextField( @@ -86,26 +88,25 @@ fun PayTemplateOrderComposable( label = { Text(stringResource(R.string.withdraw_manual_ready_subject)) }, ) - AmountField( + AmountCurrencyField( modifier = Modifier .padding(16.dp) .fillMaxWidth(), - amount = amount, - currency = currency, + initialAmount = amount, + initialCurrency = amount.currency, currencies = usableCurrencies, - readOnlyCurrency = !templateDetails.isCurrencyEditable(usableCurrencies), - readOnlyAmount = !templateDetails.isAmountEditable(), - onAmountChosen = { a, c -> - amount = a - currency = c - }, + editableCurrency = !templateDetails.isCurrencyEditable(usableCurrencies), + readOnly = !templateDetails.isAmountEditable(), + onAmountChanged = { amount = it }, + getCurrencySpec = getCurrencySpec, + label = { Text(stringResource(R.string.amount_send)) }, ) Button( modifier = Modifier.padding(16.dp), enabled = !templateDetails.isSummaryEditable() || summary.isNotBlank(), onClick = { - when (val res = onCreateAmount(amount, currency)) { + when (val res = onCreateAmount(amount.amountStr, amount.currency)) { is AmountResult.InsufficientBalance -> onError(R.string.payment_balance_insufficient) is AmountResult.InvalidAmount -> onError(R.string.amount_invalid) // NOTE: it is important to nullify non-editable values! @@ -128,39 +129,6 @@ fun PayTemplateOrderComposable( } } -@Composable -private fun AmountField( - modifier: Modifier = Modifier, - currencies: List<String>, - amount: String, - currency: String, - readOnlyAmount: Boolean = true, - readOnlyCurrency: Boolean = true, - onAmountChosen: (amount: String, currency: String) -> Unit, -) { - Row( - modifier = modifier, - ) { - AmountInputField( - modifier = Modifier - .padding(end = 16.dp) - .weight(1f), - value = amount, - onValueChange = { onAmountChosen(it, currency) }, - label = { Text(stringResource(R.string.amount_send)) }, - readOnly = readOnlyAmount, - ) - - CurrencyDropdown( - modifier = Modifier.weight(1f), - initialCurrency = currency, - currencies = currencies, - onCurrencyChanged = { onAmountChosen(amount, it) }, - readOnly = readOnlyCurrency, - ) - } -} - val defaultTemplateDetails = WalletTemplateDetails( templateContract = TemplateContractDetails( minimumAge = 18, @@ -184,6 +152,7 @@ fun PayTemplateDefaultPreview() { }, onSubmit = { _ -> }, onError = { }, + getCurrencySpec = { null }, ) } } @@ -200,6 +169,7 @@ fun PayTemplateFixedAmountPreview() { }, onSubmit = { _ -> }, onError = { }, + getCurrencySpec = { null }, ) } } @@ -216,6 +186,7 @@ fun PayTemplateBlankSubjectPreview() { }, onSubmit = { _ -> }, onError = { }, + getCurrencySpec = { null }, ) } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt @@ -19,7 +19,6 @@ package net.taler.wallet.peer import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -42,8 +41,6 @@ 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.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -56,12 +53,9 @@ import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.cleanExchange -import net.taler.wallet.compose.AmountInputField -import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS +import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.deposit.CurrencyDropdown import net.taler.wallet.exchanges.ExchangeTosStatus -import net.taler.wallet.getAmount import net.taler.wallet.transactions.TransactionInfoComposable import net.taler.wallet.useDebounce import kotlin.random.Random @@ -123,49 +117,35 @@ fun OutgoingPullIntroComposable( horizontalAlignment = CenterHorizontally, ) { var subject by rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } - - var selectedCurrency by rememberSaveable { mutableStateOf(defaultCurrency ?: currencies[0]) } - val selectedSpec: CurrencySpecification? = getCurrencySpec(selectedCurrency) - var text by rememberSaveable { mutableStateOf("0") } - val amount = remember(selectedCurrency, text) { getAmount(selectedCurrency, text) } + var amount by remember { mutableStateOf(Amount.zero(defaultCurrency ?: currencies[0])) } + val selectedSpec = remember(amount) { getCurrencySpec(amount.currency) } var checkResult by remember { mutableStateOf<CheckPeerPullCreditResult?>(null) } // TODO: make checkPeerPullCredit asynchronous! amount.useDebounce { - checkResult = amount?.let { checkPeerPullCredit(it) } + checkResult = checkPeerPullCredit(it) } LaunchedEffect(Unit) { - checkResult = amount?.let { checkPeerPullCredit(it) } + checkResult = checkPeerPullCredit(amount) } - Row(Modifier.padding(bottom = 16.dp)) { - AmountInputField( - modifier = Modifier - .weight(1f, true) - .padding(end = 16.dp), - value = text, - onValueChange = { input -> - text = input - }, - label = { Text(stringResource(R.string.amount_receive)) }, - numberOfDecimals = selectedSpec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS, - ) - - CurrencyDropdown( - modifier = Modifier.weight(1f), - currencies = currencies, - onCurrencyChanged = { selectedCurrency = it }, - initialCurrency = defaultCurrency, - readOnly = false, - ) - } + AmountCurrencyField( + modifier = Modifier + .padding(bottom = 16.dp) + .fillMaxWidth(), + initialAmount = amount, + initialCurrency = amount.currency, + currencies = currencies, + readOnly = false, + onAmountChanged = { amount = it }, + getCurrencySpec = getCurrencySpec, + isError = amount.isZero(), + label = { Text(stringResource(R.string.amount_receive)) }, + ) OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), + modifier = Modifier.fillMaxWidth(), singleLine = true, value = subject, onValueChange = { input -> @@ -183,10 +163,6 @@ fun OutgoingPullIntroComposable( } ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - Text( modifier = Modifier .fillMaxWidth() @@ -236,7 +212,7 @@ fun OutgoingPullIntroComposable( enabled = subject.isNotBlank() && res != null, onClick = { val ex = res?.exchangeBaseUrl ?: error("clickable without exchange") - if (res.tosStatus == ExchangeTosStatus.Accepted) amount?.let { + if (res.tosStatus == ExchangeTosStatus.Accepted) { onCreateInvoice( amount, subject, diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt @@ -17,7 +17,6 @@ package net.taler.wallet.peer import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -36,8 +35,6 @@ 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.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -49,12 +46,9 @@ import net.taler.common.CurrencySpecification import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo -import net.taler.wallet.compose.AmountInputField -import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS +import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.deposit.CurrencyDropdown import net.taler.wallet.exchanges.ExchangeTosStatus -import net.taler.wallet.getAmount import net.taler.wallet.peer.CheckFeeResult.InsufficientBalance import net.taler.wallet.peer.CheckFeeResult.None import net.taler.wallet.peer.CheckFeeResult.Success @@ -100,73 +94,57 @@ fun OutgoingPushIntroComposable( .verticalScroll(scrollState), horizontalAlignment = CenterHorizontally, ) { - var selectedCurrency by rememberSaveable { mutableStateOf(defaultCurrency ?: currencies[0]) } - val selectedSpec: CurrencySpecification? = getCurrencySpec(selectedCurrency) - var text by rememberSaveable { mutableStateOf("0") } - val amount = remember(selectedCurrency, text) { getAmount(selectedCurrency, text) } + var amount by remember { mutableStateOf(Amount.zero(defaultCurrency ?: currencies[0])) } + val selectedSpec = remember(amount) { getCurrencySpec(amount.currency) } var feeResult by remember { mutableStateOf<CheckFeeResult>(None) } - // TODO: make getFees asynchronous! + // TODO: make checkPeerPullCredit asynchronous! amount.useDebounce { - feeResult = amount?.let { getFees(amount) } ?: None + feeResult = getFees(it) ?: None } LaunchedEffect(Unit) { - feeResult = amount?.let { getFees(amount) } ?: None + feeResult = getFees(amount) ?: None } - Row(Modifier.padding(bottom = 16.dp)) { - AmountInputField( - modifier = Modifier - .weight(1f, true) - .padding(end = 16.dp), - value = text, - onValueChange = { input -> - text = input - }, - label = { Text(stringResource(R.string.amount_send)) }, - numberOfDecimals = selectedSpec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS, - ) - - CurrencyDropdown( - modifier = Modifier.weight(1f), - currencies = currencies, - onCurrencyChanged = { selectedCurrency = it }, - initialCurrency = defaultCurrency, - readOnly = false, - ) - } + AmountCurrencyField( + modifier = Modifier.fillMaxWidth(), + initialAmount = amount, + initialCurrency = amount.currency, + currencies = currencies, + readOnly = false, + onAmountChanged = { amount = it }, + getCurrencySpec = getCurrencySpec, + label = { Text(stringResource(R.string.amount_send)) }, + isError = amount.isZero() || feeResult is InsufficientBalance, + supportingText = { + when (val res = feeResult) { + is Success -> 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, + ) + } - when(val res = feeResult) { - is Success -> 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, - ) - } + is InsufficientBalance -> if (res.maxAmountEffective != null) { + Text(stringResource(R.string.payment_balance_insufficient_max, res.maxAmountEffective)) + } - is InsufficientBalance -> if (res.maxAmountRaw != null) { - Text( - modifier = Modifier.padding(vertical = 16.dp), - text = stringResource( - R.string.payment_balance_insufficient_max, - res.maxAmountRaw.withSpec(selectedSpec), - ), - ) + else -> {} + } } - - else -> {} - } + ) var subject by rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } OutlinedTextField( modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), + .fillMaxWidth(), singleLine = true, value = subject, onValueChange = { input -> @@ -184,10 +162,6 @@ fun OutgoingPushIntroComposable( } ) - LaunchedEffect(Unit) { - focusRequester.requestFocus() - } - Text( modifier = Modifier .fillMaxWidth() @@ -214,7 +188,7 @@ fun OutgoingPushIntroComposable( Button( enabled = feeResult is Success && subject.isNotBlank(), - onClick = { amount?.let { onSend(it, subject, hours) } }, + onClick = { onSend(amount, subject, hours) }, ) { Text(text = stringResource(R.string.send_peer_create_button)) } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -24,7 +24,6 @@ import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -74,23 +73,26 @@ import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.cleanExchange -import net.taler.wallet.compose.AmountInputField +import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.BottomButtonBox -import net.taler.wallet.compose.DEFAULT_INPUT_DECIMALS import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.deposit.CurrencyDropdown import net.taler.wallet.exchanges.ExchangeItem import net.taler.wallet.exchanges.ExchangeTosStatus import net.taler.wallet.exchanges.SelectExchangeDialogFragment -import net.taler.wallet.getAmount import net.taler.wallet.showError import net.taler.wallet.transactions.AmountType import net.taler.wallet.transactions.TransactionAmountComposable import net.taler.wallet.transactions.TransactionInfoComposable import net.taler.wallet.useDebounce -import net.taler.wallet.withdraw.WithdrawStatus.Status.* +import net.taler.wallet.withdraw.WithdrawStatus.Status.InfoReceived +import net.taler.wallet.withdraw.WithdrawStatus.Status.Loading +import net.taler.wallet.withdraw.WithdrawStatus.Status.ManualTransferRequired +import net.taler.wallet.withdraw.WithdrawStatus.Status.None +import net.taler.wallet.withdraw.WithdrawStatus.Status.Success +import net.taler.wallet.withdraw.WithdrawStatus.Status.TosReviewRequired +import net.taler.wallet.withdraw.WithdrawStatus.Status.Updating class PromptWithdrawFragment: Fragment() { private val model: MainViewModel by activityViewModels() @@ -332,7 +334,9 @@ fun WithdrawalShowInfo( onTosReview: () -> Unit, onConfirm: (age: Int?) -> Unit, ) { - val defaultAmount = status.amountInfo?.amountRaw ?: status.uriInfo?.amount + val defaultAmount = status.amountInfo?.amountRaw + ?: status.uriInfo?.amount + ?: Amount.zero(defaultCurrency) val maxAmount = status.uriInfo?.maxAmount val editableAmount = status.uriInfo?.editableAmount ?: editableCurrency val wireFee = status.uriInfo?.wireFee ?: Amount.zero(defaultCurrency) @@ -345,11 +349,14 @@ fun WithdrawalShowInfo( var selectedAge by remember { mutableStateOf<Int?>(null) } var error by remember { mutableStateOf(false) } val scrollState = rememberScrollState() + val insufficientBalance = remember(selectedAmount, maxAmount) { + maxAmount == null || selectedAmount > maxAmount + } selectedAmount.useDebounce { if (startup) { // do not fire at startup startup = false - } else it?.let { + } else { onSelectAmount(it) } } @@ -364,33 +371,38 @@ fun WithdrawalShowInfo( horizontalAlignment = Alignment.CenterHorizontally, ) { if (editableAmount) { - WithdrawAmountComposable( - defaultAmount = defaultAmount?.withSpec(spec), - editableCurrency = editableCurrency, + AmountCurrencyField( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + initialAmount = selectedAmount.withSpec(spec), + initialCurrency = selectedAmount.currency, currencies = currencies, - maxAmount = maxAmount?.withSpec(spec), - spec = spec, - onAmountChanged = { amount, err -> - selectedAmount = amount - error = err - } + editableCurrency = editableCurrency, + onAmountChanged = { selectedAmount = it }, + getCurrencySpec = { spec }, + label = { Text(stringResource(R.string.amount_withdraw)) }, + isError = selectedAmount.isZero() || maxAmount != null && selectedAmount > maxAmount, + supportingText = { + if (insufficientBalance && maxAmount != null) { + Text(stringResource(R.string.amount_excess, maxAmount)) + } + }, ) } else { - selectedAmount?.let { amount -> - TransactionAmountComposable( - label = if (wireFee.isZero()) { - stringResource(R.string.amount_total) - } else { - stringResource(R.string.amount_chosen) - }, - amount = amount, - amountType = if (wireFee.isZero()) { - AmountType.Positive - } else { - AmountType.Neutral - }, - ) - } + TransactionAmountComposable( + label = if (wireFee.isZero()) { + stringResource(R.string.amount_total) + } else { + stringResource(R.string.amount_chosen) + }, + amount = selectedAmount, + amountType = if (wireFee.isZero()) { + AmountType.Positive + } else { + AmountType.Neutral + }, + ) } if (!wireFee.isZero()) { @@ -513,65 +525,6 @@ fun WithdrawalError( } } -@Composable -fun WithdrawAmountComposable( - defaultAmount: Amount?, - editableCurrency: Boolean, - currencies: List<String>, - maxAmount: Amount?, - spec: CurrencySpecification?, - onAmountChanged: (amount: Amount?, error: Boolean) -> Unit, -) { - var text by remember { mutableStateOf(defaultAmount?.amountStr ?: "0") } - val currency = remember(defaultAmount, currencies) { defaultAmount?.currency ?: currencies[0] } - val amount = remember(currency, text) { getAmount(currency, text) } - val insufficientBalance = remember(amount, maxAmount) { - amount?.let { maxAmount == null || it > maxAmount } == true - } - - Row( - modifier = Modifier - .padding(top = 16.dp) - .padding(horizontal = 16.dp), - ) { - AmountInputField( - modifier = Modifier - .padding(end = 16.dp) - .weight(1f), - value = text, - onValueChange = { value -> - text = value - - // Update selected amount - getAmount(currency, text)?.let { - onAmountChanged(it, maxAmount != null && it > maxAmount) - } ?: onAmountChanged(null, true) - }, - label = { Text(stringResource(R.string.amount_withdraw)) }, - supportingText = { - if (insufficientBalance && maxAmount != null) { - Text(stringResource(R.string.amount_excess, maxAmount)) - } - }, - isError = insufficientBalance, - numberOfDecimals = spec?.numFractionalInputDigits ?: DEFAULT_INPUT_DECIMALS, - ) - - CurrencyDropdown( - modifier = Modifier.weight(1f), - currencies = currencies, - onCurrencyChanged = { value -> - // Update selected amount - getAmount(value, text)?.let { - onAmountChanged(it, maxAmount != null && it > maxAmount) - } ?: onAmountChanged(null, true) - }, - initialCurrency = currency, - readOnly = !editableCurrency, - ) - } -} - @Preview @Composable fun WithdrawalShowInfoPreview() {