fix: prevent cleared Bolt11 invoices from appearing in transaction history#111
fix: prevent cleared Bolt11 invoices from appearing in transaction history#111Delgado74 wants to merge 7 commits into
Conversation
…g transactions When a user generates a Bolt11 invoice from the receive screen and presses 'Clear invoice', the invoice remained visible in the transaction history because _clearInvoice() only cleared local UI state without cancelling the pending invoice on the LNBits server. Add cancelInvoice() method to InvoiceService that attempts DELETE on LNBits API endpoints. If the server does not support cancellation, the payment hash is stored in a shared set and filtered from the history screen locally.
…story When a user generated a Bolt11 invoice on the receive screen and then pressed 'Clear invoice', or when the 10-minute monitoring timeout expired, or when navigating away, the invoice remained visible in the transaction history. This happened because the invoice was only removed from local UI state but was never cancelled on the LNBits server, and no local record of the cleared invoice was kept. Key changes: - Add cancelInvoice() to InvoiceService: attempts DELETE on LNBits API endpoints to cancel the pending invoice server-side - Add ClearedInvoiceStore: persists cleared invoice payment hashes to SharedPreferences as JSON, surviving app restarts and crashes - Add _discardInvoice() helper: used by both clear and timeout paths to ensure the hash is always recorded before async cancellation - Update HistoryScreen filter: excludes pending transactions whose payment hash is in the cleared set All discard paths (_clearInvoice, timeout, dispose) now record the hash synchronously before async cancellation is attempted, eliminating race conditions between clearing and history navigation.
|
Warning Review limit reached
More reviews will be available in 59 minutes and 19 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (8)
📒 Files selected for processing (9)
📝 WalkthroughWalkthroughAdds a persisted ClearedInvoiceStore, loads it at app startup, records cleared/discarded invoice hashes from ReceiveScreen (and attempts server cancellation), and filters those hashes out of HistoryScreen results. ChangesCleared Invoice Tracking and Cancellation
Sequence DiagramsequenceDiagram
participant User
participant ReceiveScreen
participant HistoryScreen
participant ClearedInvoiceStore
participant InvoiceService
participant LNBits
User->>ReceiveScreen: Generate invoice
ReceiveScreen->>ReceiveScreen: set _generatedInvoice
User->>ReceiveScreen: Clear / Discard / Timeout
ReceiveScreen->>ClearedInvoiceStore: add(paymentHash)
ReceiveScreen->>InvoiceService: _tryCancelInvoiceOnServer(paymentHash) (async)
InvoiceService->>LNBits: DELETE /api/v1/payments/{paymentHash}
alt Server cancellation succeeds
LNBits-->>InvoiceService: 200/204
else Server cancellation fails
InvoiceService->>LNBits: DELETE /api/v1/wallet/payment/{paymentHash}
end
User->>HistoryScreen: View transaction history
HistoryScreen->>ClearedInvoiceStore: contains(paymentHash)?
ClearedInvoiceStore-->>HistoryScreen: exclude from display
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/screens/9receive_screen.dart`:
- Around line 129-131: The discard/dispose paths currently only record cleared
invoices locally via
ClearedInvoiceStore.instance.add(_generatedInvoice!.paymentHash) (seen where
_generatedInvoice is handled) but do not attempt server cancellation; update
those paths (the discard and dispose flows that call
ClearedInvoiceStore.instance.add and any similar local-only branches) to also
fire a best-effort, unawaited cancellation by calling
unawaited(_tryCancelInvoiceOnServer(hash)) with the same payment hash (e.g., use
_generatedInvoice!.paymentHash or the local hash variable) so the upstream
pending invoice is best-effort cancelled while preserving the existing local
recording.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6886fec3-b482-4f11-9b19-6de8f9e4abec
📒 Files selected for processing (5)
lib/main.dartlib/screens/7history_screen.dartlib/screens/9receive_screen.dartlib/services/cleared_invoice_store.dartlib/services/invoice_service.dart
CodeRabbit review found that _discardInvoice() and dispose() only recorded the cleared invoice hash locally but did not fire a best-effort server cancellation. This could leave pending invoices active on LNBits even when the user navigated away or the monitoring timeout fired. Add unawaited(_tryCancelInvoiceOnServer(hash)) to both paths so the DELETE attempt is always made regardless of how the invoice is discarded.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/screens/9receive_screen.dart (1)
122-136:⚠️ Potential issue | 🔴 Critical | ⚡ Quick win
context.readfromdispose()is unsafe andwallet.inKeyis the wrong credential for server cancellation.The call to
_tryCancelInvoiceOnServerfromdispose()(line 133) invokescontext.read<WalletProvider>()andcontext.read<AuthProvider>()(lines 824–825), which is unsafe during widget teardown. Per Flutter provider documentation, this can throw "Looking up a deactivated widget's ancestor is unsafe," and the error will be silently swallowed by the emptycatch (_)at line 836, leaving invoices un-cancelled with no signal.Additionally, the method passes
wallet.inKeyas theadminKeyparameter tocancelInvoice()(line 828). SinceWalletInfohas bothadminKeyandinKeyfields, and only the admin key can perform DELETE operations on the LNBits API, this will fail to cancel invoices on the server. This same bug appears increateInvoice()andcheckInvoiceStatus()calls.Fix: Cache
serverUrl,wallet, and auth data after invoice generation into_Statefields and pass them directly to_tryCancelInvoiceOnServerinstead of resolving fromcontext. Usewallet.adminKeyinstead ofwallet.inKeyfor all server operations.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/screens/9receive_screen.dart` around lines 122 - 136, The dispose() call is unsafe because _tryCancelInvoiceOnServer currently resolves WalletProvider/AuthProvider from context and uses wallet.inKey; change the flow to cache serverUrl, WalletInfo, and auth credentials into State fields when you generate the invoice (e.g., where _generatedInvoice is set), then have dispose() call unawaited(_tryCancelInvoiceOnServer(cachedServerUrl, cachedWallet, cachedAuth, ...)) using those cached fields; also update all server calls (createInvoice(), checkInvoiceStatus(), _tryCancelInvoiceOnServer) to use wallet.adminKey instead of wallet.inKey so DELETE/auth operations use the admin credential. Ensure _tryCancelInvoiceOnServer signature and calls are updated to accept the cached values rather than reading from context.
🧹 Nitpick comments (1)
lib/screens/9receive_screen.dart (1)
790-820: ⚡ Quick winDe-duplicate
_clearInvoiceand_discardInvoice.The bodies are identical except for the trailing snackbar. Have
_clearInvoicedelegate to_discardInvoiceand then show the snackbar — keeps the cleanup/recording logic in one place so future tweaks (e.g., capturing the server credentials suggested above) only need to land once.♻️ Proposed refactor
void _clearInvoice() { - _invoicePaymentTimer?.cancel(); - _invoicePaymentTimeoutTimer?.cancel(); - - if (_generatedInvoice != null) { - final hash = _generatedInvoice!.paymentHash; - ClearedInvoiceStore.instance.add(hash); - unawaited(_tryCancelInvoiceOnServer(hash)); - } - - setState(() { - _generatedInvoice = null; - }); + _discardInvoice(); _showAccentSnackBar( icon: Icons.check_circle, message: AppLocalizations.of(context)!.invoice_cleared_message, ); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/screens/9receive_screen.dart` around lines 790 - 820, Refactor to remove duplicate cleanup logic by making _discardInvoice contain the shared cancellation/clearing/recording logic (cancel _invoicePaymentTimer and _invoicePaymentTimeoutTimer, add paymentHash to ClearedInvoiceStore, call unawaited(_tryCancelInvoiceOnServer(hash)), and setState to clear _generatedInvoice), then have _clearInvoice simply call _discardInvoice() and afterwards call _showAccentSnackBar(...) to display the success message; update references to paymentHash, ClearedInvoiceStore.instance, _tryCancelInvoiceOnServer, and setState in those functions to ensure behavior is identical after delegating.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/screens/9receive_screen.dart`:
- Around line 130-134: The call to ClearedInvoiceStore.instance.add(hash)
returns a Future and must be wrapped with unawaited(...) to avoid dropping the
Future and to match the surrounding pattern (_tryCancelInvoiceOnServer is
already unawaited); replace direct calls to
ClearedInvoiceStore.instance.add(hash) with
unawaited(ClearedInvoiceStore.instance.add(hash)) at all three call sites in
this file (the branch guarded by _generatedInvoice != null and the other two
occurrences referenced in the review) so persistence I/O errors are not silently
discarded.
- Around line 822-837: In _tryCancelInvoiceOnServer replace the read-only
invoice key with the wallet's admin key when calling
InvoiceService.cancelInvoice: pass wallet.adminKey (not wallet.inKey) as the
adminKey argument to ensure the LNBits DELETE endpoint is authorized; locate the
call to cancelInvoice inside _tryCancelInvoiceOnServer and update the adminKey
parameter to use wallet.adminKey.
---
Outside diff comments:
In `@lib/screens/9receive_screen.dart`:
- Around line 122-136: The dispose() call is unsafe because
_tryCancelInvoiceOnServer currently resolves WalletProvider/AuthProvider from
context and uses wallet.inKey; change the flow to cache serverUrl, WalletInfo,
and auth credentials into State fields when you generate the invoice (e.g.,
where _generatedInvoice is set), then have dispose() call
unawaited(_tryCancelInvoiceOnServer(cachedServerUrl, cachedWallet, cachedAuth,
...)) using those cached fields; also update all server calls (createInvoice(),
checkInvoiceStatus(), _tryCancelInvoiceOnServer) to use wallet.adminKey instead
of wallet.inKey so DELETE/auth operations use the admin credential. Ensure
_tryCancelInvoiceOnServer signature and calls are updated to accept the cached
values rather than reading from context.
---
Nitpick comments:
In `@lib/screens/9receive_screen.dart`:
- Around line 790-820: Refactor to remove duplicate cleanup logic by making
_discardInvoice contain the shared cancellation/clearing/recording logic (cancel
_invoicePaymentTimer and _invoicePaymentTimeoutTimer, add paymentHash to
ClearedInvoiceStore, call unawaited(_tryCancelInvoiceOnServer(hash)), and
setState to clear _generatedInvoice), then have _clearInvoice simply call
_discardInvoice() and afterwards call _showAccentSnackBar(...) to display the
success message; update references to paymentHash, ClearedInvoiceStore.instance,
_tryCancelInvoiceOnServer, and setState in those functions to ensure behavior is
identical after delegating.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: dd069189-1d7f-4f56-8660-e129b4533a28
📒 Files selected for processing (1)
lib/screens/9receive_screen.dart
…-removes-from-history
7079d97 to
52471dd
Compare
- Add 'Clear from history' button in transaction detail bottom sheet - Add swipe-to-dismiss on pending transaction cards - Add confirmation dialog before clearing - Update all locale files (de, en, es, fr, it, pt, ru) with new strings - Integrate with ClearedInvoiceStore and InvoiceService.cancelInvoice() Closes lachispame#112
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/screens/7history_screen.dart`:
- Around line 999-1002: The call to _invoiceService.cancelInvoice is using the
invoice key (wallet.inKey) instead of the privileged admin key; update the call
site that passes serverUrl/adminKey/paymentHash so that the X-API-KEY uses
wallet.adminKey (replace wallet.inKey with wallet.adminKey) to match other
privileged calls like sendPayment, sendPaymentToLightningAddress, and
createInvoice.
- Around line 956-959: Move the setState call so it only runs when the State is
still mounted: check the mounted guard before calling setState({}) (i.e.,
perform "if (!mounted) return;" first), then call setState and continue to show
the SnackBar via ScaffoldMessenger.of(context).showSnackBar; ensure the async
completion path that currently calls setState uses the mounted check to avoid
calling setState after disposal.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 044a9266-e0e9-4df3-8613-93d4fea7ab43
⛔ Files ignored due to path filters (8)
lib/l10n/generated/app_localizations.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_de.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_en.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_es.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_fr.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_it.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_pt.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_ru.dartis excluded by!**/generated/**
📒 Files selected for processing (12)
lib/l10n/app_de.arblib/l10n/app_en.arblib/l10n/app_es.arblib/l10n/app_fr.arblib/l10n/app_it.arblib/l10n/app_pt.arblib/l10n/app_ru.arblib/main.dartlib/screens/7history_screen.dartlib/screens/9receive_screen.dartlib/services/cleared_invoice_store.dartlib/services/invoice_service.dart
✅ Files skipped from review due to trivial changes (1)
- lib/l10n/app_ru.arb
🚧 Files skipped from review as they are similar to previous changes (4)
- lib/main.dart
- lib/services/invoice_service.dart
- lib/services/cleared_invoice_store.dart
- lib/screens/9receive_screen.dart
- Use wallet.adminKey instead of wallet.inKey for cancelInvoice calls - Wrap ClearedInvoiceStore.instance.add() with unawaited() - Check mounted before setState in _doClearPendingTransaction - Cache serverUrl and WalletInfo at invoice generation for safe dispose() - Refactor _tryCancelInvoiceOnServer to use cached fields - Eliminate duplicate cleanup logic between _clearInvoice and _discardInvoice
Description
Fixes #107. When a user generates a Bolt11 invoice from the receive screen and clears it (via the "Clear invoice" button, 10-minute timeout, or navigating away), the invoice should not appear in the transaction history. Previously, it remained visible because the invoice was only removed from local UI state without any cancellation on the LNBits server or local record of the cleared invoice.
Also closes #112: users can now manually clear pending invoices from the history screen itself, either by swiping on pending transaction cards or via a "Clear from history" button in the transaction detail bottom sheet.
Changes
Why additional modifications were needed beyond issue #107 proposals
The original issue proposed four options (A-D), with Option D (hybrid: server cancel + local filtering) as the recommended approach. During implementation, three additional problems were discovered that required going beyond the original proposal:
**Race condition in **: The original Option D added the payment hash to the cleared set only inside the async method — AFTER an HTTP call to LNBits. If the user navigated to the history screen before this async operation completed, the hash wasn't in the set yet, and the invoice appeared.
Timeout and dispose were not covered: The 10-minute invoice monitoring timeout handler and the widget's method (when the user navigates away) both cleared locally but never recorded the hash anywhere. These paths left pending invoices visible in history.
No persistence across app restarts: The original proposals only kept the cleared hashes in an in-memory . If the app was closed and reopened (or crashed), the set was empty and ALL previously cleared invoices reappeared in history.
To solve all three, we added:
lib/services/cleared_invoice_store.dart): a singleton that persists cleared payment hashes to as JSON, surviving app restarts and crashes.9receive_screen.dart): a synchronous helper that records the hash to the persistent store before any async operation. Used by all three discard paths (clear button, timeout, dispose).ClearedInvoiceStore.instanceBEFORE the async call. This eliminates the race condition entirely.New: Clear pending invoices from history (#112)
ClearedInvoiceStoreand attempt server-side cancellation viaInvoiceService.cancelInvoice().lib/services/cleared_invoice_store.dartNEW FILE — Singleton that persists cleared payment hashes to SharedPreferences as JSON.
lib/services/invoice_service.dartcancelInvoice()method that attemptsDELETE /api/v1/payments/{paymentHash}on multiple LNBits API endpointslib/screens/9receive_screen.dartClearedInvoiceStore_clearInvoice(),_discardInvoice(), anddispose()now record the payment hash toClearedInvoiceStorebefore attempting server cancellation_tryCancelInvoiceOnServer()helperlib/screens/7history_screen.dart_filteredTransactionsexcludes pending transactions whosepaymentHashis inClearedInvoiceStore_doClearPendingTransaction()and_clearPendingTransaction()methods_tryCancelInvoiceOnServer()helperDismissible) on pending transaction cardsElevatedButtonin the detail bottom sheetlib/main.dartClearedInvoiceStore.instance.load()called beforerunApp()lib/l10n/clear_pending_invoiceclear_pending_confirm_titleclear_pending_confirm_messageinvoice_cleared_from_historyHybrid approach (Option D from issue)
Summary by CodeRabbit
New Features
Internationalization