taler-android

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

commit 362147de20fa7e652403a8e22e753d021b7d5a9b
parent d05e1bb523e80112e62a0f49a2520e8b697c074d
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 13 Aug 2026 19:58:34 +0200

[wallet] display wallet-core init error instead of crashing

Diffstat:
Mwallet/src/main/java/net/taler/wallet/backend/InitResponse.kt | 4++++
Mwallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt | 5++---
Mwallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt | 13++++++++++++-
Mwallet/src/main/java/net/taler/wallet/main/MainActivity.kt | 30+++++++++++++++++++++++-------
Mwallet/src/main/java/net/taler/wallet/main/MainViewModel.kt | 13+++++++++++--
Awallet/src/main/java/net/taler/wallet/main/WalletInitErrorScreen.kt | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/res/values-de/strings.xml | 3+++
Mwallet/src/main/res/values-fr/strings.xml | 3+++
Mwallet/src/main/res/values/strings.xml | 3+++
9 files changed, 188 insertions(+), 13 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt b/wallet/src/main/java/net/taler/wallet/backend/InitResponse.kt @@ -72,6 +72,10 @@ fun interface VersionReceiver { fun onVersionReceived(versionInfo: WalletCoreVersion) } +fun interface InitErrorReceiver { + fun onInitError(error: TalerErrorInfo) +} + @Serializable data class WalletCoreVersion( val implementationSemver: String, diff --git a/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt b/wallet/src/main/java/net/taler/wallet/backend/WalletBackendApi.kt @@ -23,7 +23,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.KSerializer -import kotlinx.serialization.encodeToString import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.decodeFromJsonElement import net.taler.wallet.backend.TalerErrorCode.NONE @@ -37,6 +36,7 @@ class WalletBackendApi( private val app: Application, private val initialConfig: WalletRunConfig, private val versionReceiver: VersionReceiver, + private val initErrorReceiver: InitErrorReceiver, notificationReceiver: NotificationReceiver, ) { @@ -71,8 +71,7 @@ class WalletBackendApi( }.onSuccess { response -> versionReceiver.onVersionReceived(response.versionInfo) }.onError { error -> - // TODO expose this to the UI as it can happen when using an older DB version - error("Error on init message: $error") + initErrorReceiver.onInitError(error) } } diff --git a/wallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt @@ -56,6 +56,7 @@ fun ErrorComposable( error: TalerErrorInfo, modifier: Modifier = Modifier, devMode: Boolean, + message: String? = null, scrollable: Boolean = true, onClose: (() -> Unit)? = null, ) { @@ -93,6 +94,13 @@ fun ErrorComposable( json.encodeToString(error) } + message?.let { + Text(message, + Modifier.padding(bottom = 16.dp), + style = MaterialTheme.typography.bodyLarge, + ) + } + if (devMode) { Card(Modifier .fillMaxWidth()) { @@ -152,7 +160,9 @@ fun ErrorComposable( Text(text = stringResource(R.string.close)) } - BottomInsetsSpacer() + if (scrollable) { + BottomInsetsSpacer() + } } } @@ -164,6 +174,7 @@ fun ErrorComposablePreview(devMode: Boolean = false) { error = TalerErrorInfo.makeCustomError( message = "Some random error", ), + message = "Some custom message", devMode = devMode, onClose = {}, ) diff --git a/wallet/src/main/java/net/taler/wallet/main/MainActivity.kt b/wallet/src/main/java/net/taler/wallet/main/MainActivity.kt @@ -26,8 +26,10 @@ import android.os.Bundle import android.util.Log import android.widget.Toast import android.widget.Toast.LENGTH_SHORT +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.viewModels import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL @@ -104,10 +106,15 @@ class MainActivity : FragmentActivity() { var errorInfo by remember { mutableStateOf<TalerErrorInfo?>(null) } val showObservabilityLog by model.showObservabilityLog.collectAsState(false) val devMode by model.devMode.observeAsState(false) + val initError by model.initError.collectAsState() val errorSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = !devMode) val authenticated by model.authenticated.collectAsState() val biometricEnabled by model.settingsManager.getBiometricLockEnabled(this).collectAsState(false) + val logExportLauncher = rememberLauncherForActivityResult(CreateDocument("text/plain")) { uri -> + uri?.let { model.settingsManager.exportLogcat(it) } + } + LaunchedEffect(Unit) { pendingLaunchUri?.let { uri -> nav?.navigate(WalletDestination.HandleUri(uri)) @@ -115,13 +122,22 @@ class MainActivity : FragmentActivity() { } Box(Modifier.fillMaxSize()) { - WalletNavHost( - navController = navController, - model = model, - modifier = Modifier.fillMaxSize(), - onFulfillPayment = { url: String -> launchInAppBrowser(this@MainActivity, url) }, - onShowError = { errorInfo = it } - ) + initError?.let { error -> + WalletInitErrorScreen( + model = model, + error = error, + onExportLogs = { logExportLauncher.launch("taler-wallet-logcat.txt") }, + onRetry = { model.startWallet() }, + ) + } ?: run { + WalletNavHost( + navController = navController, + model = model, + modifier = Modifier.fillMaxSize(), + onFulfillPayment = { url: String -> launchInAppBrowser(this@MainActivity, url) }, + onShowError = { errorInfo = it } + ) + } if (!authenticated && biometricEnabled) { BiometricOverlay( diff --git a/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt @@ -33,6 +33,7 @@ import net.taler.common.AmountParserException import net.taler.common.Event import net.taler.wallet.accounts.AccountManager import net.taler.wallet.backend.BackendManager +import net.taler.wallet.backend.InitErrorReceiver import net.taler.wallet.backend.NotificationPayload import net.taler.wallet.backend.NotificationReceiver import net.taler.wallet.backend.TalerErrorInfo @@ -73,7 +74,7 @@ private val observabilityNotifications = listOf( class MainViewModel( app: Application, -) : AndroidViewModel(app), VersionReceiver, NotificationReceiver { +) : AndroidViewModel(app), VersionReceiver, NotificationReceiver, InitErrorReceiver { private val mDevMode = MutableLiveData(BuildConfig.DEBUG) val devMode: LiveData<Boolean> = mDevMode @@ -100,7 +101,7 @@ class MainViewModel( logLevel = if (devMode.value == true) "TRACE" else "INFO", ) - private val api = WalletBackendApi(app, walletConfig, this, this) + private val api = WalletBackendApi(app, walletConfig, this, this, this) val networkManager = NetworkManager(app.applicationContext) val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) @@ -131,6 +132,9 @@ class MainViewModel( private val mViewMode = MutableStateFlow<ViewMode>(ViewMode.Assets) val viewMode: StateFlow<ViewMode> = mViewMode + private val mInitError = MutableStateFlow<TalerErrorInfo?>(null) + val initError: StateFlow<TalerErrorInfo?> = mInitError + fun startWallet() { api.startWallet() } @@ -139,11 +143,16 @@ class MainViewModel( api.stopWallet() } + override fun onInitError(error: TalerErrorInfo) { + mInitError.value = error + } + override fun onVersionReceived(versionInfo: WalletCoreVersion) { walletVersion = versionInfo.implementationSemver walletVersionHash = versionInfo.implementationGitHash exchangeVersion = versionInfo.exchange merchantVersion = versionInfo.merchant + mInitError.value = null } override fun onNotificationReceived(payload: NotificationPayload) { diff --git a/wallet/src/main/java/net/taler/wallet/main/WalletInitErrorScreen.kt b/wallet/src/main/java/net/taler/wallet/main/WalletInitErrorScreen.kt @@ -0,0 +1,127 @@ +/* + * This file is part of GNU Taler + * (C) 2026 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +package net.taler.wallet.main + +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.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.rounded.ErrorOutline +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import net.taler.wallet.BottomInsetsSpacer +import net.taler.wallet.R +import net.taler.wallet.backend.TalerErrorInfo +import net.taler.wallet.compose.CopyToClipboardButton +import net.taler.wallet.compose.ErrorComposable +import net.taler.wallet.compose.ExpandableCard +import net.taler.wallet.compose.GlobalScaffold +import net.taler.wallet.compose.ShareButton + +@OptIn(ExperimentalSerializationApi::class) +@Composable +fun WalletInitErrorScreen( + model: MainViewModel, + error: TalerErrorInfo, + onExportLogs: () -> Unit, + onRetry: () -> Unit, + modifier: Modifier = Modifier, +) { + val devMode by model.devMode.observeAsState(false) + GlobalScaffold( + model = model, + modifier = modifier, + title = { Text(stringResource(R.string.wallet_init_error_title)) }, + ) { innerPadding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + .verticalScroll(rememberScrollState()) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + ErrorComposable( + error = error, + devMode = false, // devMode, + message = stringResource(R.string.wallet_init_error_message), + scrollable = false, + ) + + HorizontalDivider(Modifier.size(24.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onExportLogs, + ) { + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_bug_report), + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.wallet_export_logs)) + } + + Spacer(Modifier.size(12.dp)) + + Button( + modifier = Modifier.fillMaxWidth(), + onClick = onRetry, + ) { + Icon( + Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(ButtonDefaults.IconSize), + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.transactions_retry)) + } + + BottomInsetsSpacer() + } + } +} diff --git a/wallet/src/main/res/values-de/strings.xml b/wallet/src/main/res/values-de/strings.xml @@ -393,6 +393,9 @@ <string name="settings_donau_summary">Finanzamt und Steuernummer festlegen</string> <string name="enter_uri_prefix">taler://</string> <string name="error_export">Fehlerdiagnose exportieren</string> + <string name="wallet_init_error_title">Initialisierungsfehler</string> + <string name="wallet_init_error_message">Beim Starten der Wallet ist ein Fehler aufgetreten. Dies kann nach einem App-Update oder bei inkompatiblen Wallet-Daten passieren. Bitte versuche es erneut, und falls das Problem bestehen bleibt, exportiere die Protokolle und melde dieses Problem den Entwicklern.</string> + <string name="wallet_export_logs">Protokolle exportieren</string> <string name="receive_peer_create_button_amount">%1$s anfordern</string> <string name="send_peer_create_button_amount">%1$s übertragen</string> <string name="send_deposits_no_currencies_error">Es sind keine bekannten Währungen für Einzahlungen verfügbar.</string> diff --git a/wallet/src/main/res/values-fr/strings.xml b/wallet/src/main/res/values-fr/strings.xml @@ -354,6 +354,9 @@ <string name="settings_lock_auth_summary">Nécessite une empreinte digitale ou un mot de passe pour accéder au portefeuille</string> <string name="empty">Aucun objet n\'a été trouvé</string> <string name="error">Erreur</string> + <string name="wallet_init_error_title">Erreur d\'initialisation</string> + <string name="wallet_init_error_message">Une erreur est survenue lors du démarrage du portefeuille. Cela peut arriver après une mise à jour de l\'application ou si les données du portefeuille sont incompatibles. Veuillez réessayer et, si le problème persiste, exportez les journaux et signalez ce problème aux développeurs.</string> + <string name="wallet_export_logs">Exporter les journaux</string> <string name="save">Sauvegarder</string> <string name="warning">Avertissement</string> <string name="biometric_prompt_title">Déverrouillez pour utiliser le portefeuille</string> diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml @@ -65,6 +65,9 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish <string name="enter_uri_prefix" translatable="false">taler://</string> <string name="error">Error</string> <string name="error_export">Export error diagnostics</string> + <string name="wallet_init_error_title">Initialization error</string> + <string name="wallet_init_error_message">An error occurred while starting the wallet. This can happen after an app update or if the wallet data is incompatible. Please retry, and if the problem persists, export the logs and report this issue to the developers.</string> + <string name="wallet_export_logs">Export logs</string> <string name="import_db">Import</string> <string name="language">Language</string> <string name="loading">Loading</string>