diff --git a/lib/main.dart b/lib/main.dart index 9aab4f8..c74131b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,7 @@ import 'services/wallet_service.dart'; import 'services/ln_address_service.dart'; import 'services/app_info_service.dart'; import 'services/deep_link_service.dart'; +import 'services/cleared_invoice_store.dart'; import 'screens/auth_checker.dart'; import 'screens/10send_screen.dart'; import 'l10n/generated/app_localizations.dart'; @@ -25,6 +26,9 @@ void main() async { // Initialize deep link service await DeepLinkService().initialize(); + // Load persisted cleared invoice hashes + await ClearedInvoiceStore.instance.load(); + runApp(const LaChispaApp()); } diff --git a/lib/screens/7history_screen.dart b/lib/screens/7history_screen.dart index ef05df8..e1f130e 100644 --- a/lib/screens/7history_screen.dart +++ b/lib/screens/7history_screen.dart @@ -10,6 +10,7 @@ import '../services/wallet_service.dart'; import '../models/transaction_info.dart'; import '../l10n/generated/app_localizations.dart'; import '../theme/app_tokens.dart'; +import '../services/cleared_invoice_store.dart'; class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); @@ -165,14 +166,20 @@ class _HistoryScreenState extends State with TickerProviderStateM } List get _filteredTransactions { + var filtered = _transactions.where((tx) => + !(tx.isPending && + tx.paymentHash != null && + ClearedInvoiceStore.instance.contains(tx.paymentHash)) + ).toList(); + switch (_currentFilter) { case TransactionFilter.incoming: - return _transactions.where((tx) => tx.isIncoming).toList(); + return filtered.where((tx) => tx.isIncoming).toList(); case TransactionFilter.outgoing: - return _transactions.where((tx) => tx.isOutgoing).toList(); + return filtered.where((tx) => tx.isOutgoing).toList(); case TransactionFilter.all: default: - return _transactions; + return filtered; } } diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index c4cb9ff..adc7290 100644 --- a/lib/screens/9receive_screen.dart +++ b/lib/screens/9receive_screen.dart @@ -13,6 +13,7 @@ import '../services/invoice_service.dart'; import '../services/yadio_service.dart'; import '../services/transaction_detector.dart'; import '../services/nfc_charge_service.dart'; +import '../services/cleared_invoice_store.dart'; import '../models/lightning_invoice.dart'; import '../models/wallet_info.dart'; import '../l10n/generated/app_localizations.dart'; @@ -28,6 +29,7 @@ class ReceiveScreen extends StatefulWidget { } class _ReceiveScreenState extends State { + final _amountController = TextEditingController(); final _noteController = TextEditingController(); String _selectedCurrency = 'sats'; @@ -125,6 +127,11 @@ class _ReceiveScreenState extends State { _yadioService.dispose(); _invoicePaymentTimer?.cancel(); _invoicePaymentTimeoutTimer?.cancel(); + if (_generatedInvoice != null) { + final hash = _generatedInvoice!.paymentHash; + ClearedInvoiceStore.instance.add(hash); + unawaited(_tryCancelInvoiceOnServer(hash)); + } super.dispose(); } @@ -783,6 +790,13 @@ class _ReceiveScreenState extends State { void _clearInvoice() { _invoicePaymentTimer?.cancel(); _invoicePaymentTimeoutTimer?.cancel(); + + if (_generatedInvoice != null) { + final hash = _generatedInvoice!.paymentHash; + ClearedInvoiceStore.instance.add(hash); + unawaited(_tryCancelInvoiceOnServer(hash)); + } + setState(() { _generatedInvoice = null; }); @@ -792,6 +806,36 @@ class _ReceiveScreenState extends State { ); } + void _discardInvoice() { + _invoicePaymentTimer?.cancel(); + _invoicePaymentTimeoutTimer?.cancel(); + if (_generatedInvoice != null) { + final hash = _generatedInvoice!.paymentHash; + ClearedInvoiceStore.instance.add(hash); + unawaited(_tryCancelInvoiceOnServer(hash)); + } + setState(() { + _generatedInvoice = null; + }); + } + + Future _tryCancelInvoiceOnServer(String paymentHash) async { + try { + final walletProvider = context.read(); + final authProvider = context.read(); + final serverUrl = authProvider.sessionData?.serverUrl; + final wallet = walletProvider.primaryWallet; + + if (serverUrl == null || wallet == null) return; + + await _invoiceService.cancelInvoice( + serverUrl: serverUrl, + adminKey: wallet.inKey, + paymentHash: paymentHash, + ); + } catch (_) {} + } + void _showCopySheet(LNAddress? defaultAddress) { final hasInvoice = _generatedInvoice != null; final lnurl = defaultAddress?.lnurl; @@ -1444,9 +1488,7 @@ class _ReceiveScreenState extends State { _invoicePaymentTimeoutTimer = Timer(const Duration(minutes: 10), () { _invoicePaymentTimer?.cancel(); if (!mounted) return; - setState(() { - _generatedInvoice = null; - }); + _discardInvoice(); _showInfoSnackBar( AppLocalizations.of(context)!.invoice_monitoring_timeout_message, ); diff --git a/lib/services/cleared_invoice_store.dart b/lib/services/cleared_invoice_store.dart new file mode 100644 index 0000000..9c17339 --- /dev/null +++ b/lib/services/cleared_invoice_store.dart @@ -0,0 +1,40 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; + +class ClearedInvoiceStore { + static final ClearedInvoiceStore _instance = ClearedInvoiceStore._(); + static ClearedInvoiceStore get instance => _instance; + ClearedInvoiceStore._(); + + static const String _storageKey = 'cleared_invoice_hashes'; + Set _hashes = {}; + + Future load() async { + try { + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getString(_storageKey); + if (stored != null && stored.isNotEmpty) { + final List decoded = jsonDecode(stored); + _hashes = decoded.cast().toSet(); + } + } catch (_) { + _hashes = {}; + } + } + + Future add(String hash) async { + _hashes.add(hash); + await _persist(); + } + + bool contains(String? hash) => hash != null && _hashes.contains(hash); + + Set get all => Set.unmodifiable(_hashes); + + Future _persist() async { + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_storageKey, jsonEncode(_hashes.toList())); + } catch (_) {} + } +} diff --git a/lib/services/invoice_service.dart b/lib/services/invoice_service.dart index 3ed677c..5baaa3c 100644 --- a/lib/services/invoice_service.dart +++ b/lib/services/invoice_service.dart @@ -1757,6 +1757,49 @@ class InvoiceService { }; } + Future cancelInvoice({ + required String serverUrl, + required String adminKey, + required String paymentHash, + }) async { + try { + String baseUrl = serverUrl; + if (!baseUrl.startsWith('http')) { + baseUrl = 'https://$baseUrl'; + } + + final headers = {'X-API-KEY': adminKey}; + + final endpoints = [ + '$baseUrl/api/v1/payments/$paymentHash', + '$baseUrl/api/v1/wallet/payment/$paymentHash', + ]; + + for (final endpoint in endpoints) { + try { + _debugLog('[INVOICE_SERVICE] Attempting to cancel invoice: $paymentHash'); + final response = await _dio.delete( + endpoint, + options: Options(headers: headers), + ); + if (response.statusCode == 200 || response.statusCode == 204) { + _debugLog('[INVOICE_SERVICE] Invoice cancelled successfully: $paymentHash'); + return true; + } + } catch (e) { + _debugLog('[INVOICE_SERVICE] Cancel failed at $endpoint: $e'); + continue; + } + } + + _debugLog('[INVOICE_SERVICE] No endpoint succeeded for invoice cancellation: $paymentHash'); + return false; + } catch (e) { + _debugLog('[INVOICE_SERVICE] Error cancelling invoice: $e'); + return false; + } + } + void dispose() { _dio.close(); }