commit 67c7b0c70eedaf7a3b1e702882ffd7a6fcdf2724 parent a5d00ce8afc00e9c3c5d39cfe21d0eaf7eb45e94 Author: Iván Ávalos <avalos@disroot.org> Date: Thu, 4 Dec 2025 17:00:09 +0100 [wallet] unify error screens and improve deposit flow Diffstat:
19 files changed, 202 insertions(+), 167 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/MainFragment.kt b/wallet/src/main/java/net/taler/wallet/MainFragment.kt @@ -121,6 +121,7 @@ class MainFragment: Fragment() { val online by model.networkManager.networkStatus.observeAsState(false) val balanceState by model.balanceManager.state.observeAsState(BalanceState.None) val viewMode by model.viewMode.collectAsStateLifecycleAware() + val devMode by model.devMode.observeAsState(false) val txResult by remember(viewMode) { val v = viewMode as? ViewMode.Transactions model.transactionManager.transactionsFlow(v?.selectedScope, stateFilter = v?.stateFilter) @@ -184,6 +185,7 @@ class MainFragment: Fragment() { state = balanceState, txResult = txResult, viewMode = viewMode, + devMode = devMode, onGetDemoMoneyClicked = { model.withdrawManager.withdrawTestBalance() Snackbar.make( diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AccountsFragment.kt b/wallet/src/main/java/net/taler/wallet/accounts/AccountsFragment.kt @@ -32,6 +32,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountBalance import androidx.compose.material.icons.filled.Add @@ -54,6 +56,7 @@ import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -79,10 +82,10 @@ import net.taler.wallet.accounts.ListBankAccountsResult.Error import net.taler.wallet.accounts.ListBankAccountsResult.None import net.taler.wallet.accounts.ListBankAccountsResult.Success import net.taler.wallet.compose.Avatar +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface import net.taler.wallet.showError -import net.taler.wallet.withdraw.WithdrawalError class BankAccountsFragment: Fragment() { private val model: MainViewModel by activityViewModels() @@ -98,6 +101,7 @@ class BankAccountsFragment: Fragment() { setContent { val accounts by model.accountManager.bankAccounts.collectAsState() + val devMode by model.devMode.observeAsState(false) TalerSurface { Scaffold( @@ -135,7 +139,11 @@ class BankAccountsFragment: Fragment() { } }, ) - is Error -> WithdrawalError(acc.error) + is Error -> ErrorComposable(acc.error, + devMode = devMode, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState())) } } } diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AddAccountComposable.kt b/wallet/src/main/java/net/taler/wallet/accounts/AddAccountComposable.kt @@ -23,7 +23,9 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Edit @@ -55,7 +57,6 @@ import net.taler.wallet.accounts.PaytoUri import net.taler.wallet.accounts.PaytoUriBitcoin 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 @@ -75,8 +76,11 @@ fun AddAccountComposable( val presetPaytoUri = remember(presetAccount) { presetAccount?.let { PaytoUri.parse(it.paytoUri) } } if (supportedWireTypes.isEmpty()) { - return AddAccountErrorComposable( - message = stringResource(R.string.send_deposit_no_methods_error), + return ErrorComposable( + error = TalerErrorInfo.makeCustomError( + stringResource(R.string.send_deposit_no_methods_error)), + modifier = Modifier.verticalScroll(rememberScrollState()), + devMode = false, onClose = onClose, ) } @@ -299,21 +303,6 @@ fun MakeDepositWireTypeChooser( } } -@Composable -fun AddAccountErrorComposable( - message: String, - onClose: () -> Unit, -) { - ErrorComposable( - error = TalerErrorInfo( - message = message, - code = TalerErrorCode.UNKNOWN, - ), - devMode = false, - onClose = onClose, - ) -} - @Preview @Composable fun PreviewAddAccountComposable() { diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AddAccountFragment.kt b/wallet/src/main/java/net/taler/wallet/accounts/AddAccountFragment.kt @@ -22,6 +22,7 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt @@ -32,6 +32,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material3.Badge @@ -54,21 +56,23 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount import net.taler.wallet.R +import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.balances.ScopeInfo.Auditor import net.taler.wallet.balances.ScopeInfo.Exchange import net.taler.wallet.balances.ScopeInfo.Global import net.taler.wallet.cleanExchange +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.cardPaddings import net.taler.wallet.donau.DonauSummaryItem -import net.taler.wallet.withdraw.WithdrawalError // TODO: rename to AssetsComposable @Composable fun BalancesComposable( innerPadding: PaddingValues, state: BalanceState, + devMode: Boolean, onGetDemoMoneyClicked: () -> Unit, onBalanceClicked: (balance: BalanceItem) -> Unit, onPendingClicked: (balance: BalanceItem) -> Unit, @@ -77,7 +81,11 @@ fun BalancesComposable( when (state) { is BalanceState.None -> {} is BalanceState.Loading -> LoadingScreen() - is BalanceState.Error -> WithdrawalError(state.error) + is BalanceState.Error -> ErrorComposable(state.error, + devMode = devMode, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState())) is BalanceState.Success -> if ( state.balances.isNotEmpty() || state.donauSummary.isNotEmpty()) { @@ -341,6 +349,24 @@ fun BalancesComposablePreview() { BalancesComposable( innerPadding = PaddingValues(0.dp), state = BalanceState.Success(balances, donauSummary), + devMode = false, + onGetDemoMoneyClicked = {}, + onBalanceClicked = {}, + onPendingClicked = {}, + onStatementClicked = {}, + ) + } +} + +@Preview +@Composable +fun BalancesComposableErrorPreview() { + TalerSurface { + BalancesComposable ( + innerPadding = PaddingValues(0.dp), + state = BalanceState.Error(TalerErrorInfo + .makeCustomError("Balances could not be loaded")), + devMode = false, onGetDemoMoneyClicked = {}, onBalanceClicked = {}, onPendingClicked = {}, @@ -356,6 +382,7 @@ fun BalancesComposableEmptyPreview() { BalancesComposable ( innerPadding = PaddingValues(0.dp), state = BalanceState.Success(listOf(), listOf()), + devMode = false, onGetDemoMoneyClicked = {}, onBalanceClicked = {}, onPendingClicked = {}, 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,16 +16,13 @@ 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 import androidx.compose.foundation.layout.Spacer -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 @@ -40,6 +37,7 @@ import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.serialization.ExperimentalSerializationApi @@ -52,14 +50,13 @@ import net.taler.wallet.backend.TalerErrorInfo @Composable fun ErrorComposable( error: TalerErrorInfo, + modifier: Modifier = Modifier, devMode: Boolean, onClose: (() -> Unit)? = null, ) { Column( - modifier = Modifier - .padding(16.dp) - .fillMaxSize() - .horizontalScroll(rememberScrollState()), + modifier = modifier + .padding(16.dp), horizontalAlignment = CenterHorizontally, verticalArrangement = Arrangement.Center, ) { @@ -97,6 +94,11 @@ fun ErrorComposable( } else { FontFamily.Default }, + textAlign = if (devMode) { + TextAlign.Start + } else { + TextAlign.Center + } ) Row( diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt @@ -45,8 +45,10 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R import net.taler.wallet.accounts.BankAccountRow import net.taler.wallet.accounts.KnownBankAccountInfo +import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.compose.AmountCurrencyField import net.taler.wallet.compose.BottomButtonBox +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.systemBarsPaddingBottom import net.taler.wallet.transactions.AmountType.Negative import net.taler.wallet.transactions.AmountType.Positive @@ -61,29 +63,18 @@ fun DepositAmountComposable( onMakeDeposit: (amount: Amount) -> Unit, onClose: () -> Unit, ) { - val availableScopes = remember(state.maxDepositable) { - state.maxDepositable.filterValues { it?.rawAmount?.isZero() == false } - } - - if (availableScopes.isEmpty()) { - MakeDepositErrorComposable( - message = "It is not possible to deposit to this account, please select another one", - onClose = onClose, - ) - return - } - Column( Modifier .fillMaxSize() .imePadding(), ) { - var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None()) } - // TODO: use scopeInfo instead of currency + var checkResult by remember { mutableStateOf<CheckDepositResult>(CheckDepositResult.None) } // TODO: handle unavailable scopes in UI (i.e. explain restrictions) - val currencies = remember(availableScopes) { availableScopes.keys.toList() } - var amount by remember(state.maxDepositable) { mutableStateOf(Amount.zero(currencies.first())) } - val spec = remember(amount) { getCurrencySpec(amount.currency) } + val currencies = state.account.currencies?.distinct() ?: emptyList() + var amount by remember(currencies) { + mutableStateOf(currencies.firstOrNull()?.let { Amount.zero(it) }) } + val spec = remember(amount) { amount?.let { getCurrencySpec(it.currency) } } + val maxDepositable = remember(amount) { amount?.let { state.maxDepositable[it.currency] } } Column( modifier = Modifier @@ -93,12 +84,6 @@ fun DepositAmountComposable( horizontalAlignment = CenterHorizontally, ) { - amount.useDebounce { - if (!amount.isZero()) { - checkResult = checkDeposit(amount) - } - } - BankAccountRow( account = state.account, showMenu = false, @@ -108,15 +93,33 @@ fun DepositAmountComposable( modifier = Modifier.padding(bottom = 16.dp), ) - AnimatedVisibility(checkResult.maxDepositAmountRaw != null) { - checkResult.maxDepositAmountRaw?.let { + if (currencies.isEmpty() || amount == null) { + ErrorComposable( + // FIXME: i18n string + error = TalerErrorInfo.makeCustomError( + "It is not possible to deposit to this account, please select another one"), + modifier = Modifier.weight(1f), + devMode = false, + onClose = onClose, + ) + return + } + + amount.useDebounce { + if (!amount!!.isZero()) { + checkResult = checkDeposit(amount!!) + } + } + + AnimatedVisibility(maxDepositable?.rawAmount != null) { + maxDepositable?.rawAmount?.let { Text( modifier = Modifier.padding( start = 16.dp, end = 16.dp, bottom = 16.dp, ), - text = if (checkResult.maxDepositAmountEffective == it) { + text = if (maxDepositable.effectiveAmount == it) { stringResource( R.string.amount_available_transfer, it.withSpec(spec), @@ -135,7 +138,7 @@ fun DepositAmountComposable( modifier = Modifier .padding(horizontal = 16.dp) .fillMaxWidth(), - amount = amount.withSpec(spec), + amount = amount!!.withSpec(spec), onAmountChanged = { amount = it }, editableCurrency = true, currencies = currencies, @@ -145,6 +148,9 @@ fun DepositAmountComposable( val res = checkResult if (res is CheckDepositResult.InsufficientBalance) { Text(stringResource(R.string.payment_balance_insufficient)) + } else if (res is CheckDepositResult.ExceedsLimit) { + Text(stringResource(R.string.amount_excess, + res.maxDepositAmountEffective)) } } ) @@ -163,13 +169,13 @@ fun DepositAmountComposable( TransactionAmountComposable( label = stringResource(R.string.amount_fee), - amount = fee.withSpec(amount.spec), + amount = fee.withSpec(amount?.spec), amountType = Negative, ) TransactionAmountComposable( label = stringResource(R.string.amount_send), - amount = effectiveAmount.withSpec(amount.spec), + amount = effectiveAmount.withSpec(amount?.spec), amountType = Positive, ) } @@ -181,13 +187,14 @@ fun DepositAmountComposable( BottomButtonBox(Modifier.fillMaxWidth()) { val focusManager = LocalFocusManager.current + val amount = amount Button( modifier = Modifier .systemBarsPaddingBottom(), - enabled = checkResult is CheckDepositResult.Success, + enabled = checkResult is CheckDepositResult.Success && amount != null, onClick = { focusManager.clearFocus() - onMakeDeposit(amount) + onMakeDeposit(amount!!) }, ) { Text(stringResource(R.string.send_deposit_create_button)) @@ -232,7 +239,6 @@ fun DepositAmountComposablePreview() { checkDeposit = { CheckDepositResult.Success( totalDepositCost = Amount.fromJSONString("KUDOS:10"), effectiveDepositAmount = Amount.fromJSONString("KUDOS:12"), - maxDepositAmountEffective = Amount.fromJSONString("KUDOS:12") ) }, onMakeDeposit = {}, getCurrencySpec = { null }, @@ -260,7 +266,6 @@ fun DepositAmountComposableErrorPreview() { checkDeposit = { CheckDepositResult.Success( totalDepositCost = Amount.fromJSONString("KUDOS:10"), effectiveDepositAmount = Amount.fromJSONString("KUDOS:12"), - maxDepositAmountEffective = Amount.fromJSONString("KUDOS:12") ) }, onMakeDeposit = {}, getCurrencySpec = { null }, diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositFragment.kt @@ -22,7 +22,12 @@ import android.view.View import android.view.ViewGroup import androidx.activity.compose.BackHandler import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -40,6 +45,7 @@ import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.showError import net.taler.wallet.accounts.ListBankAccountsResult.Success +import net.taler.wallet.compose.ErrorComposable class DepositFragment : Fragment() { private val model: MainViewModel by activityViewModels() @@ -68,6 +74,7 @@ class DepositFragment : Fragment() { TalerSurface { val state by depositManager.depositState.collectAsStateLifecycleAware() val knownBankAccounts by accountManager.bankAccounts.collectAsStateLifecycleAware() + val devMode by model.devMode.observeAsState(false) BackHandler(state is DepositState.AccountSelected) { depositManager.resetDepositState() @@ -78,11 +85,15 @@ class DepositFragment : Fragment() { LoadingScreen() } - is DepositState.Error -> { - MakeDepositErrorComposable(s.error.userFacingMsg) { + is DepositState.Error -> ErrorComposable(s.error, + devMode = devMode, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + onClose = { findNavController().popBackStack() - } - } + }, + ) is DepositState.Start -> { MakeDepositComposable( diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt @@ -23,9 +23,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import net.taler.common.Amount @@ -54,7 +54,7 @@ class DepositManager( if (!uriString.startsWith("payto://")) return false val u = Uri.parse(uriString) if (!u.authority.equals("iban", ignoreCase = true)) return false - return u.pathSegments.size >= 1 + return u.pathSegments.isNotEmpty() } @UiThread @@ -65,24 +65,28 @@ class DepositManager( } suspend fun checkDepositFees(paytoUri: String, amount: Amount): CheckDepositResult { - val max = getMaxDepositAmount(amount.currency, paytoUri) - var response: CheckDepositResult = CheckDepositResult.None( - maxDepositAmountEffective = max?.effectiveAmount, - maxDepositAmountRaw = max?.rawAmount, - ) + var response: CheckDepositResult = CheckDepositResult.None api.request("checkDeposit", CheckDepositResponse.serializer()) { put("depositPaytoUri", paytoUri) put("amount", amount.toJSONString()) }.onSuccess { - response = CheckDepositResult.Success( - totalDepositCost = it.totalDepositCost, - effectiveDepositAmount = it.effectiveDepositAmount, - kycSoftLimit = it.kycSoftLimit, - kycHardLimit = it.kycHardLimit, - kycExchanges = it.kycExchanges, - maxDepositAmountEffective = max?.effectiveAmount, - maxDepositAmountRaw = max?.rawAmount, - ) + runBlocking { + val max = getMaxDepositAmount(amount.currency, paytoUri) + response = if (max?.effectiveAmount != null && amount > max.effectiveAmount) { + CheckDepositResult.ExceedsLimit( + maxDepositAmountEffective = max.effectiveAmount, + maxDepositAmountRaw = max.rawAmount, + ) + } else { + CheckDepositResult.Success( + totalDepositCost = it.totalDepositCost, + effectiveDepositAmount = it.effectiveDepositAmount, + kycSoftLimit = it.kycSoftLimit, + kycHardLimit = it.kycHardLimit, + kycExchanges = it.kycExchanges, + ) + } + } }.onError { error -> Log.e(TAG, "Error checkDeposit $error") if (error.code == WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE) { @@ -98,8 +102,6 @@ class DepositManager( response = CheckDepositResult.InsufficientBalance( maxAmountEffective = maxAmountEffective, maxAmountRaw = maxAmountRaw, - maxDepositAmountEffective = max?.effectiveAmount, - maxDepositAmountRaw = max?.rawAmount, ) } } @@ -230,19 +232,16 @@ data class CheckDepositResponse( @Serializable sealed class CheckDepositResult { - abstract val maxDepositAmountEffective: Amount? - abstract val maxDepositAmountRaw: Amount? - - data class None( - override val maxDepositAmountEffective: Amount? = null, - override val maxDepositAmountRaw: Amount? = null, - ): CheckDepositResult() + data object None: CheckDepositResult() data class InsufficientBalance( val maxAmountEffective: Amount?, val maxAmountRaw: Amount?, - override val maxDepositAmountEffective: Amount?, - override val maxDepositAmountRaw: Amount? = null, + ): CheckDepositResult() + + data class ExceedsLimit( + val maxDepositAmountEffective: Amount, + val maxDepositAmountRaw: Amount, ): CheckDepositResult() data class Success( @@ -251,8 +250,6 @@ sealed class CheckDepositResult { val kycSoftLimit: Amount? = null, val kycHardLimit: Amount? = null, val kycExchanges: List<String>? = null, - override val maxDepositAmountEffective: Amount?, - override val maxDepositAmountRaw: Amount? = null, ): CheckDepositResult() } diff --git a/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/MakeDepositComposable.kt @@ -32,9 +32,6 @@ import net.taler.wallet.BottomInsetsSpacer import net.taler.wallet.R 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.compose.ErrorComposable @Composable fun MakeDepositComposable( @@ -77,19 +74,4 @@ fun MakeDepositComposable( BottomInsetsSpacer() } } -} - -@Composable -fun MakeDepositErrorComposable( - message: String, - onClose: () -> Unit, -) { - 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/donau/DonauStatementFragment.kt b/wallet/src/main/java/net/taler/wallet/donau/DonauStatementFragment.kt @@ -21,12 +21,16 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -57,14 +61,17 @@ class DonauStatementFragment: Fragment() { setContent { TalerSurface { val status by model.donauManager.donauStatementsStatus.collectAsStateLifecycleAware() - val devMode by model.devMode.observeAsState() + val devMode by model.devMode.observeAsState(false) when (val s = status) { is GetDonauStatementsStatus.None, is GetDonauStatementsStatus.Loading -> LoadingScreen() is GetDonauStatementsStatus.Error -> ErrorComposable( error = s.error, - devMode = devMode == true, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + devMode = devMode, ) is GetDonauStatementsStatus.Success -> if (s.statements.isEmpty()) { diff --git a/wallet/src/main/java/net/taler/wallet/donau/SetDonauFragment.kt b/wallet/src/main/java/net/taler/wallet/donau/SetDonauFragment.kt @@ -25,8 +25,10 @@ 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.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -110,6 +112,9 @@ class SetDonauFragment: Fragment() { ) is Error -> ErrorComposable( error = status.error, + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), devMode = devMode == true, ) } diff --git a/wallet/src/main/java/net/taler/wallet/main/MainComposable.kt b/wallet/src/main/java/net/taler/wallet/main/MainComposable.kt @@ -32,6 +32,7 @@ fun MainComposable( state: BalanceState, txResult: TransactionsResult, viewMode: ViewMode, + devMode: Boolean, onGetDemoMoneyClicked: () -> Unit, onBalanceClicked: (balance: BalanceItem) -> Unit, onPendingClicked: (balance: BalanceItem) -> Unit, @@ -44,6 +45,7 @@ fun MainComposable( is ViewMode.Assets -> BalancesComposable( innerPadding = innerPadding, state = state, + devMode = devMode, onGetDemoMoneyClicked = onGetDemoMoneyClicked, onBalanceClicked = onBalanceClicked, onPendingClicked = onPendingClicked, diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt @@ -77,7 +77,6 @@ fun OutgoingPullComposable( checkPeerPullCredit: suspend (amount: AmountScope, loading: Boolean) -> CheckPeerPullCreditResult?, onCreateInvoice: (amount: AmountScope, subject: String, hours: Long, exchangeBaseUrl: String) -> Unit, onTosAccept: (exchangeBaseUrl: String) -> Unit, - onClose: () -> Unit, ) { var subject by rememberSaveable { mutableStateOf("") } var amount by remember { @@ -138,7 +137,7 @@ fun OutgoingPullComposable( scopes = scopes, readOnly = false, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), - showAmount = !tosReview, + showAmount = !tosReview && state !is OutgoingError, showShortcuts = true, onAmountChanged = { amount = it.copy( @@ -163,7 +162,9 @@ fun OutgoingPullComposable( } if (state is OutgoingError) { - ErrorComposable(state.info, devMode, onClose) + ErrorComposable(state.info, + modifier = Modifier.weight(1f), + devMode = devMode) return@Column } @@ -299,7 +300,6 @@ fun PeerPullComposableCreatingPreview() { checkPeerPullCredit = { _, _ -> null }, onCreateInvoice = { _, _, _, _ -> }, onTosAccept = {}, - onClose = {}, ) } } @@ -321,7 +321,6 @@ fun PeerPullComposableCheckingPreview() { checkPeerPullCredit = { _, _ -> null }, onCreateInvoice = { _, _, _, _ -> }, onTosAccept = {}, - onClose = {}, ) } } @@ -345,7 +344,6 @@ fun PeerPullComposableCheckedPreview() { checkPeerPullCredit = { _, _ -> null }, onCreateInvoice = { _, _, _, _ -> }, onTosAccept = {}, - onClose = {}, ) } } @@ -369,7 +367,6 @@ fun PeerPullComposableErrorPreview() { checkPeerPullCredit = { _, _ -> null }, onCreateInvoice = { _, _, _, _ -> }, onTosAccept = {}, - onClose = {}, ) } } \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullFragment.kt @@ -74,9 +74,6 @@ class OutgoingPullFragment : Fragment() { loading = loading, ) }, - onClose = { - findNavController().navigate(R.id.action_nav_peer_pull_to_nav_main) - } ) } } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt @@ -80,25 +80,27 @@ fun OutgoingPushComposable( 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 -> LoadingScreen() - is OutgoingIntro, is OutgoingChecked -> OutgoingPushIntroComposable( + is OutgoingIntro, is OutgoingChecked, is OutgoingError -> OutgoingPushIntroComposable( + state = state, defaultScope = defaultScope, scopes = scopes, + devMode = devMode, getCurrencySpec = getCurrencySpec, getFees = getFees, onSend = onSend, ) - is OutgoingError -> ErrorComposable(state.info, devMode, onClose) } } @Composable fun OutgoingPushIntroComposable( + 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, @@ -169,6 +171,7 @@ fun OutgoingPushIntroComposable( scopes = scopes, readOnly = false, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + showAmount = state !is OutgoingError, showShortcuts = true, onAmountChanged = { amount = it.copy(userInput = true) @@ -208,6 +211,13 @@ fun OutgoingPushIntroComposable( } ) + if (state is OutgoingError) { + ErrorComposable(state.info, + modifier = Modifier.weight(1f), + devMode = devMode) + return@Column + } + AnimatedVisibility(feeResult is Success && !amount.amount.isZero()) { Column( modifier = Modifier.padding(bottom = 8.dp), @@ -311,7 +321,6 @@ fun PeerPushComposableCreatingPreview() { exchangeBaseUrl = "https://exchange.demo.taler.net" ) }, onSend = { _, _, _ -> }, - onClose = {}, ) } } @@ -338,7 +347,6 @@ fun PeerPushComposableCheckingPreview() { exchangeBaseUrl = "https://exchange.demo.taler.net" ) }, onSend = { _, _, _ -> }, - onClose = {}, ) } } @@ -367,7 +375,6 @@ fun PeerPushComposableCheckedPreview() { exchangeBaseUrl = "https://exchange.demo.taler.net" ) }, onSend = { _, _, _ -> }, - onClose = {}, ) } } @@ -395,7 +402,6 @@ fun PeerPushComposableErrorPreview() { exchangeBaseUrl = "https://exchange.demo.taler.net" ) }, onSend = { _, _, _ -> }, - onClose = {}, ) } } \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushFragment.kt @@ -30,7 +30,6 @@ import androidx.fragment.app.activityViewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import androidx.navigation.findNavController import androidx.navigation.fragment.findNavController import kotlinx.coroutines.launch import net.taler.wallet.MainViewModel @@ -81,9 +80,6 @@ class OutgoingPushFragment : Fragment() { peerManager.checkPeerPushFees(it.amount, restrictScope = it.scope) }, onSend = this@OutgoingPushFragment::onSend, - onClose = { - findNavController().navigate(R.id.action_nav_peer_push_to_nav_main) - } ) } } diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawFragment.kt @@ -21,19 +21,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment.Companion.Center -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -50,7 +41,6 @@ import net.taler.wallet.MainViewModel import net.taler.wallet.R import net.taler.wallet.main.ViewMode import net.taler.wallet.backend.BackendManager -import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.AmountScope import net.taler.wallet.compose.EmptyComposable @@ -244,22 +234,4 @@ class PromptWithdrawFragment: Fragment() { exchangeBaseUrl = exchange.exchangeBaseUrl, ) } -} - -@Composable -fun WithdrawalError( - error: TalerErrorInfo, -) { - Box( - modifier = Modifier - .padding(16.dp) - .fillMaxSize(), - contentAlignment = Center, - ) { - Text( - text = error.userFacingMsg, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.error, - ) - } } \ No newline at end of file diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt @@ -50,11 +50,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount import net.taler.wallet.R +import net.taler.wallet.backend.TalerErrorCode +import net.taler.wallet.backend.TalerErrorInfo 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.ErrorComposable import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface import net.taler.wallet.compose.WarningLabel @@ -162,7 +165,9 @@ fun WithdrawalShowInfo( LoadingScreen(Modifier.weight(1f)) return } else if (status.status == Error && status.error != null) { - WithdrawalError(status.error) + ErrorComposable(status.error, + modifier = Modifier.weight(1f), + devMode = devMode) return } else if (status.isCashAcceptor) { WarningLabel( @@ -342,7 +347,7 @@ private fun buildPreviewWithdrawStatus( talerWithdrawUri = "taler://", exchangeBaseUrl = "exchange.head.taler.net", transactionId = "tx:343434", - error = null, + error = TalerErrorInfo(TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE), uriInfo = WithdrawalDetailsForUri( amount = null, currency = "KUDOS", @@ -412,7 +417,31 @@ fun WithdrawalShowInfoTosReviewPreview() { WithdrawalShowInfo( status = buildPreviewWithdrawStatus(TosReviewRequired), devMode = true, -// defaultScope = ScopeInfo.Exchange("KUDOS", "https://exchange.demo.taler.net/"), + initialAmountScope = AmountScope( + amount = Amount.fromJSONString("KUDOS:10.10"), + scope = ScopeInfo.Exchange("KUDOS", "https://exchange.demo.taler.net/"), + ), + editableScope = true, + scopes = listOf( + ScopeInfo.Exchange("KUDOS", "https://exchange.demo.taler.net/"), + ScopeInfo.Exchange("TESTKUDOS", "https://exchange.test.taler.net/"), + ScopeInfo.Global("CHF"), + ), + onSelectExchange = {}, + onSelectAmount = { _, _ -> }, + onTosReview = {}, + onConfirm = {}, + ) + } +} + +@Preview +@Composable +fun WithdrawalShowInfoErrorPreview() { + TalerSurface { + WithdrawalShowInfo( + status = buildPreviewWithdrawStatus(Error), + devMode = true, initialAmountScope = AmountScope( amount = Amount.fromJSONString("KUDOS:10.10"), scope = ScopeInfo.Exchange("KUDOS", "https://exchange.demo.taler.net/"),