Description
When a user generates a Bolt11 invoice from the receive screen (ReceiveScreen) and then presses the "Clear invoice" button, the invoice should be completely removed with no trace. However, the invoice remains visible in the history screen (HistoryScreen), causing confusion.
Current behavior:
- User opens the receive screen and generates a Bolt11 invoice
- User decides not to use it and presses "Clear invoice"
- The invoice disappears from the receive screen
- When navigating to the history screen, the invoice appears as a pending transaction
Expected behavior:
- When clearing an invoice, it should not appear in the transaction history
Root Cause
The issue is in lib/screens/9receive_screen.dart:782-792:
void _clearInvoice() {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
setState(() {
_generatedInvoice = null;
});
_showAccentSnackBar(
icon: Icons.check_circle,
message: AppLocalizations.of(context)!.invoice_cleared_message,
);
}
_clearInvoice() only clears the widget's local state (_generatedInvoice = null) and cancels the monitoring timers. It does NOT make any API call to cancel/delete the invoice on the LNBits server.
Full Bug Flow
-
Invoice creation (9receive_screen.dart:1296-1379): _confirmRequestAmount() calls _invoiceService.createInvoice() which does a POST /api/v1/payments with out: false to LNBits. This creates a pending transaction record on the server.
-
Local cleanup (9receive_screen.dart:782-792): _clearInvoice() only cancels payment monitoring timers and resets _generatedInvoice to null. The invoice remains active on the LNBits server.
-
History loading (7history_screen.dart:100-165, wallet_service.dart:301-419): _loadTransactions() calls walletService.getWalletTransactions() which does a GET /api/v1/payments. LNBits returns all transactions, including the pending invoices that the user "cleared".
-
Response parsing (transaction_info.dart:86-181): TransactionInfo.fromJson() converts pending invoices ("pending": true) into TransactionInfo objects with status: TransactionStatus.pending, which are displayed in the history.
Possible Solutions
Option A (Recommended): Cancel the invoice on the LNBits server
Add a cancelInvoice() method in InvoiceService that attempts to cancel the pending invoice via the LNBits API, and call it from _clearInvoice().
Implementation:
In lib/services/invoice_service.dart, add:
Future<bool> cancelInvoice({
required String serverUrl,
required String adminKey,
required String paymentHash,
}) async {
String baseUrl = serverUrl;
if (!baseUrl.startsWith('http')) {
baseUrl = 'https://\';
}
final headers = {'X-API-KEY': adminKey};
// Try multiple endpoints for compatibility across LNBits versions
final endpoints = [
'\/api/v1/payments/\',
'\/api/v1/wallet/payment/\',
];
for (final endpoint in endpoints) {
try {
final response = await _dio.delete(
endpoint,
options: Options(headers: headers),
);
if (response.statusCode == 200 || response.statusCode == 204) {
return true;
}
} catch (e) {
continue;
}
}
return false;
}
Risks:
- LNBits may not expose a DELETE endpoint for pending invoices in all versions/implementations
- If the endpoint does not exist, cancellation fails silently
Option B: Local filtering with static Set
Maintain a static Set<String> of cleared invoice paymentHash values and filter them in the history screen.
Implementation:
In lib/screens/9receive_screen.dart:
static final Set<String> clearedInvoiceHashes = {};
void _clearInvoice() {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
if (_generatedInvoice != null) {
clearedInvoiceHashes.add(_generatedInvoice!.paymentHash);
}
setState(() {
_generatedInvoice = null;
});
_showAccentSnackBar(...);
}
In lib/screens/7history_screen.dart:
List<TransactionInfo> get _filteredTransactions {
var filtered = _transactions.where((tx) =>
!(tx.isPending &&
tx.paymentHash != null &&
_ReceiveScreenState.clearedInvoiceHashes.contains(tx.paymentHash))
).toList();
switch (_currentFilter) {
case TransactionFilter.all:
default:
return filtered;
case TransactionFilter.incoming:
return filtered.where((tx) => tx.isIncoming).toList();
case TransactionFilter.outgoing:
return filtered.where((tx) => tx.isOutgoing).toList();
}
}
Risks:
- Only works during the current session (lost on app restart)
- The invoice still exists on the server and could be paid unexpectedly
- Does not scale well if many invoices are cleared
Option C: Hide all pending transactions from history
Filter out all pending status transactions from the history screen regardless of origin.
Implementation:
In lib/screens/7history_screen.dart, modify _filteredTransactions:
List<TransactionInfo> get _filteredTransactions {
var base = _transactions.where((tx) => !tx.isPending).toList();
switch (_currentFilter) {
case TransactionFilter.all:
default:
return base;
case TransactionFilter.incoming:
return base.where((tx) => tx.isIncoming).toList();
case TransactionFilter.outgoing:
return base.where((tx) => tx.isOutgoing).toList();
}
}
Risks:
- Hides all pending transactions, not just cleared ones
- An incoming payment in transit would not be visible until completed
- May confuse users expecting to see pending transactions
Option D (Hybrid - Final Recommendation): Combine A + B
Attempt server-side cancellation (Option A) first. If it fails, add the hash to the local cleared set (Option B) as a fallback. This provides the best UX: if the server supports it, the invoice is truly gone; if not, it is at least filtered locally.
static final Set<String> clearedInvoiceHashes = {};
void _clearInvoice() async {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
if (_generatedInvoice != null && mounted) {
final hash = _generatedInvoice!.paymentHash;
try {
final walletProvider = context.read<WalletProvider>();
final authProvider = context.read<AuthProvider>();
final cancelled = await _invoiceService.cancelInvoice(
serverUrl: authProvider.sessionData!.serverUrl,
adminKey: walletProvider.primaryWallet!.inKey,
paymentHash: hash,
);
if (!cancelled) {
clearedInvoiceHashes.add(hash);
}
} catch (e) {
clearedInvoiceHashes.add(hash);
}
}
setState(() {
_generatedInvoice = null;
});
_showAccentSnackBar(
icon: Icons.check_circle,
message: AppLocalizations.of(context)!.invoice_cleared_message,
);
}
Relevant Files
| File |
Lines |
Role |
lib/screens/9receive_screen.dart |
782-792 |
_clearInvoice() — only clears local state |
lib/screens/9receive_screen.dart |
1296-1379 |
_confirmRequestAmount() — creates invoice on LNBits |
lib/services/invoice_service.dart |
98-430 |
createInvoice() — POST to LNBits |
lib/services/wallet_service.dart |
301-419 |
getWalletTransactions() — GET history from server |
lib/screens/7history_screen.dart |
100-165 |
_loadTransactions() — loads history |
lib/models/transaction_info.dart |
86-181 |
fromJson() — parses pending invoices as valid transactions |
Description
When a user generates a Bolt11 invoice from the receive screen (ReceiveScreen) and then presses the "Clear invoice" button, the invoice should be completely removed with no trace. However, the invoice remains visible in the history screen (HistoryScreen), causing confusion.
Current behavior:
Expected behavior:
Root Cause
The issue is in
lib/screens/9receive_screen.dart:782-792:_clearInvoice()only clears the widget's local state (_generatedInvoice = null) and cancels the monitoring timers. It does NOT make any API call to cancel/delete the invoice on the LNBits server.Full Bug Flow
Invoice creation (
9receive_screen.dart:1296-1379):_confirmRequestAmount()calls_invoiceService.createInvoice()which does aPOST /api/v1/paymentswithout: falseto LNBits. This creates a pending transaction record on the server.Local cleanup (
9receive_screen.dart:782-792):_clearInvoice()only cancels payment monitoring timers and resets_generatedInvoicetonull. The invoice remains active on the LNBits server.History loading (
7history_screen.dart:100-165,wallet_service.dart:301-419):_loadTransactions()callswalletService.getWalletTransactions()which does aGET /api/v1/payments. LNBits returns all transactions, including the pending invoices that the user "cleared".Response parsing (
transaction_info.dart:86-181):TransactionInfo.fromJson()converts pending invoices ("pending": true) intoTransactionInfoobjects withstatus: TransactionStatus.pending, which are displayed in the history.Possible Solutions
Option A (Recommended): Cancel the invoice on the LNBits server
Add a
cancelInvoice()method inInvoiceServicethat attempts to cancel the pending invoice via the LNBits API, and call it from_clearInvoice().Implementation:
In
lib/services/invoice_service.dart, add:Risks:
Option B: Local filtering with static Set
Maintain a static
Set<String>of cleared invoicepaymentHashvalues and filter them in the history screen.Implementation:
In
lib/screens/9receive_screen.dart:In
lib/screens/7history_screen.dart:Risks:
Option C: Hide all pending transactions from history
Filter out all
pendingstatus transactions from the history screen regardless of origin.Implementation:
In
lib/screens/7history_screen.dart, modify_filteredTransactions:Risks:
Option D (Hybrid - Final Recommendation): Combine A + B
Attempt server-side cancellation (Option A) first. If it fails, add the hash to the local cleared set (Option B) as a fallback. This provides the best UX: if the server supports it, the invoice is truly gone; if not, it is at least filtered locally.
Relevant Files
lib/screens/9receive_screen.dart_clearInvoice()— only clears local statelib/screens/9receive_screen.dart_confirmRequestAmount()— creates invoice on LNBitslib/services/invoice_service.dartcreateInvoice()— POST to LNBitslib/services/wallet_service.dartgetWalletTransactions()— GET history from serverlib/screens/7history_screen.dart_loadTransactions()— loads historylib/models/transaction_info.dartfromJson()— parses pending invoices as valid transactions