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:
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>