taler-android

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

commit 4a72da387532c044c684e1678ba72d1252ca2f79
parent 01cfba831d1a03f0c29a012b5a7df600b671ddbb
Author: Iván Ávalos <avalos@disroot.org>
Date:   Wed, 28 May 2025 16:08:57 +0200

[wallet] unify confirmation buttons

Diffstat:
Mwallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt | 187+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt | 228++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mwallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt | 265++++++++++++++++++++++++++++++++++++++++++-------------------------------------
3 files changed, 359 insertions(+), 321 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt @@ -18,6 +18,7 @@ package net.taler.wallet.deposit import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding @@ -45,6 +46,8 @@ import net.taler.wallet.R import net.taler.wallet.accounts.BankAccountRow import net.taler.wallet.accounts.KnownBankAccountInfo import net.taler.wallet.compose.AmountCurrencyField +import net.taler.wallet.compose.BottomButtonBox +import net.taler.wallet.systemBarsPaddingBottom import net.taler.wallet.transactions.AmountType.Negative import net.taler.wallet.transactions.AmountType.Positive import net.taler.wallet.transactions.TransactionAmountComposable @@ -70,13 +73,10 @@ fun DepositAmountComposable( return } - val scrollState = rememberScrollState() Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(scrollState) + Modifier + .fillMaxSize() .imePadding(), - horizontalAlignment = CenterHorizontally, ) { var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None()) } // TODO: use scopeInfo instead of currency @@ -85,107 +85,114 @@ fun DepositAmountComposable( var amount by remember(state.maxDepositable) { mutableStateOf(Amount.zero(currencies.first())) } val spec = remember(amount) { getCurrencySpec(amount.currency) } - amount.useDebounce { - if (!amount.isZero()) { - checkResult = checkDeposit(amount) + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), + horizontalAlignment = CenterHorizontally, + ) { + + amount.useDebounce { + if (!amount.isZero()) { + checkResult = checkDeposit(amount) + } } - } - BankAccountRow( - account = state.account, - showMenu = false, - ) + BankAccountRow( + account = state.account, + showMenu = false, + ) - HorizontalDivider( - modifier = Modifier.padding(bottom = 16.dp), - ) + HorizontalDivider( + modifier = Modifier.padding(bottom = 16.dp), + ) - AnimatedVisibility(checkResult.maxDepositAmountRaw != null) { - checkResult.maxDepositAmountRaw?.let { - Text( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - text = if (checkResult.maxDepositAmountEffective == it) { - stringResource( - R.string.amount_available_transfer, - it.withSpec(spec), - ) - } else { - stringResource( - R.string.amount_available_transfer_fees, - it.withSpec(spec), - ) - }, - ) - } - } - - AmountCurrencyField( - modifier = Modifier - .padding(horizontal = 16.dp) - .fillMaxWidth(), - amount = amount.withSpec(spec), - onAmountChanged = { amount = it }, - editableCurrency = true, - currencies = currencies, - isError = checkResult !is CheckDepositResult.Success, - label = { Text(stringResource(R.string.amount_deposit)) }, - supportingText = { - val res = checkResult - if (res is CheckDepositResult.InsufficientBalance && res.maxAmountEffective != null) { + AnimatedVisibility(checkResult.maxDepositAmountRaw != null) { + checkResult.maxDepositAmountRaw?.let { Text( - stringResource( - R.string.payment_balance_insufficient_max, - res.maxAmountEffective.withSpec(spec), - ) + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + text = if (checkResult.maxDepositAmountEffective == it) { + stringResource( + R.string.amount_available_transfer, + it.withSpec(spec), + ) + } else { + stringResource( + R.string.amount_available_transfer_fees, + it.withSpec(spec), + ) + }, ) } } - ) - AnimatedVisibility(visible = checkResult is CheckDepositResult.Success) { - val res = checkResult as? CheckDepositResult.Success ?: return@AnimatedVisibility + AmountCurrencyField( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + amount = amount.withSpec(spec), + onAmountChanged = { amount = it }, + editableCurrency = true, + currencies = currencies, + isError = checkResult !is CheckDepositResult.Success, + label = { Text(stringResource(R.string.amount_deposit)) }, + supportingText = { + val res = checkResult + if (res is CheckDepositResult.InsufficientBalance) { + Text(stringResource(R.string.payment_balance_insufficient)) + } + } + ) - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = CenterHorizontally, - ) { - val totalAmount = res.totalDepositCost - val effectiveAmount = res.effectiveDepositAmount - if (totalAmount > effectiveAmount) { - val fee = totalAmount - effectiveAmount + AnimatedVisibility(visible = checkResult is CheckDepositResult.Success) { + val res = checkResult as? CheckDepositResult.Success ?: return@AnimatedVisibility - TransactionAmountComposable( - label = stringResource(R.string.amount_fee), - amount = fee.withSpec(amount.spec), - amountType = Negative, - ) - } + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = CenterHorizontally, + ) { + val totalAmount = res.totalDepositCost + val effectiveAmount = res.effectiveDepositAmount + if (totalAmount > effectiveAmount) { + val fee = totalAmount - effectiveAmount + + TransactionAmountComposable( + label = stringResource(R.string.amount_fee), + amount = fee.withSpec(amount.spec), + amountType = Negative, + ) - TransactionAmountComposable( - label = stringResource(R.string.amount_send), - amount = effectiveAmount.withSpec(amount.spec), - amountType = Positive, - ) + TransactionAmountComposable( + label = stringResource(R.string.amount_send), + amount = effectiveAmount.withSpec(amount.spec), + amountType = Positive, + ) + } + } } - } - val focusManager = LocalFocusManager.current - Button( - modifier = Modifier.padding(16.dp), - enabled = checkResult is CheckDepositResult.Success, - onClick = { - focusManager.clearFocus() - onMakeDeposit(amount) - }, - ) { - Text(stringResource(R.string.send_deposit_create_button)) + BottomInsetsSpacer() } - BottomInsetsSpacer() + BottomButtonBox(Modifier.fillMaxWidth()) { + val focusManager = LocalFocusManager.current + Button( + modifier = Modifier + .systemBarsPaddingBottom(), + enabled = checkResult is CheckDepositResult.Success, + onClick = { + focusManager.clearFocus() + onMakeDeposit(amount) + }, + ) { + Text(stringResource(R.string.send_deposit_create_button)) + } + } } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt @@ -21,6 +21,7 @@ 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.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -43,7 +44,6 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.serialization.json.JsonPrimitive @@ -57,8 +57,10 @@ import net.taler.wallet.balances.ScopeInfo 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.TalerSurface import net.taler.wallet.exchanges.ExchangeTosStatus +import net.taler.wallet.systemBarsPaddingBottom import net.taler.wallet.transactions.TransactionInfoComposable import net.taler.wallet.useDebounce import kotlin.random.Random @@ -111,129 +113,139 @@ fun OutgoingPullIntroComposable( onCreateInvoice: (amount: AmountScope, subject: String, hours: Long, exchangeBaseUrl: String) -> Unit, onTosAccept: (exchangeBaseUrl: String) -> Unit, ) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .verticalScroll(scrollState), - horizontalAlignment = CenterHorizontally, - ) { - var subject by rememberSaveable { mutableStateOf("") } - var amount by remember { - val scope = defaultScope ?: scopes[0] - val currency = scope.currency - mutableStateOf(AmountScope(Amount.zero(currency), scope)) - } - val selectedSpec = remember(amount.scope) { getCurrencySpec(amount.scope) } - var checkResult by remember { mutableStateOf<CheckPeerPullCreditResult?>(null) } + var subject by rememberSaveable { mutableStateOf("") } + var amount by remember { + val scope = defaultScope ?: scopes[0] + val currency = scope.currency + mutableStateOf(AmountScope(Amount.zero(currency), scope)) + } + val selectedSpec = remember(amount.scope) { getCurrencySpec(amount.scope) } + var checkResult by remember { mutableStateOf<CheckPeerPullCreditResult?>(null) } + val res = checkResult - amount.useDebounce { - checkResult = checkPeerPullCredit(it) - } + var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) } + var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) } - LaunchedEffect(Unit) { - checkResult = checkPeerPullCredit(amount) - } - - AmountScopeField( - modifier = Modifier - .padding(bottom = 16.dp) - .fillMaxWidth(), - amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)), - scopes = scopes, - readOnly = false, - onAmountChanged = { amount = it }, - isError = amount.amount.isZero(), - label = { Text(stringResource(R.string.amount_receive)) }, - ) + amount.useDebounce { + checkResult = checkPeerPullCredit(it) + } - OutlinedTextField( - modifier = Modifier.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, - ) - } - ) + LaunchedEffect(Unit) { + checkResult = checkPeerPullCredit(amount) + } - Text( + Column( + Modifier + .fillMaxSize() + .imePadding(), + ) { + Column( modifier = Modifier .fillMaxWidth() - .padding(top = 5.dp), - color = if (subject.isBlank()) MaterialTheme.colorScheme.error else Color.Unspecified, - text = stringResource(R.string.char_count, subject.length, MAX_LENGTH_SUBJECT), - textAlign = TextAlign.End, - ) + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = CenterHorizontally, + ) { + AmountScopeField( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)), + scopes = scopes, + readOnly = false, + 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)) + }, + ) - val res = checkResult - 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, + ) + } + } + + checkResult?.exchangeBaseUrl?.let { exchangeBaseUrl -> + TransactionInfoComposable( + label = stringResource(id = R.string.withdraw_exchange), + info = cleanExchange(exchangeBaseUrl), ) } - } - 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 } - var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) } - var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) } - ExpirationComposable( - modifier = Modifier.padding(top = 8.dp, bottom = 16.dp), - option = option, - hours = hours, - onOptionChange = { option = it } - ) { hours = it } + BottomInsetsSpacer() + } - Button( - modifier = Modifier.padding(16.dp), - enabled = subject.isNotBlank() && res != null, - onClick = { - val ex = res?.exchangeBaseUrl ?: error("clickable without exchange") - if (res.tosStatus == ExchangeTosStatus.Accepted) { - onCreateInvoice( - amount, - subject, - hours, - ex - ) - } else onTosAccept(ex) - }, - ) { - if (checkResult != null && checkResult?.tosStatus != ExchangeTosStatus.Accepted) { - Text(text = stringResource(R.string.exchange_tos_accept)) - } else { - Text(text = stringResource(R.string.receive_peer_create_button)) + BottomButtonBox(Modifier.fillMaxWidth()) { + Button( + modifier = Modifier + .systemBarsPaddingBottom(), + enabled = subject.isNotBlank() && res != null, + onClick = { + val ex = res?.exchangeBaseUrl ?: error("clickable without exchange") + if (res.tosStatus == ExchangeTosStatus.Accepted) { + onCreateInvoice( + amount, + subject, + hours, + ex + ) + } else onTosAccept(ex) + }, + ) { + if (checkResult != null && checkResult?.tosStatus != ExchangeTosStatus.Accepted) { + Text(text = stringResource(R.string.exchange_tos_accept)) + } else { + Text(text = stringResource(R.string.receive_peer_create_button)) + } } } - - BottomInsetsSpacer() } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt @@ -18,7 +18,9 @@ package net.taler.wallet.peer import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -51,12 +53,14 @@ import net.taler.wallet.balances.ScopeInfo 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.TalerSurface import net.taler.wallet.exchanges.ExchangeTosStatus import net.taler.wallet.payment.stringResId import net.taler.wallet.peer.CheckFeeResult.InsufficientBalance import net.taler.wallet.peer.CheckFeeResult.None import net.taler.wallet.peer.CheckFeeResult.Success +import net.taler.wallet.systemBarsPaddingBottom import net.taler.wallet.transactions.TransactionInfoComposable import net.taler.wallet.useDebounce import kotlin.random.Random @@ -92,146 +96,161 @@ fun OutgoingPushIntroComposable( getFees: suspend (amount: AmountScope) -> CheckFeeResult?, onSend: (amount: AmountScope, summary: String, hours: Long) -> Unit, ) { - val scrollState = rememberScrollState() - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .verticalScroll(scrollState), - horizontalAlignment = CenterHorizontally, - ) { - var amount by remember { - val scope = defaultScope ?: scopes[0] - val currency = scope.currency - mutableStateOf(AmountScope(Amount.zero(currency), scope)) - } - val selectedSpec = remember(amount.scope) { getCurrencySpec(amount.scope) } - var feeResult by remember { mutableStateOf<CheckFeeResult>(None()) } + var amount by remember { + val scope = defaultScope ?: scopes[0] + val currency = scope.currency + mutableStateOf(AmountScope(Amount.zero(currency), scope)) + } + val selectedSpec = remember(amount.scope) { getCurrencySpec(amount.scope) } + var feeResult by remember { mutableStateOf<CheckFeeResult>(None()) } + var subject by rememberSaveable { mutableStateOf("") } - amount.useDebounce { - feeResult = getFees(it) ?: None() - } + var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) } + var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) } - LaunchedEffect(Unit) { - feeResult = getFees(amount) ?: None() - } + amount.useDebounce { + feeResult = getFees(it) ?: None() + } - AnimatedVisibility(feeResult.maxDepositAmountRaw != null) { - feeResult.maxDepositAmountRaw?.let { - Text( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ), - text = if (feeResult.maxDepositAmountEffective == it) { - stringResource( - R.string.amount_available_transfer, - it.withSpec(selectedSpec), - ) - } else { - stringResource( - R.string.amount_available_transfer_fees, - it.withSpec(selectedSpec), - ) - }, - ) + LaunchedEffect(Unit) { + feeResult = getFees(amount) ?: None() + } + + Column( + Modifier + .fillMaxSize() + .imePadding(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .verticalScroll(rememberScrollState()), + horizontalAlignment = CenterHorizontally, + ) { + AnimatedVisibility(feeResult.maxDepositAmountRaw != null) { + feeResult.maxDepositAmountRaw?.let { + Text( + modifier = Modifier.padding(16.dp), + text = if (feeResult.maxDepositAmountEffective == it) { + stringResource( + R.string.amount_available_transfer, + it.withSpec(selectedSpec), + ) + } else { + stringResource( + R.string.amount_available_transfer_fees, + it.withSpec(selectedSpec), + ) + }, + ) + } } - } - AmountScopeField( - modifier = Modifier.fillMaxWidth(), - amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)), - scopes = scopes, - readOnly = false, - onAmountChanged = { amount = it }, - label = { Text(stringResource(R.string.amount_send)) }, - isError = amount.amount.isZero() || feeResult is InsufficientBalance, - supportingText = { - when (val res = feeResult) { - is Success -> if (res.amountEffective > res.amountRaw) { - val fee = res.amountEffective - res.amountRaw - Text( - text = stringResource( - id = R.string.payment_fee, - fee.withSpec(selectedSpec) - ), - softWrap = false, - color = MaterialTheme.colorScheme.error, - ) - } + AmountScopeField( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + amount = amount.copy(amount = amount.amount.withSpec(selectedSpec)), + scopes = scopes, + readOnly = false, + onAmountChanged = { amount = it }, + label = { Text(stringResource(R.string.amount_send)) }, + isError = amount.amount.isZero() || feeResult is InsufficientBalance, + supportingText = { + when (val res = feeResult) { + is Success -> if (res.amountEffective > res.amountRaw) { + val fee = res.amountEffective - res.amountRaw + Text( + text = stringResource( + id = R.string.payment_fee, + fee.withSpec(selectedSpec) + ), + softWrap = false, + color = MaterialTheme.colorScheme.error, + ) + } - is InsufficientBalance -> { - Text(stringResource(res.causeHint?.stringResId() - ?: R.string.payment_balance_insufficient)) - } + is InsufficientBalance -> { + Text( + stringResource( + res.causeHint?.stringResId() + ?: R.string.payment_balance_insufficient + ) + ) + } - else -> {} + else -> {} + } } - } - ) + ) - var subject by rememberSaveable { mutableStateOf("") } - OutlinedTextField( - modifier = Modifier - .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)) - }, - ) + 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)) + }, + ) - 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, + ) - var option by rememberSaveable { mutableStateOf(DEFAULT_EXPIRY) } - var hours by rememberSaveable { mutableLongStateOf(DEFAULT_EXPIRY.hours) } - ExpirationComposable( - modifier = Modifier.padding(vertical = 8.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 } - AnimatedVisibility(feeResult is Success) { - (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), - ) + AnimatedVisibility(feeResult is Success) { + (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), + ) + } } } - } - Button( - enabled = feeResult is Success && subject.isNotBlank(), - onClick = { onSend(amount, subject, hours) }, - ) { - Text(text = stringResource(R.string.send_peer_create_button)) + BottomInsetsSpacer() } - BottomInsetsSpacer() + BottomButtonBox(Modifier.fillMaxWidth()) { + Button( + modifier = Modifier.systemBarsPaddingBottom(), + enabled = feeResult is Success && subject.isNotBlank(), + onClick = { onSend(amount, subject, hours) }, + ) { + Text(text = stringResource(R.string.send_peer_create_button)) + } + } } }