commit bb89662a54c8d4d020ba99ce6404c4c52748090a parent 23529967362047c373abbe24ada108334fe41abb Author: Iván Ávalos <avalos@disroot.org> Date: Mon, 26 Oct 2026 15:25:15 +0100 [wallet] show failed data queries as errors Diffstat:
12 files changed, 200 insertions(+), 52 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/accounts/AddBankAccountScreen.kt b/wallet/src/main/java/net/taler/wallet/accounts/AddBankAccountScreen.kt @@ -22,6 +22,7 @@ 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.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -31,6 +32,7 @@ import androidx.compose.ui.res.stringResource import kotlinx.coroutines.launch import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface @@ -50,18 +52,25 @@ fun AddBankAccountScreen( var depositWireTypes by remember { mutableStateOf<GetDepositWireTypesResponse?>(null) } var bankAccount by remember { mutableStateOf<KnownBankAccountInfo?>(null) } + var loadError by remember { mutableStateOf<TalerErrorInfo?>(null) } + var reload by remember { mutableStateOf(0) } - LaunchedEffect(bankAccountId) { + LaunchedEffect(bankAccountId, reload) { + depositWireTypes = null + bankAccount = null + loadError = null if (bankAccountId != null) { bankAccount = accountManager.getBankAccountById(bankAccountId) { error -> - onShowError(error) + loadError = error } } + depositManager.getDepositWireTypes( + onSuccess = { depositWireTypes = it }, + onError = { loadError = it }, + ) } - LaunchedEffect(Unit) { - depositWireTypes = depositManager.getDepositWireTypes() - } + val devMode by model.devMode.observeAsState(false) TalerSurface { GlobalScaffold( @@ -76,13 +85,25 @@ fun AddBankAccountScreen( } }, ) { paddingValues -> - if (depositWireTypes == null || (bankAccountId != null && bankAccount == null)) { - LoadingScreen(Modifier.padding(paddingValues)) - } else { - AddAccountComposable( + val error = loadError + val currentWireTypes = depositWireTypes + when { + error != null -> ErrorComposable( + error = error, + devMode = devMode, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + scrollable = true, + onRetry = { reload += 1 }, + onClose = onNavigateBack, + ) + + currentWireTypes != null && + (bankAccountId == null || bankAccount != null) -> AddAccountComposable( modifier = Modifier.padding(paddingValues), presetAccount = bankAccount, - depositWireTypes = depositWireTypes!!, + depositWireTypes = currentWireTypes, validateIban = depositManager::validateIban, onSubmit = { paytoUri, label -> coroutineScope.launch { @@ -98,6 +119,8 @@ fun AddBankAccountScreen( }, onClose = onNavigateBack, ) + + else -> LoadingScreen(Modifier.padding(paddingValues)) } } } diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt @@ -82,8 +82,11 @@ class BalanceManager( Log.e(TAG, "Error retrieving balances: $it") mState.postValue(BalanceState.Error(it)) }.onSuccess { res -> + var currencySpecError: TalerErrorInfo? = null val balances = res.balances.map { balance -> - val spec = exchangeManager.getCurrencySpecification(balance.scopeInfo) + val spec = exchangeManager.getCurrencySpecification(balance.scopeInfo) { + currencySpecError = it + } balance.copy( available = balance.available.withSpec(spec), pendingIncoming = balance.pendingIncoming.withSpec(spec), @@ -100,6 +103,11 @@ class BalanceManager( ) } ?: emptyList() + currencySpecError?.let { error -> + mState.postValue(BalanceState.Error(error)) + return@onSuccess + } + mBalances.postValue(balances) mState.postValue(BalanceState.Success( balances = balances, diff --git a/wallet/src/main/java/net/taler/wallet/contacts/ContactScreen.kt b/wallet/src/main/java/net/taler/wallet/contacts/ContactScreen.kt @@ -64,6 +64,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect 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,6 +80,7 @@ import net.taler.wallet.NavigateCallback import net.taler.wallet.R import net.taler.wallet.WalletDestination import net.taler.wallet.balances.ScopeInfo +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.QrCodeParams import net.taler.wallet.compose.QrCodeUriComposable import net.taler.wallet.compose.TalerSurface @@ -97,6 +99,7 @@ fun ContactScreen( val contactManager = model.contactManager val contactResult by contactManager.contacts.collectAsState() val lookupResult by contactManager.lookupResult.collectAsState() + val devMode by model.devMode.observeAsState(false) var showAddDialog by remember { mutableStateOf(false) } var showNicknameDialog by remember { mutableStateOf(false) } var pendingContact by remember { mutableStateOf<PendingContact?>(null) } @@ -195,7 +198,13 @@ fun ContactScreen( when (val res = contactResult) { is ContactResult.Loading -> Text("Loading...", modifier = Modifier.padding(16.dp)) - is ContactResult.Error -> Text("Error: ${res.error}", modifier = Modifier.padding(16.dp)) + is ContactResult.Error -> ErrorComposable( + error = res.error, + devMode = devMode, + modifier = Modifier.weight(1f), + scrollable = true, + onRetry = { contactManager.loadContacts() }, + ) is ContactResult.Success -> { val filteredContacts = res.contacts.filter { contact -> contact.petname.fuzzyMatches(searchQuery) || diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt @@ -66,6 +66,7 @@ fun DepositAmountComposable( checkDeposit: suspend (amount: Amount) -> CheckDepositResult, onMakeDeposit: (amount: Amount) -> Unit, onClose: () -> Unit, + devMode: Boolean = false, ) { Column( modifier @@ -128,7 +129,20 @@ fun DepositAmountComposable( } val spec = remember(amount) { getCurrencySpec(amount!!.currency) } - val maxDepositable = remember(amount) { state.maxDepositable[amount!!.currency] } + val maxDepositable = remember(amount) { state.maxDepositable[amount!!.currency] } + + state.maxDepositError?.let { error -> + if (maxDepositable == null) { + ErrorComposable( + error = error, + modifier = Modifier.fillMaxWidth(), + devMode = devMode, + scrollable = false, + onClose = onClose, + ) + return + } + } amount.useDebounce { debouncedAmount -> if (debouncedAmount == null || debouncedAmount.isZero()) { diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt @@ -35,6 +35,7 @@ import net.taler.wallet.accounts.PaytoUriIban import net.taler.wallet.accounts.PaytoUriTalerBank import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE +import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.backend.WalletResponse import net.taler.wallet.balances.BalanceManager @@ -66,9 +67,12 @@ class DepositManager( @UiThread fun selectAccount(account: KnownBankAccountInfo) = scope.launch { - getMaxDepositableForPayto(account.paytoUri).let { response -> - mDepositState.value = DepositState.AccountSelected(account, response) - } + val (maxDepositable, error) = getMaxDepositableForPayto(account.paytoUri) + mDepositState.value = DepositState.AccountSelected( + account = account, + maxDepositable = maxDepositable, + maxDepositError = error, + ) } suspend fun checkDepositFees(paytoUri: String, amount: Amount): CheckDepositResult { @@ -127,21 +131,22 @@ class DepositManager( private suspend fun getMaxDepositAmount( currency: String, depositPaytoUri: String?, + onError: (TalerErrorInfo) -> Unit = {}, ): GetMaxDepositAmountResponse? { var response: GetMaxDepositAmountResponse? = null api.request("getMaxDepositAmount", GetMaxDepositAmountResponse.serializer()) { depositPaytoUri?.let { put("depositPaytoUri", it) } put("currency", currency) - }.onError { error -> - Log.e(TAG, "Error getMaxDepositAmount $error") }.onSuccess { val spec = getCurrencySpec(currency) response = it.copy( effectiveAmount = it.effectiveAmount.withSpec(spec), rawAmount = it.rawAmount.withSpec(spec), ) + }.onError { error -> + Log.e(TAG, "Error getMaxDepositAmount $error") + onError(error) } - return response } @@ -186,26 +191,31 @@ class DepositManager( suspend fun getDepositWireTypes( currency: String? = null, scopeInfo: ScopeInfo? = null, - ): GetDepositWireTypesResponse? { - var result: GetDepositWireTypesResponse? = null + onSuccess: (GetDepositWireTypesResponse) -> Unit, + onError: (TalerErrorInfo) -> Unit, + ) { api.request("getDepositWireTypes", GetDepositWireTypesResponse.serializer()) { scopeInfo?.let { put("scopeInfo", JSONObject(BackendManager.json.encodeToString(it))) } currency?.let { put("currency", it) } this + }.onSuccess { + onSuccess(it) }.onError { Log.e(TAG, "Error getDepositWireTypes $it") - }.onSuccess { - result = it + onError(it) } - return result } private suspend fun getMaxDepositableForPayto( paytoUri: String, - ): Map<String, GetMaxDepositAmountResponse?> { - return balanceManager.getCurrencies().associateWith { currency -> - getMaxDepositAmount(currency, paytoUri) + ): Pair<Map<String, GetMaxDepositAmountResponse?>, TalerErrorInfo?> { + var error: TalerErrorInfo? = null + val maxDepositable = balanceManager.getCurrencies().associateWith { currency -> + getMaxDepositAmount(currency, paytoUri) { + if (error == null) error = it + } } + return maxDepositable to error } } diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt @@ -111,12 +111,10 @@ fun DepositScreen( onClose = onNavigateBack, ) - is DepositState.Start -> { - MakeDepositComposable( + is DepositState.Start -> when (val accounts = knownBankAccounts) { + is ListBankAccountsResult.Success -> MakeDepositComposable( modifier = Modifier.padding(paddingValues), - knownBankAccounts = (knownBankAccounts as? ListBankAccountsResult.Success) - ?.accounts - ?: emptyList(), + knownBankAccounts = accounts.accounts, onAccountSelected = { account -> depositManager.selectAccount(account) }, @@ -124,6 +122,19 @@ fun DepositScreen( onNavigate(WalletDestination.BankAccounts(), false) } ) + + is ListBankAccountsResult.Error -> ErrorComposable( + error = accounts.error, + devMode = devMode, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + scrollable = true, + onRetry = { accountManager.listBankAccounts() }, + onClose = onNavigateBack, + ) + + ListBankAccountsResult.None -> LoadingScreen(Modifier.padding(paddingValues)) } is DepositState.AccountSelected -> { @@ -140,7 +151,8 @@ fun DepositScreen( }, onClose = { depositManager.resetDepositState() - } + }, + devMode = devMode, ) } } diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositState.kt @@ -25,6 +25,7 @@ sealed class DepositState { data class AccountSelected( val account: KnownBankAccountInfo, val maxDepositable: Map<String, GetMaxDepositAmountResponse?>, + val maxDepositError: TalerErrorInfo? = null, ) : DepositState() data object MakingDeposit : DepositState() diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt @@ -71,6 +71,7 @@ import net.taler.wallet.backend.WalletResponse import net.taler.wallet.balances.BalanceManager import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.EmptyComposable +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.main.MainViewModel @@ -88,6 +89,7 @@ fun ExchangeListScreen( val devMode by model.devMode.observeAsState(false) val addError by exchangeManager.addError.observeAsState() val listError by exchangeManager.listError.observeAsState() + val listErrorState by exchangeManager.listErrorState.observeAsState() val deleteError by exchangeManager.deleteError.observeAsState() val reloadError by exchangeManager.reloadError.observeAsState() val snackbarHostState = remember { SnackbarHostState() } @@ -111,8 +113,10 @@ fun ExchangeListScreen( } } - LaunchedEffect(listError) { - listError?.getIfNotConsumed()?.let { onShowError(it) } + LaunchedEffect(listError, exchanges) { + listError?.getIfNotConsumed()?.let { + if (exchanges.isNotEmpty()) onShowError(it) + } } LaunchedEffect(deleteError) { @@ -142,7 +146,16 @@ fun ExchangeListScreen( } ) { padding -> if (exchanges.isEmpty()) { - EmptyComposable( + listErrorState?.let { error -> + ErrorComposable( + error = error, + devMode = devMode, + modifier = Modifier.fillMaxSize().padding(padding), + scrollable = true, + onRetry = { exchangeManager.reloadList() }, + onClose = onNavigateBack, + ) + } ?: EmptyComposable( modifier = Modifier.fillMaxSize().padding(padding), message = stringResource(R.string.exchange_list_empty), ) diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -60,6 +60,9 @@ class ExchangeManager( private val mListError = MutableLiveData<Event<TalerErrorInfo>>() val listError: LiveData<Event<TalerErrorInfo>> = mListError + private val mListErrorState = MutableLiveData<TalerErrorInfo?>(null) + val listErrorState: LiveData<TalerErrorInfo?> = mListErrorState + private val mDeleteError = MutableLiveData<Event<TalerErrorInfo>>() val deleteError: LiveData<Event<TalerErrorInfo>> = mDeleteError @@ -76,10 +79,12 @@ class ExchangeManager( val response = api.request("listExchanges", ExchangeListResponse.serializer()) response.onError { mProgress.value = false + mListErrorState.value = it mListError.value = it.toEvent() }.onSuccess { Log.d(TAG, "Exchange list: ${it.exchanges}") mProgress.value = false + mListErrorState.value = null mExchanges.value = it.exchanges scope.launch { // load and cache all currency specs @@ -94,6 +99,10 @@ class ExchangeManager( return mExchanges } + fun reloadList() { + list() + } + fun add(exchangeUrl: String, onSuccess: (() -> Unit)? = null) = scope.launch { mProgress.value = true api.request<Unit>("addExchange") { @@ -275,7 +284,10 @@ class ExchangeManager( } } - suspend fun getCurrencySpecification(scopeInfo: ScopeInfo): CurrencySpecification? { + suspend fun getCurrencySpecification( + scopeInfo: ScopeInfo, + onError: ((TalerErrorInfo) -> Unit)? = null, + ): CurrencySpecification? { if (currencySpecs.containsKey(scopeInfo)) { return currencySpecs[scopeInfo] } @@ -290,6 +302,7 @@ class ExchangeManager( spec = it.currencySpecification }.onError { Log.e(TAG, "Error getting currency spec for scope $scopeInfo: $it") + onError?.invoke(it) } return spec } diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt @@ -221,6 +221,15 @@ fun OutgoingPushIntroComposable( } ) + if (feeResult is CheckFeeResult.Error) { + ErrorComposable( + error = (feeResult as CheckFeeResult.Error).error, + modifier = Modifier.fillMaxWidth(), + devMode = devMode, + scrollable = false, + ) + } + if (state is OutgoingError) { ErrorComposable( error = state.info, diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -72,6 +72,12 @@ sealed class CheckFeeResult { override val maxDepositAmountEffective: Amount? = null, override val maxDepositAmountRaw: Amount? = null, ): CheckFeeResult() + + data class Error( + val error: TalerErrorInfo, + override val maxDepositAmountEffective: Amount? = null, + override val maxDepositAmountRaw: Amount? = null, + ): CheckFeeResult() } @Serializable @@ -185,10 +191,23 @@ class PeerManager( exchangeBaseUrl: String? = null, restrictScope: ScopeInfo? = null, ): CheckFeeResult { - val max = getMaxPeerPushDebitAmount(amount.currency, exchangeBaseUrl, restrictScope = restrictScope) + var max: GetMaxPeerPushDebitAmountResponse? = null + var maxError: TalerErrorInfo? = null + getMaxPeerPushDebitAmount( + amount.currency, + exchangeBaseUrl, + restrictScope = restrictScope, + onSuccess = { max = it }, + onError = { maxError = it }, + ) + maxError?.let { return CheckFeeResult.Error(it) } + val maxAmount = max ?: return CheckFeeResult.Error( + TalerErrorInfo.makeCustomError("Could not determine the maximum send amount."), + ) + var response: CheckFeeResult = CheckFeeResult.None( - maxDepositAmountEffective = max?.effectiveAmount, - maxDepositAmountRaw = max?.rawAmount, + maxDepositAmountEffective = maxAmount.effectiveAmount, + maxDepositAmountRaw = maxAmount.rawAmount, ) api.request( @@ -204,8 +223,8 @@ class PeerManager( is CheckPeerPushDebitOkResponse -> CheckFeeResult.Success( amountRaw = r.amountRaw, amountEffective = r.amountEffective, - maxDepositAmountEffective = max?.effectiveAmount, - maxDepositAmountRaw = max?.rawAmount, + maxDepositAmountEffective = maxAmount.effectiveAmount, + maxDepositAmountRaw = maxAmount.rawAmount, exchangeBaseUrl = r.exchangeBaseUrl, defaultExpiration = r.defaultExpiration, ) @@ -213,13 +232,14 @@ class PeerManager( is CheckPeerPushDebitInsufficientBalanceResponse -> CheckFeeResult.InsufficientBalance( maxAmountEffective = r.insufficientBalanceDetails.maxEffectiveSpendAmount, maxAmountRaw = r.insufficientBalanceDetails.balanceAvailable, - maxDepositAmountEffective = max?.effectiveAmount, - maxDepositAmountRaw = max?.rawAmount, + maxDepositAmountEffective = maxAmount.effectiveAmount, + maxDepositAmountRaw = maxAmount.rawAmount, causeHint = r.insufficientBalanceDetails.causeHint, ) } }.onError { error -> Log.e(TAG, "got checkPeerPushDebit error result $error") + response = CheckFeeResult.Error(error) } return response @@ -229,19 +249,22 @@ class PeerManager( currency: String, exchangeBaseUrl: String? = null, restrictScope: ScopeInfo? = null, - ): GetMaxPeerPushDebitAmountResponse? { - var response: GetMaxPeerPushDebitAmountResponse? = null - api.request("getMaxPeerPushDebitAmount", GetMaxPeerPushDebitAmountResponse.serializer()) { + onSuccess: (GetMaxPeerPushDebitAmountResponse) -> Unit, + onError: (TalerErrorInfo) -> Unit, + ) { + api.request( + "getMaxPeerPushDebitAmount", + GetMaxPeerPushDebitAmountResponse.serializer(), + ) { exchangeBaseUrl?.let { put("exchangeBaseUrl", it) } restrictScope?.let { put("restrictScope", JSONObject(BackendManager.json.encodeToString(it))) } put("currency", currency) + }.onSuccess { + onSuccess(it) }.onError { error -> Log.e(TAG, "got getMaxPeerPushDebitAmount error result $error") - }.onSuccess { - response = it + onError(error) } - - return response } fun initiatePeerPushDebit( diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawScreen.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawScreen.kt @@ -29,6 +29,7 @@ import net.taler.common.Amount import net.taler.wallet.R import net.taler.wallet.WalletDestination import net.taler.wallet.compose.AmountScope +import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.collectAsStateLifecycleAware @@ -135,7 +136,19 @@ fun PromptWithdrawScreen( }, onNavigateBack = onNavigateBack, ) { paddingValues -> - if (status.selectedAmount == null && status.selectedScope == null) { + val withdrawError = status.error + if (status.status == WithdrawStatus.Status.Error && withdrawError != null) { + ErrorComposable( + error = withdrawError, + devMode = devMode, + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + scrollable = true, + onRetry = { withdrawManager.resetWithdrawal() }, + onClose = onNavigateBack, + ) + } else if (status.selectedAmount == null && status.selectedScope == null) { LoadingScreen(Modifier.padding(paddingValues)) } else { WithdrawalShowInfo(