diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b177357..b0b9458 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -377,5 +377,10 @@ "share_ready_message": "Bereit zum Teilen", "lnurl_copied_message": "LNURL in die Zwischenablage kopiert", "qr_scanner_title": "QR scannen", - "qr_scanner_instructions": "Kamera auf den QR-Code richten\num Rechnung oder Adresse zu scannen" + "qr_scanner_instructions": "Kamera auf den QR-Code richten\num Rechnung oder Adresse zu scannen", + + "clear_pending_invoice": "Aus Verlauf entfernen", + "clear_pending_confirm_title": "Ausstehende Rechnung entfernen?", + "clear_pending_confirm_message": "Diese ausstehende Transaktion wird aus dem Verlauf entfernt. Die Rechnung bleibt bis zum Ablauf auf LNBits bestehen.", + "invoice_cleared_from_history": "Rechnung aus Verlauf entfernt" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e5fc63f..a1aa902 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -392,5 +392,10 @@ "share_ready_message": "Ready to share", "lnurl_copied_message": "LNURL copied to clipboard", "qr_scanner_title": "Scan QR", - "qr_scanner_instructions": "Point the camera at the QR code\nto scan the invoice or address" + "qr_scanner_instructions": "Point the camera at the QR code\nto scan the invoice or address", + + "clear_pending_invoice": "Clear from history", + "clear_pending_confirm_title": "Clear pending invoice?", + "clear_pending_confirm_message": "This pending transaction will be removed from history. The invoice will remain on LNBits until it expires.", + "invoice_cleared_from_history": "Invoice removed from history" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 9abb5fa..aa713f3 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -377,5 +377,10 @@ "share_ready_message": "Listo para compartir", "lnurl_copied_message": "LNURL copiado al portapapeles", "qr_scanner_title": "Escanear QR", - "qr_scanner_instructions": "Apunta la cámara al código QR\npara escanear la factura o dirección" + "qr_scanner_instructions": "Apunta la cámara al código QR\npara escanear la factura o dirección", + + "clear_pending_invoice": "Limpiar del historial", + "clear_pending_confirm_title": "¿Limpiar factura pendiente?", + "clear_pending_confirm_message": "Esta transacción pendiente se eliminará del historial. La factura seguirá existiendo en LNBits hasta su expiración.", + "invoice_cleared_from_history": "Factura eliminada del historial" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 7e07862..3d6d1c3 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -377,5 +377,10 @@ "share_ready_message": "Prêt à partager", "lnurl_copied_message": "LNURL copié dans le presse-papiers", "qr_scanner_title": "Scanner QR", - "qr_scanner_instructions": "Pointez la caméra vers le code QR\npour scanner la facture ou l'adresse" + "qr_scanner_instructions": "Pointez la caméra vers le code QR\npour scanner la facture ou l'adresse", + + "clear_pending_invoice": "Effacer de l'historique", + "clear_pending_confirm_title": "Effacer la facture en attente ?", + "clear_pending_confirm_message": "Cette transaction en attente sera supprimée de l'historique. La facture restera sur LNBits jusqu'à son expiration.", + "invoice_cleared_from_history": "Facture supprimée de l'historique" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 0e62f78..1ce3b2b 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -377,5 +377,10 @@ "share_ready_message": "Pronto da condividere", "lnurl_copied_message": "LNURL copiato negli appunti", "qr_scanner_title": "Scansiona QR", - "qr_scanner_instructions": "Punta la fotocamera sul codice QR\nper scansionare la fattura o l'indirizzo" + "qr_scanner_instructions": "Punta la fotocamera sul codice QR\nper scansionare la fattura o l'indirizzo", + + "clear_pending_invoice": "Cancella dalla cronologia", + "clear_pending_confirm_title": "Cancellare fattura in sospeso?", + "clear_pending_confirm_message": "Questa transazione in sospeso verrà rimossa dalla cronologia. La fattura rimarrà su LNBits fino alla scadenza.", + "invoice_cleared_from_history": "Fattura rimossa dalla cronologia" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 5a3e9a5..110dceb 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -377,5 +377,10 @@ "share_ready_message": "Pronto para compartilhar", "lnurl_copied_message": "LNURL copiado para a área de transferência", "qr_scanner_title": "Escanear QR", - "qr_scanner_instructions": "Aponte a câmera para o código QR\npara escanear a fatura ou endereço" + "qr_scanner_instructions": "Aponte a câmera para o código QR\npara escanear a fatura ou endereço", + + "clear_pending_invoice": "Limpar do histórico", + "clear_pending_confirm_title": "Limpar fatura pendente?", + "clear_pending_confirm_message": "Esta transação pendente será removida do histórico. A fatura continuará existindo no LNBits até expirar.", + "invoice_cleared_from_history": "Fatura removida do histórico" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 8076ab3..2e72578 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -377,5 +377,10 @@ "share_ready_message": "Готово к отправке", "lnurl_copied_message": "LNURL скопирован в буфер обмена", "qr_scanner_title": "Сканировать QR", - "qr_scanner_instructions": "Наведите камеру на QR-код\nчтобы сканировать счёт или адрес" + "qr_scanner_instructions": "Наведите камеру на QR-код\nчтобы сканировать счёт или адрес", + + "clear_pending_invoice": "Удалить из истории", + "clear_pending_confirm_title": "Удалить ожидающий счёт?", + "clear_pending_confirm_message": "Эта ожидающая транзакция будет удалена из истории. Счёт останется на LNBits до истечения срока.", + "invoice_cleared_from_history": "Счёт удалён из истории" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 49457e1..a5c1244 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -1925,6 +1925,30 @@ abstract class AppLocalizations { /// In es, this message translates to: /// **'Apunta la cámara al código QR\npara escanear la factura o dirección'** String get qr_scanner_instructions; + + /// No description provided for @clear_pending_invoice. + /// + /// In es, this message translates to: + /// **'Limpiar del historial'** + String get clear_pending_invoice; + + /// No description provided for @clear_pending_confirm_title. + /// + /// In es, this message translates to: + /// **'¿Limpiar factura pendiente?'** + String get clear_pending_confirm_title; + + /// No description provided for @clear_pending_confirm_message. + /// + /// In es, this message translates to: + /// **'Esta transacción pendiente se eliminará del historial. La factura seguirá existiendo en LNBits hasta su expiración.'** + String get clear_pending_confirm_message; + + /// No description provided for @invoice_cleared_from_history. + /// + /// In es, this message translates to: + /// **'Factura eliminada del historial'** + String get invoice_cleared_from_history; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 031d657..102df37 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1014,4 +1014,17 @@ class AppLocalizationsDe extends AppLocalizations { @override String get qr_scanner_instructions => 'Kamera auf den QR-Code richten\num Rechnung oder Adresse zu scannen'; + + @override + String get clear_pending_invoice => 'Aus Verlauf entfernen'; + + @override + String get clear_pending_confirm_title => 'Ausstehende Rechnung entfernen?'; + + @override + String get clear_pending_confirm_message => + 'Diese ausstehende Transaktion wird aus dem Verlauf entfernt. Die Rechnung bleibt bis zum Ablauf auf LNBits bestehen.'; + + @override + String get invoice_cleared_from_history => 'Rechnung aus Verlauf entfernt'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 3b87bf7..0f33df2 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -989,4 +989,17 @@ class AppLocalizationsEn extends AppLocalizations { @override String get qr_scanner_instructions => 'Point the camera at the QR code\nto scan the invoice or address'; + + @override + String get clear_pending_invoice => 'Clear from history'; + + @override + String get clear_pending_confirm_title => 'Clear pending invoice?'; + + @override + String get clear_pending_confirm_message => + 'This pending transaction will be removed from history. The invoice will remain on LNBits until it expires.'; + + @override + String get invoice_cleared_from_history => 'Invoice removed from history'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index d28b1d1..137aa14 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -1003,4 +1003,17 @@ class AppLocalizationsEs extends AppLocalizations { @override String get qr_scanner_instructions => 'Apunta la cámara al código QR\npara escanear la factura o dirección'; + + @override + String get clear_pending_invoice => 'Limpiar del historial'; + + @override + String get clear_pending_confirm_title => '¿Limpiar factura pendiente?'; + + @override + String get clear_pending_confirm_message => + 'Esta transacción pendiente se eliminará del historial. La factura seguirá existiendo en LNBits hasta su expiración.'; + + @override + String get invoice_cleared_from_history => 'Factura eliminada del historial'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index c1c2d0a..1f1adfb 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1020,4 +1020,18 @@ class AppLocalizationsFr extends AppLocalizations { @override String get qr_scanner_instructions => 'Pointez la caméra vers le code QR\npour scanner la facture ou l\'adresse'; + + @override + String get clear_pending_invoice => 'Effacer de l\'historique'; + + @override + String get clear_pending_confirm_title => 'Effacer la facture en attente ?'; + + @override + String get clear_pending_confirm_message => + 'Cette transaction en attente sera supprimée de l\'historique. La facture restera sur LNBits jusqu\'à son expiration.'; + + @override + String get invoice_cleared_from_history => + 'Facture supprimée de l\'historique'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 6021611..1a157c3 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1017,4 +1017,17 @@ class AppLocalizationsIt extends AppLocalizations { @override String get qr_scanner_instructions => 'Punta la fotocamera sul codice QR\nper scansionare la fattura o l\'indirizzo'; + + @override + String get clear_pending_invoice => 'Cancella dalla cronologia'; + + @override + String get clear_pending_confirm_title => 'Cancellare fattura in sospeso?'; + + @override + String get clear_pending_confirm_message => + 'Questa transazione in sospeso verrà rimossa dalla cronologia. La fattura rimarrà su LNBits fino alla scadenza.'; + + @override + String get invoice_cleared_from_history => 'Fattura rimossa dalla cronologia'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index 23cfd53..57a6d23 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -1003,4 +1003,17 @@ class AppLocalizationsPt extends AppLocalizations { @override String get qr_scanner_instructions => 'Aponte a câmera para o código QR\npara escanear a fatura ou endereço'; + + @override + String get clear_pending_invoice => 'Limpar do histórico'; + + @override + String get clear_pending_confirm_title => 'Limpar fatura pendente?'; + + @override + String get clear_pending_confirm_message => + 'Esta transação pendente será removida do histórico. A fatura continuará existindo no LNBits até expirar.'; + + @override + String get invoice_cleared_from_history => 'Fatura removida do histórico'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index aa87202..3e43c49 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -993,4 +993,17 @@ class AppLocalizationsRu extends AppLocalizations { @override String get qr_scanner_instructions => 'Наведите камеру на QR-код\nчтобы сканировать счёт или адрес'; + + @override + String get clear_pending_invoice => 'Удалить из истории'; + + @override + String get clear_pending_confirm_title => 'Удалить ожидающий счёт?'; + + @override + String get clear_pending_confirm_message => + 'Эта ожидающая транзакция будет удалена из истории. Счёт останется на LNBits до истечения срока.'; + + @override + String get invoice_cleared_from_history => 'Счёт удалён из истории'; } 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..a73722b 100644 --- a/lib/screens/7history_screen.dart +++ b/lib/screens/7history_screen.dart @@ -10,6 +10,8 @@ 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'; +import '../services/invoice_service.dart'; class HistoryScreen extends StatefulWidget { const HistoryScreen({super.key}); @@ -32,6 +34,7 @@ class _HistoryScreenState extends State with TickerProviderStateM TransactionFilter _currentFilter = TransactionFilter.all; final ScrollController _scrollController = ScrollController(); + final InvoiceService _invoiceService = InvoiceService(); @override void initState() { @@ -165,14 +168,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; } } @@ -617,7 +626,7 @@ class _HistoryScreenState extends State with TickerProviderStateM Widget _buildTransactionCard(TransactionInfo transaction, int index, AppTokens t) { final iconColor = _getTransactionIconColor(transaction, t); - return Container( + final card = Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: t.surface, @@ -633,7 +642,6 @@ class _HistoryScreenState extends State with TickerProviderStateM padding: const EdgeInsets.all(16), child: Row( children: [ - // Leading icon Container( width: 48, height: 48, @@ -650,7 +658,6 @@ class _HistoryScreenState extends State with TickerProviderStateM const SizedBox(width: 16), - // Content Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -700,12 +707,11 @@ class _HistoryScreenState extends State with TickerProviderStateM ), ), - // Amount column Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - transaction.displayAmount.split('\n').first, // Show sats amount + transaction.displayAmount.split('\n').first, style: TextStyle( color: iconColor, fontSize: 16, @@ -737,6 +743,60 @@ class _HistoryScreenState extends State with TickerProviderStateM ), ), ); + + if (transaction.isPending && transaction.paymentHash != null) { + return Dismissible( + key: ValueKey('pending_${transaction.paymentHash}'), + direction: DismissDirection.horizontal, + confirmDismiss: (direction) async { + final l = AppLocalizations.of(context)!; + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l.clear_pending_confirm_title), + content: Text(l.clear_pending_confirm_message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(l.cancel_button), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(l.clear_pending_invoice), + ), + ], + ), + ); + if (confirmed == true) { + _doClearPendingTransaction(transaction); + } + return false; + }, + background: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: t.statusWarning.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(16), + ), + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 24), + child: Icon(Icons.delete_outline, color: t.statusWarning, size: 28), + ), + secondaryBackground: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: t.statusWarning.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(16), + ), + alignment: Alignment.centerLeft, + padding: const EdgeInsets.only(left: 24), + child: Icon(Icons.delete_outline, color: t.statusWarning, size: 28), + ), + child: card, + ); + } + + return card; } void _showTransactionDetails(TransactionInfo transaction) { @@ -855,6 +915,29 @@ class _HistoryScreenState extends State with TickerProviderStateM if (transaction.fee != null) _buildDetailRow(t, 'Fee', '${(transaction.fee! / 1000).toStringAsFixed(3)} sats'), _buildDetailRow(t, AppLocalizations.of(context)!.invoice_status_label, _getTransactionStatus(transaction)), + + if (transaction.isPending && transaction.paymentHash != null) ...[ + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + Navigator.pop(context); + _clearPendingTransaction(transaction); + }, + icon: const Icon(Icons.delete_outline, size: 18), + label: Text(AppLocalizations.of(context)!.clear_pending_invoice), + style: ElevatedButton.styleFrom( + backgroundColor: t.statusWarning.withValues(alpha: 0.2), + foregroundColor: t.statusWarning, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], ], ), ), @@ -864,6 +947,62 @@ class _HistoryScreenState extends State with TickerProviderStateM ); } + void _doClearPendingTransaction(TransactionInfo transaction) { + if (transaction.paymentHash == null) return; + + unawaited(ClearedInvoiceStore.instance.add(transaction.paymentHash!)); + unawaited(_tryCancelInvoiceOnServer(transaction.paymentHash!)); + + if (!mounted) return; + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context)!.invoice_cleared_from_history)), + ); + } + + Future _clearPendingTransaction(TransactionInfo transaction) async { + if (transaction.paymentHash == null) return; + + final l = AppLocalizations.of(context)!; + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(l.clear_pending_confirm_title), + content: Text(l.clear_pending_confirm_message), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: Text(l.cancel_button), + ), + TextButton( + onPressed: () => Navigator.pop(context, true), + child: Text(l.clear_pending_invoice), + ), + ], + ), + ); + + if (confirmed != true) return; + _doClearPendingTransaction(transaction); + } + + 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.adminKey, + paymentHash: paymentHash, + ); + } catch (_) {} + } + Widget _buildInvoiceQRSection(TransactionInfo transaction, AppTokens t) { final invoice = transaction.invoice!; final l = AppLocalizations.of(context)!; diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index c4cb9ff..e7a7a0e 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'; @@ -46,6 +48,9 @@ class _ReceiveScreenState extends State { bool _nfcAvailable = false; bool _nfcChecked = false; + String? _cachedServerUrl; + WalletInfo? _cachedWallet; + @override void initState() { super.initState(); @@ -125,6 +130,11 @@ class _ReceiveScreenState extends State { _yadioService.dispose(); _invoicePaymentTimer?.cancel(); _invoicePaymentTimeoutTimer?.cancel(); + if (_generatedInvoice != null) { + final hash = _generatedInvoice!.paymentHash; + unawaited(ClearedInvoiceStore.instance.add(hash)); + unawaited(_tryCancelInvoiceOnServer(hash)); + } super.dispose(); } @@ -781,15 +791,37 @@ class _ReceiveScreenState extends State { } void _clearInvoice() { + _discardInvoice(); + _showAccentSnackBar( + icon: Icons.check_circle, + message: AppLocalizations.of(context)!.invoice_cleared_message, + ); + } + + void _discardInvoice() { _invoicePaymentTimer?.cancel(); _invoicePaymentTimeoutTimer?.cancel(); + if (_generatedInvoice != null) { + final hash = _generatedInvoice!.paymentHash; + unawaited(ClearedInvoiceStore.instance.add(hash)); + unawaited(_tryCancelInvoiceOnServer(hash)); + } setState(() { _generatedInvoice = null; }); - _showAccentSnackBar( - icon: Icons.check_circle, - message: AppLocalizations.of(context)!.invoice_cleared_message, - ); + } + + Future _tryCancelInvoiceOnServer(String paymentHash) async { + final serverUrl = _cachedServerUrl; + final wallet = _cachedWallet; + if (serverUrl == null || wallet == null) return; + try { + await _invoiceService.cancelInvoice( + serverUrl: serverUrl, + adminKey: wallet.adminKey, + paymentHash: paymentHash, + ); + } catch (_) {} } void _showCopySheet(LNAddress? defaultAddress) { @@ -1322,6 +1354,9 @@ class _ReceiveScreenState extends State { final wallet = walletProvider.primaryWallet; final serverUrl = authProvider.sessionData?.serverUrl; + _cachedWallet = wallet; + _cachedServerUrl = serverUrl; + if (wallet == null || serverUrl == null) { throw Exception(AppLocalizations.of(context)!.no_wallet_error); } @@ -1444,9 +1479,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(); }