From 8d76737771bdb22e0e86cb7bed26a5c8fcd31235 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sat, 29 Nov 2025 19:31:07 +0100 Subject: [PATCH] Improve error handling across async flows --- .../electricdreams/numo/ModernPOSActivity.kt | 15 ++ .../numo/core/cashu/CashuWalletManager.kt | 30 +++- .../feature/settings/MintsSettingsActivity.kt | 166 ++++++++++-------- .../feature/settings/RestoreWalletActivity.kt | 68 +++---- .../numo/payment/LightningMintHandler.kt | 29 ++- app/src/main/res/values/strings.xml | 4 + 6 files changed, 202 insertions(+), 110 deletions(-) diff --git a/app/src/main/java/com/electricdreams/numo/ModernPOSActivity.kt b/app/src/main/java/com/electricdreams/numo/ModernPOSActivity.kt index 3e568bd3..d352aacd 100644 --- a/app/src/main/java/com/electricdreams/numo/ModernPOSActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/ModernPOSActivity.kt @@ -166,6 +166,13 @@ class ModernPOSActivity : AppCompatActivity(), SatocashWallet.OperationFeedback, // Lifecycle methods override fun onResume() { super.onResume() + CashuWalletManager.setErrorListener(object : CashuWalletManager.WalletErrorListener { + override fun onWalletError(message: String) { + runOnUiThread { + Toast.makeText(this@ModernPOSActivity, message, Toast.LENGTH_LONG).show() + } + } + }) // Reapply theme when returning from settings uiCoordinator.applyTheme() @@ -184,6 +191,7 @@ class ModernPOSActivity : AppCompatActivity(), SatocashWallet.OperationFeedback, override fun onPause() { super.onPause() + CashuWalletManager.setErrorListener(null) nfcAdapter?.disableForegroundDispatch(this) } @@ -193,6 +201,13 @@ class ModernPOSActivity : AppCompatActivity(), SatocashWallet.OperationFeedback, super.onDestroy() } + override fun onDestroy() { + CashuWalletManager.setErrorListener(null) + uiCoordinator.stopServices() + bitcoinPriceWorker?.stop() + super.onDestroy() + } + override fun onConfigurationChanged(newConfig: android.content.res.Configuration) { super.onConfigurationChanged(newConfig) // Dialog layout handled by managers diff --git a/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt b/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt index 7475e927..d3904bd3 100644 --- a/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt +++ b/app/src/main/java/com/electricdreams/numo/core/cashu/CashuWalletManager.kt @@ -51,7 +51,12 @@ object CashuWalletManager : MintManager.MintChangeListener { // Build initial wallet val initialMints = mintManager.getAllowedMints() scope.launch { - rebuildWallet(initialMints) + try { + rebuildWallet(initialMints) + } catch (t: Throwable) { + Log.e(TAG, "Failed to rebuild wallet during init", t) + notifyWalletError("Wallet initialization failed: ${t.localizedMessage ?: "Unknown error"}") + } } } @@ -174,13 +179,23 @@ object CashuWalletManager : MintManager.MintChangeListener { override fun onMintsChanged(newMints: List) { Log.d(TAG, "Mint list changed, rebuilding wallet with ${'$'}{newMints.size} mints") scope.launch { - rebuildWallet(newMints) + try { + rebuildWallet(newMints) + } catch (t: Throwable) { + Log.e(TAG, "Failed to rebuild wallet after mint change", t) + notifyWalletError("Unable to update wallet: ${t.localizedMessage ?: "Unknown error"}") + } } } /** Current MultiMintWallet instance, or null if initialization failed or not complete. */ fun getWallet(): MultiMintWallet? = wallet + /** Set an optional callback that surfaces wallet errors to the UI layer. */ + fun setErrorListener(listener: WalletErrorListener?) { + walletErrorListener = listener + } + /** Current database instance, mostly for debugging or future use. */ fun getDatabase(): WalletSqliteDatabase? = database @@ -416,6 +431,17 @@ object CashuWalletManager : MintManager.MintChangeListener { Log.d(TAG, "Initialized MultiMintWallet with ${'$'}{mints.size} mints; DB=${'$'}{dbFile.absolutePath}") } catch (t: Throwable) { Log.e(TAG, "Failed to initialize MultiMintWallet", t) + notifyWalletError("Wallet initialization failed: ${t.localizedMessage ?: "Unknown error"}") } } + + private fun notifyWalletError(message: String) { + walletErrorListener?.onWalletError(message) + } + + interface WalletErrorListener { + fun onWalletError(message: String) + } + + private var walletErrorListener: WalletErrorListener? = null } diff --git a/app/src/main/java/com/electricdreams/numo/feature/settings/MintsSettingsActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/settings/MintsSettingsActivity.kt index 95c2e910..6bd07539 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/settings/MintsSettingsActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/settings/MintsSettingsActivity.kt @@ -208,42 +208,51 @@ class MintsSettingsActivity : AppCompatActivity() { } lifecycleScope.launch { - // Load balances - val balances = withContext(Dispatchers.IO) { - CashuWalletManager.getAllMintBalances() - } - mintBalances.clear() - mintBalances.putAll(balances) - - // Auto-select lightning mint if none selected - if (selectedLightningMint == null || !mints.contains(selectedLightningMint)) { - val highestBalanceMint = mints.maxByOrNull { mintBalances[it] ?: 0L } - highestBalanceMint?.let { setLightningMint(it, animate = false) } + try { + // Load balances + val balances = withContext(Dispatchers.IO) { + CashuWalletManager.getAllMintBalances() + } + mintBalances.clear() + mintBalances.putAll(balances) + + // Auto-select lightning mint if none selected + if (selectedLightningMint == null || !mints.contains(selectedLightningMint)) { + val highestBalanceMint = mints.maxByOrNull { mintBalances[it] ?: 0L } + highestBalanceMint?.let { setLightningMint(it, animate = false) } + } + + // Build UI + buildMintList(mints) + updateLightningMintCard() + updateTotalBalance() + + // Refresh stale mint info + refreshStaleMintInfo() + } catch (t: Throwable) { + Log.e(TAG, "Failed to load mint balances", t) + Toast.makeText(this@MintsSettingsActivity, getString(R.string.error_loading_mints), Toast.LENGTH_LONG).show() } - - // Build UI - buildMintList(mints) - updateLightningMintCard() - updateTotalBalance() - - // Refresh stale mint info - refreshStaleMintInfo() } } private fun refreshBalances() { lifecycleScope.launch { - val balances = withContext(Dispatchers.IO) { - CashuWalletManager.getAllMintBalances() + try { + val balances = withContext(Dispatchers.IO) { + CashuWalletManager.getAllMintBalances() + } + mintBalances.clear() + mintBalances.putAll(balances) + + // Update UI + val mints = mintManager.getAllowedMints() + buildMintList(mints) + updateLightningMintCard() + updateTotalBalance() + } catch (t: Throwable) { + Log.e(TAG, "Failed to refresh mint balances", t) } - mintBalances.clear() - mintBalances.putAll(balances) - - // Update UI - val mints = mintManager.getAllowedMints() - buildMintList(mints) - updateLightningMintCard() - updateTotalBalance() } } @@ -409,36 +418,41 @@ class MintsSettingsActivity : AppCompatActivity() { addMintCard.setLoading(true) lifecycleScope.launch { - val isValid = validateMintUrl(mintUrl) - - if (!isValid) { - addMintCard.setLoading(false) - Toast.makeText( - this@MintsSettingsActivity, - getString(R.string.mints_invalid_url), - Toast.LENGTH_LONG - ).show() - return@launch - } - - val added = mintManager.addMint(mintUrl) - if (added) { - fetchAndStoreMintInfo(mintUrl) - loadMintsAndBalances() - addMintCard.clearInput() - addMintCard.collapseIfExpanded() - - // Broadcast that mints changed so other activities can refresh - BalanceRefreshBroadcast.send(this@MintsSettingsActivity, BalanceRefreshBroadcast.REASON_MINT_ADDED) + try { + val isValid = validateMintUrl(mintUrl) - Toast.makeText( - this@MintsSettingsActivity, - getString(R.string.mints_added_toast), - Toast.LENGTH_SHORT - ).show() + if (!isValid) { + addMintCard.setLoading(false) + Toast.makeText( + this@MintsSettingsActivity, + getString(R.string.mints_invalid_url), + Toast.LENGTH_LONG + ).show() + return@launch + } + + val added = mintManager.addMint(mintUrl) + if (added) { + fetchAndStoreMintInfo(mintUrl) + loadMintsAndBalances() + addMintCard.clearInput() + addMintCard.collapseIfExpanded() + + // Broadcast that mints changed so other activities can refresh + BalanceRefreshBroadcast.send(this@MintsSettingsActivity, BalanceRefreshBroadcast.REASON_MINT_ADDED) + + Toast.makeText( + this@MintsSettingsActivity, + getString(R.string.mints_added_toast), + Toast.LENGTH_SHORT + ).show() + } + } catch (t: Throwable) { + Log.e(TAG, "Failed to add mint", t) + Toast.makeText(this@MintsSettingsActivity, R.string.mints_add_failed, Toast.LENGTH_LONG).show() + } finally { + addMintCard.setLoading(false) } - - addMintCard.setLoading(false) } } @@ -478,29 +492,37 @@ class MintsSettingsActivity : AppCompatActivity() { private suspend fun fetchAndStoreMintInfo(mintUrl: String) { withContext(Dispatchers.IO) { - val info = CashuWalletManager.fetchMintInfo(mintUrl) - if (info != null) { - val json = CashuWalletManager.mintInfoToJson(info) - mintManager.setMintInfo(mintUrl, json) - mintManager.setMintRefreshTimestamp(mintUrl) - - info.iconUrl?.let { iconUrl -> - if (iconUrl.isNotEmpty()) { - MintIconCache.downloadAndCacheIcon(mintUrl, iconUrl) + try { + val info = CashuWalletManager.fetchMintInfo(mintUrl) + if (info != null) { + val json = CashuWalletManager.mintInfoToJson(info) + mintManager.setMintInfo(mintUrl, json) + mintManager.setMintRefreshTimestamp(mintUrl) + + info.iconUrl?.let { iconUrl -> + if (iconUrl.isNotEmpty()) { + MintIconCache.downloadAndCacheIcon(mintUrl, iconUrl) + } } } + } catch (t: Throwable) { + Log.w(TAG, "Failed to fetch mint info for ${mintUrl}", t) } } } private fun refreshStaleMintInfo() { lifecycleScope.launch { - val mintsToRefresh = mintManager.getMintsNeedingRefresh() - for (mintUrl in mintsToRefresh) { - fetchAndStoreMintInfo(mintUrl) - } - if (mintsToRefresh.isNotEmpty()) { - updateLightningMintCard() + try { + val mintsToRefresh = mintManager.getMintsNeedingRefresh() + for (mintUrl in mintsToRefresh) { + fetchAndStoreMintInfo(mintUrl) + } + if (mintsToRefresh.isNotEmpty()) { + updateLightningMintCard() + } + } catch (t: Throwable) { + Log.w(TAG, "Failed to refresh mint info", t) } } } diff --git a/app/src/main/java/com/electricdreams/numo/feature/settings/RestoreWalletActivity.kt b/app/src/main/java/com/electricdreams/numo/feature/settings/RestoreWalletActivity.kt index cf5f58d5..bcb079f3 100644 --- a/app/src/main/java/com/electricdreams/numo/feature/settings/RestoreWalletActivity.kt +++ b/app/src/main/java/com/electricdreams/numo/feature/settings/RestoreWalletActivity.kt @@ -399,38 +399,45 @@ class RestoreWalletActivity : AppCompatActivity() { fetchingStatus.text = getString(R.string.onboarding_fetching_searching_backup) lifecycleScope.launch { - // Fetch backup from Nostr - val result = withContext(Dispatchers.IO) { - fetchMintBackupSuspend(mnemonic) - } - - // Get existing configured mints - val mintManager = MintManager.getInstance(this@RestoreWalletActivity) - val existingMints = mintManager.getAllowedMints() - - // Reset mint sets - discoveredMints.clear() - selectedMints.clear() + try { + // Fetch backup from Nostr + val result = withContext(Dispatchers.IO) { + fetchMintBackupSuspend(mnemonic) + } - if (result.success && result.mints.isNotEmpty()) { - // Backup found! Add discovered mints - backupFound = true - backupTimestamp = result.timestamp - discoveredMints.addAll(result.mints) - selectedMints.addAll(result.mints) - } else { - // No backup found, use existing mints - backupFound = false - backupTimestamp = null - } + // Get existing configured mints + val mintManager = MintManager.getInstance(this@RestoreWalletActivity) + val existingMints = mintManager.getAllowedMints() + + // Reset mint sets + discoveredMints.clear() + selectedMints.clear() + + if (result.success && result.mints.isNotEmpty()) { + // Backup found! Add discovered mints + backupFound = true + backupTimestamp = result.timestamp + discoveredMints.addAll(result.mints) + selectedMints.addAll(result.mints) + } else { + // No backup found, use existing mints + backupFound = false + backupTimestamp = null + } - // Also include existing configured mints - discoveredMints.addAll(existingMints) - selectedMints.addAll(existingMints) + // Also include existing configured mints + discoveredMints.addAll(existingMints) + selectedMints.addAll(existingMints) - // Proceed to review screen - withContext(Dispatchers.Main) { - updateUIForStep(RestoreStep.REVIEW_MINTS) + // Proceed to review screen + withContext(Dispatchers.Main) { + updateUIForStep(RestoreStep.REVIEW_MINTS) + } + } catch (t: Throwable) { + withContext(Dispatchers.Main) { + Toast.makeText(this@RestoreWalletActivity, getString(R.string.restore_backup_failed), Toast.LENGTH_LONG).show() + } + Log.e(TAG, "Failed to fetch mint backup", t) } } } @@ -637,11 +644,12 @@ class RestoreWalletActivity : AppCompatActivity() { showSuccess(balanceChanges) } } catch (e: Exception) { + Log.e(TAG, "Restore failed", e) withContext(Dispatchers.Main) { updateUIForStep(RestoreStep.REVIEW_MINTS) Toast.makeText( this@RestoreWalletActivity, - "Restore failed: ${e.message}", + getString(R.string.restore_failed_generic), Toast.LENGTH_LONG ).show() } diff --git a/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt b/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt index 3d104e61..0f085114 100644 --- a/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt +++ b/app/src/main/java/com/electricdreams/numo/payment/LightningMintHandler.kt @@ -237,6 +237,9 @@ class LightningMintHandler( Log.d(TAG, "WebSocket subscription cancelled for resumed quote $quoteId") } catch (e: Exception) { Log.e(TAG, "WebSocket error for resumed quote $quoteId: ${e.message}", e) + launch(Dispatchers.Main) { + callback.onStatusUpdate("Connection lost, retrying...") + } } } @@ -247,6 +250,9 @@ class LightningMintHandler( Log.d(TAG, "Polling cancelled for resumed quote $quoteId") } catch (e: Exception) { Log.e(TAG, "Polling error for resumed quote $quoteId: ${e.message}", e) + launch(Dispatchers.Main) { + callback.onStatusUpdate("Checking payment status...") + } } } @@ -468,14 +474,22 @@ class LightningMintHandler( return false } - Log.d(TAG, "Mint quote $quoteId is paid (detected by $source), calling wallet.mint") - val proofs = wallet.mint(mintUrl, quoteId, null) - Log.d(TAG, "Lightning mint completed with ${proofs.size} proofs ($source)") + return try { + Log.d(TAG, "Mint quote ${quoteId} is paid (detected by ${source}), calling wallet.mint") + val proofs = wallet.mint(mintUrl, quoteId, null) + Log.d(TAG, "Lightning mint completed with ${proofs.size} proofs (${source})") - uiScope.launch(Dispatchers.Main) { - callback.onPaymentSuccess() + uiScope.launch(Dispatchers.Main) { + callback.onPaymentSuccess() + } + true + } catch (t: Throwable) { + Log.e(TAG, "Minting failed for quote ${quoteId}", t) + uiScope.launch(Dispatchers.Main) { + callback.onError(t.localizedMessage ?: "Failed to mint proofs") + } + false } - return true } /** @@ -523,6 +537,9 @@ class LightningMintHandler( } catch (e: Exception) { // Log but continue polling - transient errors shouldn't stop us Log.w(TAG, "Error polling mint quote $quoteId: ${e.message}") + uiScope.launch(Dispatchers.Main) { + callback.onStatusUpdate("Lost connection to mint, retrying...") + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23aac4cf..f4843f06 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -805,4 +805,8 @@ Version: %1$s Software: Unknown Version: Unknown + Could not fetch your cloud backup. Please try again. + Wallet restore failed. Please review your seed and try again. + Could not load mint balances. Please try again. + Unable to add mint. Check your connection and try again.