Skip to content
4 changes: 4 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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());
}

Expand Down
13 changes: 10 additions & 3 deletions lib/screens/7history_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down Expand Up @@ -165,14 +166,20 @@ class _HistoryScreenState extends State<HistoryScreen> with TickerProviderStateM
}

List<TransactionInfo> 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;
}
}

Expand Down
44 changes: 41 additions & 3 deletions lib/screens/9receive_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -28,6 +29,7 @@ class ReceiveScreen extends StatefulWidget {
}

class _ReceiveScreenState extends State<ReceiveScreen> {

final _amountController = TextEditingController();
final _noteController = TextEditingController();
String _selectedCurrency = 'sats';
Expand Down Expand Up @@ -124,6 +126,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
_yadioService.dispose();
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
if (_generatedInvoice != null) {
ClearedInvoiceStore.instance.add(_generatedInvoice!.paymentHash);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
super.dispose();
}

Expand Down Expand Up @@ -782,6 +787,13 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
void _clearInvoice() {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();

if (_generatedInvoice != null) {
final hash = _generatedInvoice!.paymentHash;
ClearedInvoiceStore.instance.add(hash);
unawaited(_tryCancelInvoiceOnServer(hash));
}

setState(() {
_generatedInvoice = null;
});
Expand All @@ -791,6 +803,34 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
);
}

void _discardInvoice() {
_invoicePaymentTimer?.cancel();
_invoicePaymentTimeoutTimer?.cancel();
if (_generatedInvoice != null) {
ClearedInvoiceStore.instance.add(_generatedInvoice!.paymentHash);
}
setState(() {
_generatedInvoice = null;
});
}

Future<void> _tryCancelInvoiceOnServer(String paymentHash) async {
try {
final walletProvider = context.read<WalletProvider>();
final authProvider = context.read<AuthProvider>();
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 (_) {}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

void _showCopySheet(LNAddress? defaultAddress) {
final hasInvoice = _generatedInvoice != null;
final lnurl = defaultAddress?.lnurl;
Expand Down Expand Up @@ -1436,9 +1476,7 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
_invoicePaymentTimeoutTimer = Timer(const Duration(minutes: 10), () {
_invoicePaymentTimer?.cancel();
if (!mounted) return;
setState(() {
_generatedInvoice = null;
});
_discardInvoice();
_showInfoSnackBar(
AppLocalizations.of(context)!.invoice_monitoring_timeout_message,
);
Expand Down
40 changes: 40 additions & 0 deletions lib/services/cleared_invoice_store.dart
Original file line number Diff line number Diff line change
@@ -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<String> _hashes = {};

Future<void> load() async {
try {
final prefs = await SharedPreferences.getInstance();
final stored = prefs.getString(_storageKey);
if (stored != null && stored.isNotEmpty) {
final List<dynamic> decoded = jsonDecode(stored);
_hashes = decoded.cast<String>().toSet();
}
} catch (_) {
_hashes = {};
}
}

Future<void> add(String hash) async {
_hashes.add(hash);
await _persist();
}

bool contains(String? hash) => hash != null && _hashes.contains(hash);

Set<String> get all => Set.unmodifiable(_hashes);

Future<void> _persist() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_storageKey, jsonEncode(_hashes.toList()));
} catch (_) {}
}
}
43 changes: 43 additions & 0 deletions lib/services/invoice_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,49 @@ class InvoiceService {
};
}

Future<bool> 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();
}
Expand Down