From 735d97b1e0188595fbe83bc107bf6e4260ba5a5e Mon Sep 17 00:00:00 2001 From: "@Delagado74" Date: Sat, 2 May 2026 12:29:04 -0400 Subject: [PATCH 01/13] feat(hce): add Android HCE configuration (Manifest, APDU service, strings) --- android/app/src/main/AndroidManifest.xml | 12 ++++++++++++ android/app/src/main/res/values/strings.xml | 5 +++++ android/app/src/main/res/xml/apduservice.xml | 7 +++++++ 3 files changed, 24 insertions(+) create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/xml/apduservice.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index acae414..5d137bc 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -79,6 +79,18 @@ + + + + + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + android.nfc.tech.IsoDep + + + android.nfc.tech.Ndef + + diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index 4354536..8cf29bd 100644 --- a/lib/screens/9receive_screen.dart +++ b/lib/screens/9receive_screen.dart @@ -980,17 +980,57 @@ class _ReceiveScreenState extends State { return; } - if (_generatedInvoice != null) { - _openNfcChargeSheet(_generatedInvoice!.paymentRequest); + // Sin factura → HCE directo (BoltCard requiere factura) + if (_generatedInvoice == null) { + final lnAddressProvider = context.read(); + final lnurl = lnAddressProvider.defaultAddress?.lnurl; + if (lnurl != null) { + _openNfcChargeSheet(lnurl, modo: ModoNfcRecibir.hceWallet); + } else { + _showRequestAmountModal(autoStartNfcAfterGenerate: true); + } return; } - _showRequestAmountModal(autoStartNfcAfterGenerate: true); + // Con factura → preguntar modo + final modo = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Modo NFC'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.credit_card), + title: const Text('Cargar BoltCard'), + subtitle: const Text('Leer tarjeta y enviar factura'), + onTap: () => Navigator.pop(context, ModoNfcRecibir.lectorBoltcard), + ), + ListTile( + leading: const Icon(Icons.nfc), + title: const Text('Emular HCE (Phoenix)'), + subtitle: const Text('Teléfono como tarjeta'), + onTap: () => Navigator.pop(context, ModoNfcRecibir.hceWallet), + ), + ], + ), + ), + ); + + if (modo == null) return; + + if (modo == ModoNfcRecibir.hceWallet) { + _openNfcChargeSheet(_generatedInvoice!.paymentRequest, + modo: modo); + } else { + _openNfcChargeSheet(_generatedInvoice!.paymentRequest, + modo: modo); + } } - void _openNfcChargeSheet(String lnurl, {bool useHce = true}) { + void _openNfcChargeSheet(String contenido, {required ModoNfcRecibir modo}) { setState(() { - _isHceActive = useHce; + _isHceActive = modo == ModoNfcRecibir.hceWallet; }); showModalBottomSheet( context: context, @@ -999,8 +1039,8 @@ class _ReceiveScreenState extends State { isDismissible: false, enableDrag: false, builder: (sheetContext) => _NfcChargeSheet( - invoice: lnurl, - useHce: useHce, + invoice: contenido, + modo: modo, onFinish: () { setState(() { _isHceActive = false; @@ -1020,7 +1060,7 @@ class _ReceiveScreenState extends State { ); } - void _showRequestAmountModal({bool autoStartNfcAfterGenerate = false}) { + void _showRequestAmountModal({bool autoStartNfcAfterGenerate = false, bool modoLector = false}) { _amountController.clear(); _noteController.clear(); setState(() { @@ -1272,6 +1312,7 @@ class _ReceiveScreenState extends State { onPressed: () => _confirmRequestAmount( modalContext, autoStartNfcAfterGenerate: autoStartNfcAfterGenerate, + modoLector: modoLector, ), style: ElevatedButton.styleFrom( backgroundColor: context.tokens.accentSolid, @@ -1309,6 +1350,7 @@ class _ReceiveScreenState extends State { void _confirmRequestAmount( BuildContext modalContext, { bool autoStartNfcAfterGenerate = false, + bool modoLector = false, }) async { if (_amountController.text.trim().isEmpty) { _showErrorSnackBar(AppLocalizations.of(context)!.invalid_amount_error); @@ -1381,15 +1423,25 @@ class _ReceiveScreenState extends State { _startInvoicePaymentMonitoring(invoice, wallet, serverUrl); if (autoStartNfcAfterGenerate && _nfcAvailable) { - // Obtener Lightning Address del provider - final lnAddressProvider = context.read(); - final defaultAddress = lnAddressProvider.defaultAddress; - - // Detectar qué modo usar: si hay LNURL de Lightning Address, usar HCE (Phoenix) - // Si no, intentar BoltCard (lector) - final lnurlForHce = defaultAddress?.lnurl ?? invoice.paymentRequest; - final useHce = defaultAddress?.lnurl != null; - _openNfcChargeSheet(lnurlForHce, useHce: useHce); + // Decidir modo según si es lector o HCE + if (modoLector) { + // Leer BoltCard → usar invoice generada + if (_generatedInvoice != null) { + _openNfcChargeSheet(_generatedInvoice!.paymentRequest, + modo: ModoNfcRecibir.lectorBoltcard); + } + } else { + // HCE → usar LNURL o invoice según corresponda + final lnAddressProvider = context.read(); + final lnurl = lnAddressProvider.defaultAddress?.lnurl; + + if (lnurl != null) { + _openNfcChargeSheet(lnurl, modo: ModoNfcRecibir.hceWallet); + } else if (_generatedInvoice != null) { + _openNfcChargeSheet(_generatedInvoice!.paymentRequest, + modo: ModoNfcRecibir.hceWallet); + } + } } } catch (e) { setState(() { @@ -1562,11 +1614,11 @@ class _ReceiveScreenState extends State { class _NfcChargeSheet extends StatefulWidget { final String invoice; - final bool useHce; + final ModoNfcRecibir modo; final VoidCallback? onFinish; const _NfcChargeSheet({ required this.invoice, - this.useHce = true, + required this.modo, this.onFinish, }); @@ -1589,8 +1641,8 @@ class _NfcChargeSheetState extends State<_NfcChargeSheet> { Future _start() async { await _service.startChargeSession( - lnurlWithdraw: widget.invoice, - useHce: widget.useHce, + lnurlOrInvoice: widget.invoice, + modo: widget.modo, onStatus: (result) { if (!mounted) return; setState(() { @@ -1652,7 +1704,7 @@ class _NfcChargeSheetState extends State<_NfcChargeSheet> { switch (_status) { case NfcChargeStatus.scanning: case NfcChargeStatus.reading: - return widget.useHce + return widget.modo == ModoNfcRecibir.hceWallet ? l10n.nfc_hce_message : l10n.nfc_scanning_message; case NfcChargeStatus.charging: diff --git a/lib/services/nfc_charge_service.dart b/lib/services/nfc_charge_service.dart index 0dfd30c..f0fbc1e 100644 --- a/lib/services/nfc_charge_service.dart +++ b/lib/services/nfc_charge_service.dart @@ -23,6 +23,11 @@ enum NfcChargeStatus { callbackError, } +enum ModoNfcRecibir { + hceWallet, // Emitir HCE → pagador usa Phoenix u otra wallet + lectorBoltcard, // Lector NFC → pagador usa BoltCard física +} + class NfcChargeResult { final NfcChargeStatus status; final String? message; @@ -65,97 +70,113 @@ class NfcChargeService { bool get isSessionActive => _sessionActive; Future startChargeSession({ - required String lnurlWithdraw, + required String lnurlOrInvoice, + required ModoNfcRecibir modo, required void Function(NfcChargeResult) onStatus, - bool useHce = true, // true para Phoenix (emular), false para BoltCard (leer) }) async { if (_sessionActive) return; _sessionActive = true; + + // CRÍTICO: asegurarse que HCE está detenido antes de activar lector + if (modo == ModoNfcRecibir.lectorBoltcard) { + try { await _hce.stopNfcHce(); } catch (_) {} + await Future.delayed(const Duration(milliseconds: 300)); + } + onStatus(const NfcChargeResult(NfcChargeStatus.scanning)); try { - if (useHce) { - // HCE Mode: Emular tarjeta NFC con LNURL para recibir pagos (Phoenix) - _debugLog('HCE: Iniciando emulación NFC con LNURL: $lnurlWithdraw'); - await _hce.startNfcHce( - lnurlWithdraw, - mimeType: 'text/plain', - persistMessage: false, - ); - _debugLog('HCE: Emulación NFC iniciada correctamente'); - onStatus(const NfcChargeResult(NfcChargeStatus.reading)); - } else { - // BoltCard Mode: Leer tarjeta NFC (lector) - _debugLog('NFC: Iniciando lectura de tarjeta'); - await NfcManager.instance.startSession( - pollingOptions: NfcPollingOption.values.toSet(), - invalidateAfterFirstRead: true, - alertMessage: 'Acerca la tarjeta', - onDiscovered: (NfcTag tag) async { - try { - onStatus(const NfcChargeResult(NfcChargeStatus.reading)); - final url = _extractUriFromTag(tag); - if (url == null) { - onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'Tag no compatible'); - return; - } - _debugLog('Tag URL: $url'); - final metaResponse = await _dio.get(url); - final meta = metaResponse.data; - if (meta is! Map) { - onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'Respuesta inválida'); - return; - } - if (meta['tag'] != 'withdrawRequest') { - onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'No es Boltcard'); - return; - } - final callbackValue = meta['callback']; - final k1Value = meta['k1']; - if (callbackValue is! String || - callbackValue.isEmpty || - k1Value is! String || - k1Value.isEmpty) { - onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'Datos incompletos'); - return; - } - final callback = callbackValue; - final k1 = k1Value; - onStatus(const NfcChargeResult(NfcChargeStatus.charging)); - final claim = await _dio.get(callback, queryParameters: { - 'k1': k1, - 'pr': lnurlWithdraw, - }); - final claimData = claim.data; - if (claimData is Map && claimData['status'] == 'OK') { - onStatus(const NfcChargeResult(NfcChargeStatus.success)); - await _safeStop(alertMessage: 'OK'); - } else { - final reason = claimData is Map - ? claimData['reason']?.toString() - : null; + switch (modo) { + case ModoNfcRecibir.hceWallet: + // HCE Mode: Emitir para wallet (Phoenix) + _debugLog('HCE: Emitiendo para wallet: $lnurlOrInvoice'); + await _hce.startNfcHce( + lnurlOrInvoice, + mimeType: 'text/plain', + persistMessage: false, + ); + _debugLog('HCE: Emulación NFC iniciada correctamente'); + onStatus(const NfcChargeResult(NfcChargeStatus.reading)); + break; + + case ModoNfcRecibir.lectorBoltcard: + // BoltCard Mode: Lector NFC + _debugLog('NFC: Iniciando lectura de tarjeta'); + await NfcManager.instance.startSession( + pollingOptions: {NfcPollingOption.iso14443}, + invalidateAfterFirstRead: false, + alertMessage: 'Acerca la tarjeta', + onDiscovered: (NfcTag tag) async { + try { + onStatus(const NfcChargeResult(NfcChargeStatus.reading)); + + final url = _extractUriFromTag(tag); + if (url == null) { + onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); + await _safeStop(errorMessage: 'Tag no compatible'); + return; + } + _debugLog('Tag URL: $url'); + + final metaResponse = await _dio.get(url); + final meta = metaResponse.data; + if (meta is! Map) { + onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); + await _safeStop(errorMessage: 'Respuesta inválida'); + return; + } + if (meta['tag'] != 'withdrawRequest') { + onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); + await _safeStop(errorMessage: 'No es Boltcard'); + return; + } + final callbackValue = meta['callback']; + final k1Value = meta['k1']; + if (callbackValue is! String || + callbackValue.isEmpty || + k1Value is! String || + k1Value.isEmpty) { + onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); + await _safeStop(errorMessage: 'Datos incompletos'); + return; + } + final callback = callbackValue; + final k1 = k1Value; + + onStatus(const NfcChargeResult(NfcChargeStatus.charging)); + + final claim = await _dio.get(callback, queryParameters: { + 'k1': k1, + 'pr': lnurlOrInvoice, + }); + + final claimData = claim.data; + if (claimData is Map && claimData['status'] == 'OK') { + onStatus(const NfcChargeResult(NfcChargeStatus.success)); + await _safeStop(alertMessage: 'OK'); + } else { + final reason = claimData is Map + ? claimData['reason']?.toString() + : null; + onStatus(NfcChargeResult( + NfcChargeStatus.callbackError, + message: reason, + )); + await _safeStop(errorMessage: reason); + } + } catch (e) { + _debugLog('Discovery handler error: $e'); onStatus(NfcChargeResult( - NfcChargeStatus.callbackError, - message: reason, + NfcChargeStatus.networkError, + message: e.toString(), )); - await _safeStop(errorMessage: reason); + await _safeStop(errorMessage: 'Error de red'); + } finally { + _sessionActive = false; } - } catch (e) { - _debugLog('Discovery handler error: $e'); - onStatus(NfcChargeResult( - NfcChargeStatus.networkError, - message: e.toString(), - )); - await _safeStop(errorMessage: 'Error de red'); - } finally { - _sessionActive = false; - } - }, - ); + }, + ); + break; } } catch (e) { _debugLog('startSession error: $e'); From 3ef532c5a35cd26d3cfbdf9803c3b2519a107ea4 Mon Sep 17 00:00:00 2001 From: "@Delagado74" Date: Sat, 2 May 2026 19:34:12 -0400 Subject: [PATCH 09/13] feat(i18n): add NFC mode and error messages for all languages - Add i18n keys for NFC mode selector (BoltCard/HCE) - Add error messages for BoltCard reader mode - Update receive screen to use AppLocalizations - Fix NfcChargeService to accept AppLocalizations - Move service init to didChangeDependencies (fix InheritedWidget error) - Add SingleTickerProviderStateMixin to _NfcChargeSheetState - Fix _tabController not defined error --- lib/l10n/app_de.arb | 12 +++- lib/l10n/app_en.arb | 12 +++- lib/l10n/app_es.arb | 12 +++- lib/l10n/app_fr.arb | 12 +++- lib/l10n/app_it.arb | 12 +++- lib/l10n/app_pt.arb | 12 +++- lib/l10n/app_ru.arb | 12 +++- lib/l10n/generated/app_localizations.dart | 60 ++++++++++++++++++++ lib/l10n/generated/app_localizations_de.dart | 30 ++++++++++ lib/l10n/generated/app_localizations_en.dart | 30 ++++++++++ lib/l10n/generated/app_localizations_es.dart | 30 ++++++++++ lib/l10n/generated/app_localizations_fr.dart | 31 ++++++++++ lib/l10n/generated/app_localizations_it.dart | 30 ++++++++++ lib/l10n/generated/app_localizations_pt.dart | 30 ++++++++++ lib/l10n/generated/app_localizations_ru.dart | 30 ++++++++++ lib/screens/9receive_screen.dart | 29 +++++++--- lib/services/nfc_charge_service.dart | 16 +++--- 17 files changed, 378 insertions(+), 22 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 4d75739..99a3c2d 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -374,5 +374,15 @@ "nfc_charge_error_prefix": "NFC-Fehler beim Einziehen: ", "nfc_charge_unknown_error": "Unbekannter Fehler beim Einziehen", "share_ready_message": "Bereit zum Teilen", - "lnurl_copied_message": "LNURL in die Zwischenablage kopiert" + "lnurl_copied_message": "LNURL in die Zwischenablage kopiert", + "nfc_mode_title": "NFC-Modus", + "nfc_mode_boltcard": "BoltCard laden", + "nfc_mode_boltcard_subtitle": "Karte lesen und Rechnung senden", + "nfc_mode_hce": "HCE emulieren", + "nfc_mode_hce_subtitle": "Telefon als Karte", + "nfc_tag_not_compatible": "Tag nicht kompatibel", + "nfc_not_boltcard": "Keine Boltcard", + "nfc_incomplete_data": "Unvollständige Daten", + "nfc_invalid_response": "Ungültige Antwort", + "nfc_network_error": "Netzwerkfehler" } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 741a0d8..fd462e7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -389,5 +389,15 @@ "nfc_charge_error_prefix": "NFC charge error: ", "nfc_charge_unknown_error": "Unknown error during charge", "share_ready_message": "Ready to share", - "lnurl_copied_message": "LNURL copied to clipboard" + "lnurl_copied_message": "LNURL copied to clipboard", + "nfc_mode_title": "NFC Mode", + "nfc_mode_boltcard": "Load BoltCard", + "nfc_mode_boltcard_subtitle": "Read card and send invoice", + "nfc_mode_hce": "Emulate HCE", + "nfc_mode_hce_subtitle": "Phone as card", + "nfc_tag_not_compatible": "Tag not compatible", + "nfc_not_boltcard": "Not a Boltcard", + "nfc_incomplete_data": "Incomplete data", + "nfc_invalid_response": "Invalid response", + "nfc_network_error": "Network error" } \ No newline at end of file diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 80d206c..083ccdf 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -374,5 +374,15 @@ "nfc_charge_error_prefix": "Error en cobro NFC: ", "nfc_charge_unknown_error": "Error desconocido al cobrar", "share_ready_message": "Listo para compartir", - "lnurl_copied_message": "LNURL copiado al portapapeles" + "lnurl_copied_message": "LNURL copiado al portapapeles", + "nfc_mode_title": "Modo NFC", + "nfc_mode_boltcard": "Cargar BoltCard", + "nfc_mode_boltcard_subtitle": "Leer tarjeta y enviar factura", + "nfc_mode_hce": "Emular HCE", + "nfc_mode_hce_subtitle": "Teléfono como tarjeta", + "nfc_tag_not_compatible": "Tag no compatible", + "nfc_not_boltcard": "No es Boltcard", + "nfc_incomplete_data": "Datos incompletos", + "nfc_invalid_response": "Respuesta inválida", + "nfc_network_error": "Error de red" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 9a73668..a53168c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -374,5 +374,15 @@ "nfc_charge_error_prefix": "Erreur d'encaissement NFC : ", "nfc_charge_unknown_error": "Erreur inconnue lors de l'encaissement", "share_ready_message": "Prêt à partager", - "lnurl_copied_message": "LNURL copié dans le presse-papiers" + "lnurl_copied_message": "LNURL copié dans le presse-papiers", + "nfc_mode_title": "Mode NFC", + "nfc_mode_boltcard": "Charger BoltCard", + "nfc_mode_boltcard_subtitle": "Lire la carte et envoyer la facture", + "nfc_mode_hce": "Émuler HCE", + "nfc_mode_hce_subtitle": "Téléphone comme carte", + "nfc_tag_not_compatible": "Tag non compatible", + "nfc_not_boltcard": "Pas une Boltcard", + "nfc_incomplete_data": "Données incomplètes", + "nfc_invalid_response": "Réponse invalide", + "nfc_network_error": "Erreur réseau" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 3f43b1e..cd73102 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -374,5 +374,15 @@ "nfc_charge_error_prefix": "Errore incasso NFC: ", "nfc_charge_unknown_error": "Errore sconosciuto durante l'incasso", "share_ready_message": "Pronto da condividere", - "lnurl_copied_message": "LNURL copiato negli appunti" + "lnurl_copied_message": "LNURL copiato negli appunti", + "nfc_mode_title": "Modalità NFC", + "nfc_mode_boltcard": "Carica BoltCard", + "nfc_mode_boltcard_subtitle": "Leggi carta e invia fattura", + "nfc_mode_hce": "Emula HCE", + "nfc_mode_hce_subtitle": "Telefono come carta", + "nfc_tag_not_compatible": "Tag non compatibile", + "nfc_not_boltcard": "Non è una Boltcard", + "nfc_incomplete_data": "Dati incompleti", + "nfc_invalid_response": "Risposta non valida", + "nfc_network_error": "Errore di rete" } \ No newline at end of file diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 8d92bda..4c6e711 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -374,5 +374,15 @@ "nfc_charge_error_prefix": "Erro ao cobrar com NFC: ", "nfc_charge_unknown_error": "Erro desconhecido ao cobrar", "share_ready_message": "Pronto para compartilhar", - "lnurl_copied_message": "LNURL copiado para a área de transferência" + "lnurl_copied_message": "LNURL copiado para a área de transferência", + "nfc_mode_title": "Modo NFC", + "nfc_mode_boltcard": "Carregar BoltCard", + "nfc_mode_boltcard_subtitle": "Ler cartão e enviar fatura", + "nfc_mode_hce": "Emular HCE", + "nfc_mode_hce_subtitle": "Telefone como cartão", + "nfc_tag_not_compatible": "Tag não compatível", + "nfc_not_boltcard": "Não é Boltcard", + "nfc_incomplete_data": "Dados incompletos", + "nfc_invalid_response": "Resposta inválida", + "nfc_network_error": "Erro de rede" } \ No newline at end of file diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index bb56879..5737c89 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -374,5 +374,15 @@ "nfc_charge_error_prefix": "Ошибка приёма по NFC: ", "nfc_charge_unknown_error": "Неизвестная ошибка при списании", "share_ready_message": "Готово к отправке", - "lnurl_copied_message": "LNURL скопирован в буфер обмена" + "lnurl_copied_message": "LNURL скопирован в буфер обмена", + "nfc_mode_title": "Режим NFC", + "nfc_mode_boltcard": "Загрузить BoltCard", + "nfc_mode_boltcard_subtitle": "Считать карту и отправить счёт", + "nfc_mode_hce": "Эмуляция HCE", + "nfc_mode_hce_subtitle": "Телефон как карта", + "nfc_tag_not_compatible": "Тег не совместим", + "nfc_not_boltcard": "Не Boltcard", + "nfc_incomplete_data": "Неполные данные", + "nfc_invalid_response": "Неверный ответ", + "nfc_network_error": "Ошибка сети" } \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index ea2860b..9c5ce30 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -1907,6 +1907,66 @@ abstract class AppLocalizations { /// In es, this message translates to: /// **'LNURL copiado al portapapeles'** String get lnurl_copied_message; + + /// No description provided for @nfc_mode_title. + /// + /// In es, this message translates to: + /// **'Modo NFC'** + String get nfc_mode_title; + + /// No description provided for @nfc_mode_boltcard. + /// + /// In es, this message translates to: + /// **'Cargar BoltCard'** + String get nfc_mode_boltcard; + + /// No description provided for @nfc_mode_boltcard_subtitle. + /// + /// In es, this message translates to: + /// **'Leer tarjeta y enviar factura'** + String get nfc_mode_boltcard_subtitle; + + /// No description provided for @nfc_mode_hce. + /// + /// In es, this message translates to: + /// **'Emular HCE'** + String get nfc_mode_hce; + + /// No description provided for @nfc_mode_hce_subtitle. + /// + /// In es, this message translates to: + /// **'Teléfono como tarjeta'** + String get nfc_mode_hce_subtitle; + + /// No description provided for @nfc_tag_not_compatible. + /// + /// In es, this message translates to: + /// **'Tag no compatible'** + String get nfc_tag_not_compatible; + + /// No description provided for @nfc_not_boltcard. + /// + /// In es, this message translates to: + /// **'No es Boltcard'** + String get nfc_not_boltcard; + + /// No description provided for @nfc_incomplete_data. + /// + /// In es, this message translates to: + /// **'Datos incompletos'** + String get nfc_incomplete_data; + + /// No description provided for @nfc_invalid_response. + /// + /// In es, this message translates to: + /// **'Respuesta inválida'** + String get nfc_invalid_response; + + /// No description provided for @nfc_network_error. + /// + /// In es, this message translates to: + /// **'Error de red'** + String get nfc_network_error; } class _AppLocalizationsDelegate diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 2d9c7ac..cea1355 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1001,4 +1001,34 @@ class AppLocalizationsDe extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL in die Zwischenablage kopiert'; + + @override + String get nfc_mode_title => 'NFC-Modus'; + + @override + String get nfc_mode_boltcard => 'BoltCard laden'; + + @override + String get nfc_mode_boltcard_subtitle => 'Karte lesen und Rechnung senden'; + + @override + String get nfc_mode_hce => 'HCE emulieren'; + + @override + String get nfc_mode_hce_subtitle => 'Telefon als Karte'; + + @override + String get nfc_tag_not_compatible => 'Tag nicht kompatibel'; + + @override + String get nfc_not_boltcard => 'Keine Boltcard'; + + @override + String get nfc_incomplete_data => 'Unvollständige Daten'; + + @override + String get nfc_invalid_response => 'Ungültige Antwort'; + + @override + String get nfc_network_error => 'Netzwerkfehler'; } diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 8f9cfc8..d36cd9b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -977,4 +977,34 @@ class AppLocalizationsEn extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL copied to clipboard'; + + @override + String get nfc_mode_title => 'NFC Mode'; + + @override + String get nfc_mode_boltcard => 'Load BoltCard'; + + @override + String get nfc_mode_boltcard_subtitle => 'Read card and send invoice'; + + @override + String get nfc_mode_hce => 'Emulate HCE'; + + @override + String get nfc_mode_hce_subtitle => 'Phone as card'; + + @override + String get nfc_tag_not_compatible => 'Tag not compatible'; + + @override + String get nfc_not_boltcard => 'Not a Boltcard'; + + @override + String get nfc_incomplete_data => 'Incomplete data'; + + @override + String get nfc_invalid_response => 'Invalid response'; + + @override + String get nfc_network_error => 'Network error'; } diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 653888e..6868e67 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -990,4 +990,34 @@ class AppLocalizationsEs extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL copiado al portapapeles'; + + @override + String get nfc_mode_title => 'Modo NFC'; + + @override + String get nfc_mode_boltcard => 'Cargar BoltCard'; + + @override + String get nfc_mode_boltcard_subtitle => 'Leer tarjeta y enviar factura'; + + @override + String get nfc_mode_hce => 'Emular HCE'; + + @override + String get nfc_mode_hce_subtitle => 'Teléfono como tarjeta'; + + @override + String get nfc_tag_not_compatible => 'Tag no compatible'; + + @override + String get nfc_not_boltcard => 'No es Boltcard'; + + @override + String get nfc_incomplete_data => 'Datos incompletos'; + + @override + String get nfc_invalid_response => 'Respuesta inválida'; + + @override + String get nfc_network_error => 'Error de red'; } diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 4636009..471d38b 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1008,4 +1008,35 @@ class AppLocalizationsFr extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL copié dans le presse-papiers'; + + @override + String get nfc_mode_title => 'Mode NFC'; + + @override + String get nfc_mode_boltcard => 'Charger BoltCard'; + + @override + String get nfc_mode_boltcard_subtitle => + 'Lire la carte et envoyer la facture'; + + @override + String get nfc_mode_hce => 'Émuler HCE'; + + @override + String get nfc_mode_hce_subtitle => 'Téléphone comme carte'; + + @override + String get nfc_tag_not_compatible => 'Tag non compatible'; + + @override + String get nfc_not_boltcard => 'Pas une Boltcard'; + + @override + String get nfc_incomplete_data => 'Données incomplètes'; + + @override + String get nfc_invalid_response => 'Réponse invalide'; + + @override + String get nfc_network_error => 'Erreur réseau'; } diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index 26fb956..aeaf60c 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1004,4 +1004,34 @@ class AppLocalizationsIt extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL copiato negli appunti'; + + @override + String get nfc_mode_title => 'Modalità NFC'; + + @override + String get nfc_mode_boltcard => 'Carica BoltCard'; + + @override + String get nfc_mode_boltcard_subtitle => 'Leggi carta e invia fattura'; + + @override + String get nfc_mode_hce => 'Emula HCE'; + + @override + String get nfc_mode_hce_subtitle => 'Telefono come carta'; + + @override + String get nfc_tag_not_compatible => 'Tag non compatibile'; + + @override + String get nfc_not_boltcard => 'Non è una Boltcard'; + + @override + String get nfc_incomplete_data => 'Dati incompleti'; + + @override + String get nfc_invalid_response => 'Risposta non valida'; + + @override + String get nfc_network_error => 'Errore di rete'; } diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index fa3cff8..c9554ed 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -990,4 +990,34 @@ class AppLocalizationsPt extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL copiado para a área de transferência'; + + @override + String get nfc_mode_title => 'Modo NFC'; + + @override + String get nfc_mode_boltcard => 'Carregar BoltCard'; + + @override + String get nfc_mode_boltcard_subtitle => 'Ler cartão e enviar fatura'; + + @override + String get nfc_mode_hce => 'Emular HCE'; + + @override + String get nfc_mode_hce_subtitle => 'Telefone como cartão'; + + @override + String get nfc_tag_not_compatible => 'Tag não compatível'; + + @override + String get nfc_not_boltcard => 'Não é Boltcard'; + + @override + String get nfc_incomplete_data => 'Dados incompletos'; + + @override + String get nfc_invalid_response => 'Resposta inválida'; + + @override + String get nfc_network_error => 'Erro de rede'; } diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index b1a2d34..aefa197 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -981,4 +981,34 @@ class AppLocalizationsRu extends AppLocalizations { @override String get lnurl_copied_message => 'LNURL скопирован в буфер обмена'; + + @override + String get nfc_mode_title => 'Режим NFC'; + + @override + String get nfc_mode_boltcard => 'Загрузить BoltCard'; + + @override + String get nfc_mode_boltcard_subtitle => 'Считать карту и отправить счёт'; + + @override + String get nfc_mode_hce => 'Эмуляция HCE'; + + @override + String get nfc_mode_hce_subtitle => 'Телефон как карта'; + + @override + String get nfc_tag_not_compatible => 'Тег не совместим'; + + @override + String get nfc_not_boltcard => 'Не Boltcard'; + + @override + String get nfc_incomplete_data => 'Неполные данные'; + + @override + String get nfc_invalid_response => 'Неверный ответ'; + + @override + String get nfc_network_error => 'Ошибка сети'; } diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index 8cf29bd..3bd26cb 100644 --- a/lib/screens/9receive_screen.dart +++ b/lib/screens/9receive_screen.dart @@ -980,6 +980,8 @@ class _ReceiveScreenState extends State { return; } + final l10n = AppLocalizations.of(context)!; + // Sin factura → HCE directo (BoltCard requiere factura) if (_generatedInvoice == null) { final lnAddressProvider = context.read(); @@ -996,20 +998,20 @@ class _ReceiveScreenState extends State { final modo = await showDialog( context: context, builder: (context) => AlertDialog( - title: const Text('Modo NFC'), + title: Text(l10n.nfc_mode_title), content: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.credit_card), - title: const Text('Cargar BoltCard'), - subtitle: const Text('Leer tarjeta y enviar factura'), + title: Text(l10n.nfc_mode_boltcard), + subtitle: Text(l10n.nfc_mode_boltcard_subtitle), onTap: () => Navigator.pop(context, ModoNfcRecibir.lectorBoltcard), ), ListTile( leading: const Icon(Icons.nfc), - title: const Text('Emular HCE (Phoenix)'), - subtitle: const Text('Teléfono como tarjeta'), + title: Text(l10n.nfc_mode_hce), + subtitle: Text(l10n.nfc_mode_hce_subtitle), onTap: () => Navigator.pop(context, ModoNfcRecibir.hceWallet), ), ], @@ -1626,17 +1628,28 @@ class _NfcChargeSheet extends StatefulWidget { State<_NfcChargeSheet> createState() => _NfcChargeSheetState(); } -class _NfcChargeSheetState extends State<_NfcChargeSheet> { +class _NfcChargeSheetState extends State<_NfcChargeSheet> with SingleTickerProviderStateMixin { late final NfcChargeService _service; NfcChargeStatus _status = NfcChargeStatus.scanning; String? _errorMessage; bool _autoCloseScheduled = false; + late final TabController _tabController; + bool _serviceInitialized = false; @override void initState() { super.initState(); - _service = NfcChargeService(); - _start(); + _tabController = TabController(length: 2, vsync: this); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_serviceInitialized) { + _service = NfcChargeService(AppLocalizations.of(context)!); + _serviceInitialized = true; + _start(); + } } Future _start() async { diff --git a/lib/services/nfc_charge_service.dart b/lib/services/nfc_charge_service.dart index f0fbc1e..d876745 100644 --- a/lib/services/nfc_charge_service.dart +++ b/lib/services/nfc_charge_service.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:nfc_manager/nfc_manager.dart'; import '../core/utils/proxy_config.dart'; import 'app_info_service.dart'; +import '../l10n/generated/app_localizations.dart'; void _debugLog(String message) { if (kDebugMode) { @@ -37,8 +38,9 @@ class NfcChargeResult { class NfcChargeService { final Dio _dio = Dio(); bool _sessionActive = false; + final AppLocalizations _l10n; - NfcChargeService() { + NfcChargeService(this._l10n) { _configureDio(); } @@ -113,7 +115,7 @@ class NfcChargeService { final url = _extractUriFromTag(tag); if (url == null) { onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'Tag no compatible'); + await _safeStop(errorMessage: _l10n.nfc_tag_not_compatible); return; } _debugLog('Tag URL: $url'); @@ -122,12 +124,12 @@ class NfcChargeService { final meta = metaResponse.data; if (meta is! Map) { onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'Respuesta inválida'); + await _safeStop(errorMessage: _l10n.nfc_invalid_response); return; } if (meta['tag'] != 'withdrawRequest') { onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'No es Boltcard'); + await _safeStop(errorMessage: _l10n.nfc_not_boltcard); return; } final callbackValue = meta['callback']; @@ -137,7 +139,7 @@ class NfcChargeService { k1Value is! String || k1Value.isEmpty) { onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); - await _safeStop(errorMessage: 'Datos incompletos'); + await _safeStop(errorMessage: _l10n.nfc_incomplete_data); return; } final callback = callbackValue; @@ -164,13 +166,13 @@ class NfcChargeService { )); await _safeStop(errorMessage: reason); } - } catch (e) { + } catch (e) { _debugLog('Discovery handler error: $e'); onStatus(NfcChargeResult( NfcChargeStatus.networkError, message: e.toString(), )); - await _safeStop(errorMessage: 'Error de red'); + await _safeStop(errorMessage: _l10n.nfc_network_error); } finally { _sessionActive = false; } From 34f3deb91afda3d6be3b4b3f23a1eddd2fb1a525 Mon Sep 17 00:00:00 2001 From: "@Delagado74" Date: Sun, 3 May 2026 05:25:29 -0400 Subject: [PATCH 10/13] fix(nfc): add missing flutter_nfc_hce dependency and instantiate HCE service --- lib/services/nfc_charge_service.dart | 2 ++ pubspec.yaml | 1 + 2 files changed, 3 insertions(+) diff --git a/lib/services/nfc_charge_service.dart b/lib/services/nfc_charge_service.dart index d876745..30e3310 100644 --- a/lib/services/nfc_charge_service.dart +++ b/lib/services/nfc_charge_service.dart @@ -4,6 +4,7 @@ import 'dart:io' show Platform; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:nfc_manager/nfc_manager.dart'; +import 'package:flutter_nfc_hce/flutter_nfc_hce.dart'; import '../core/utils/proxy_config.dart'; import 'app_info_service.dart'; import '../l10n/generated/app_localizations.dart'; @@ -37,6 +38,7 @@ class NfcChargeResult { class NfcChargeService { final Dio _dio = Dio(); + final FlutterNfcHce _hce = FlutterNfcHce(); bool _sessionActive = false; final AppLocalizations _l10n; diff --git a/pubspec.yaml b/pubspec.yaml index da89a2c..9b46ae5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: app_links: ^6.3.2 share_plus: ^10.1.4 nfc_manager: ^3.5.0 + flutter_nfc_hce: ^0.1.8 dev_dependencies: flutter_test: From 94f543111413518e92e64988ba560e979478f494 Mon Sep 17 00:00:00 2001 From: "@Delagado74" Date: Sun, 3 May 2026 06:08:15 -0400 Subject: [PATCH 11/13] fix(nfc): address all CodeRabbit issues - HCE security, i18n, UI cleanup and session handling --- android/app/src/main/res/xml/apduservice.xml | 4 +-- lib/l10n/app_de.arb | 4 +-- lib/l10n/app_en.arb | 4 +-- lib/l10n/app_es.arb | 4 +-- lib/l10n/app_fr.arb | 4 +-- lib/l10n/app_it.arb | 4 +-- lib/l10n/app_pt.arb | 4 +-- lib/l10n/app_ru.arb | 4 +-- lib/l10n/generated/app_localizations.dart | 4 +-- lib/l10n/generated/app_localizations_de.dart | 4 +-- lib/l10n/generated/app_localizations_en.dart | 4 +-- lib/l10n/generated/app_localizations_es.dart | 4 +-- lib/l10n/generated/app_localizations_fr.dart | 4 +-- lib/l10n/generated/app_localizations_it.dart | 4 +-- lib/l10n/generated/app_localizations_pt.dart | 4 +-- lib/l10n/generated/app_localizations_ru.dart | 4 +-- lib/screens/9receive_screen.dart | 28 +++++++------------- lib/services/nfc_charge_service.dart | 9 +++++++ 18 files changed, 51 insertions(+), 50 deletions(-) diff --git a/android/app/src/main/res/xml/apduservice.xml b/android/app/src/main/res/xml/apduservice.xml index 759c467..ff679d5 100644 --- a/android/app/src/main/res/xml/apduservice.xml +++ b/android/app/src/main/res/xml/apduservice.xml @@ -1,7 +1,7 @@ + android:requireDeviceUnlock="true"> - + diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 99a3c2d..ab1bef3 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -376,8 +376,8 @@ "share_ready_message": "Bereit zum Teilen", "lnurl_copied_message": "LNURL in die Zwischenablage kopiert", "nfc_mode_title": "NFC-Modus", - "nfc_mode_boltcard": "BoltCard laden", - "nfc_mode_boltcard_subtitle": "Karte lesen und Rechnung senden", + "nfc_mode_boltcard": "BoltCard abbuchen", + "nfc_mode_boltcard_subtitle": "Karte lesen und Rechnung einziehen", "nfc_mode_hce": "HCE emulieren", "nfc_mode_hce_subtitle": "Telefon als Karte", "nfc_tag_not_compatible": "Tag nicht kompatibel", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index fd462e7..52ca8fd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -391,8 +391,8 @@ "share_ready_message": "Ready to share", "lnurl_copied_message": "LNURL copied to clipboard", "nfc_mode_title": "NFC Mode", - "nfc_mode_boltcard": "Load BoltCard", - "nfc_mode_boltcard_subtitle": "Read card and send invoice", + "nfc_mode_boltcard": "Charge BoltCard", + "nfc_mode_boltcard_subtitle": "Read card and charge invoice", "nfc_mode_hce": "Emulate HCE", "nfc_mode_hce_subtitle": "Phone as card", "nfc_tag_not_compatible": "Tag not compatible", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 083ccdf..2d58e55 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -376,8 +376,8 @@ "share_ready_message": "Listo para compartir", "lnurl_copied_message": "LNURL copiado al portapapeles", "nfc_mode_title": "Modo NFC", - "nfc_mode_boltcard": "Cargar BoltCard", - "nfc_mode_boltcard_subtitle": "Leer tarjeta y enviar factura", + "nfc_mode_boltcard": "Cobrar BoltCard", + "nfc_mode_boltcard_subtitle": "Leer tarjeta y cobrar factura", "nfc_mode_hce": "Emular HCE", "nfc_mode_hce_subtitle": "Teléfono como tarjeta", "nfc_tag_not_compatible": "Tag no compatible", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a53168c..0321390 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -376,8 +376,8 @@ "share_ready_message": "Prêt à partager", "lnurl_copied_message": "LNURL copié dans le presse-papiers", "nfc_mode_title": "Mode NFC", - "nfc_mode_boltcard": "Charger BoltCard", - "nfc_mode_boltcard_subtitle": "Lire la carte et envoyer la facture", + "nfc_mode_boltcard": "Débiter BoltCard", + "nfc_mode_boltcard_subtitle": "Lire la carte et débiter la facture", "nfc_mode_hce": "Émuler HCE", "nfc_mode_hce_subtitle": "Téléphone comme carte", "nfc_tag_not_compatible": "Tag non compatible", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index cd73102..7534458 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -376,8 +376,8 @@ "share_ready_message": "Pronto da condividere", "lnurl_copied_message": "LNURL copiato negli appunti", "nfc_mode_title": "Modalità NFC", - "nfc_mode_boltcard": "Carica BoltCard", - "nfc_mode_boltcard_subtitle": "Leggi carta e invia fattura", + "nfc_mode_boltcard": "Addebita BoltCard", + "nfc_mode_boltcard_subtitle": "Leggi carta e addebita fattura", "nfc_mode_hce": "Emula HCE", "nfc_mode_hce_subtitle": "Telefono come carta", "nfc_tag_not_compatible": "Tag non compatibile", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 4c6e711..4463fef 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -376,8 +376,8 @@ "share_ready_message": "Pronto para compartilhar", "lnurl_copied_message": "LNURL copiado para a área de transferência", "nfc_mode_title": "Modo NFC", - "nfc_mode_boltcard": "Carregar BoltCard", - "nfc_mode_boltcard_subtitle": "Ler cartão e enviar fatura", + "nfc_mode_boltcard": "Cobrar BoltCard", + "nfc_mode_boltcard_subtitle": "Ler cartão e cobrar fatura", "nfc_mode_hce": "Emular HCE", "nfc_mode_hce_subtitle": "Telefone como cartão", "nfc_tag_not_compatible": "Tag não compatível", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 5737c89..7861d29 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -376,8 +376,8 @@ "share_ready_message": "Готово к отправке", "lnurl_copied_message": "LNURL скопирован в буфер обмена", "nfc_mode_title": "Режим NFC", - "nfc_mode_boltcard": "Загрузить BoltCard", - "nfc_mode_boltcard_subtitle": "Считать карту и отправить счёт", + "nfc_mode_boltcard": "Списать с BoltCard", + "nfc_mode_boltcard_subtitle": "Считать карту и списать средства", "nfc_mode_hce": "Эмуляция HCE", "nfc_mode_hce_subtitle": "Телефон как карта", "nfc_tag_not_compatible": "Тег не совместим", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 9c5ce30..17e4856 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -1917,13 +1917,13 @@ abstract class AppLocalizations { /// No description provided for @nfc_mode_boltcard. /// /// In es, this message translates to: - /// **'Cargar BoltCard'** + /// **'Cobrar BoltCard'** String get nfc_mode_boltcard; /// No description provided for @nfc_mode_boltcard_subtitle. /// /// In es, this message translates to: - /// **'Leer tarjeta y enviar factura'** + /// **'Leer tarjeta y cobrar factura'** String get nfc_mode_boltcard_subtitle; /// No description provided for @nfc_mode_hce. diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index cea1355..f5652bb 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -1006,10 +1006,10 @@ class AppLocalizationsDe extends AppLocalizations { String get nfc_mode_title => 'NFC-Modus'; @override - String get nfc_mode_boltcard => 'BoltCard laden'; + String get nfc_mode_boltcard => 'BoltCard abbuchen'; @override - String get nfc_mode_boltcard_subtitle => 'Karte lesen und Rechnung senden'; + String get nfc_mode_boltcard_subtitle => 'Karte lesen und Rechnung einziehen'; @override String get nfc_mode_hce => 'HCE emulieren'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index d36cd9b..9143cd6 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -982,10 +982,10 @@ class AppLocalizationsEn extends AppLocalizations { String get nfc_mode_title => 'NFC Mode'; @override - String get nfc_mode_boltcard => 'Load BoltCard'; + String get nfc_mode_boltcard => 'Charge BoltCard'; @override - String get nfc_mode_boltcard_subtitle => 'Read card and send invoice'; + String get nfc_mode_boltcard_subtitle => 'Read card and charge invoice'; @override String get nfc_mode_hce => 'Emulate HCE'; diff --git a/lib/l10n/generated/app_localizations_es.dart b/lib/l10n/generated/app_localizations_es.dart index 6868e67..1342f6e 100644 --- a/lib/l10n/generated/app_localizations_es.dart +++ b/lib/l10n/generated/app_localizations_es.dart @@ -995,10 +995,10 @@ class AppLocalizationsEs extends AppLocalizations { String get nfc_mode_title => 'Modo NFC'; @override - String get nfc_mode_boltcard => 'Cargar BoltCard'; + String get nfc_mode_boltcard => 'Cobrar BoltCard'; @override - String get nfc_mode_boltcard_subtitle => 'Leer tarjeta y enviar factura'; + String get nfc_mode_boltcard_subtitle => 'Leer tarjeta y cobrar factura'; @override String get nfc_mode_hce => 'Emular HCE'; diff --git a/lib/l10n/generated/app_localizations_fr.dart b/lib/l10n/generated/app_localizations_fr.dart index 471d38b..a6c7a6c 100644 --- a/lib/l10n/generated/app_localizations_fr.dart +++ b/lib/l10n/generated/app_localizations_fr.dart @@ -1013,11 +1013,11 @@ class AppLocalizationsFr extends AppLocalizations { String get nfc_mode_title => 'Mode NFC'; @override - String get nfc_mode_boltcard => 'Charger BoltCard'; + String get nfc_mode_boltcard => 'Débiter BoltCard'; @override String get nfc_mode_boltcard_subtitle => - 'Lire la carte et envoyer la facture'; + 'Lire la carte et débiter la facture'; @override String get nfc_mode_hce => 'Émuler HCE'; diff --git a/lib/l10n/generated/app_localizations_it.dart b/lib/l10n/generated/app_localizations_it.dart index aeaf60c..3e19870 100644 --- a/lib/l10n/generated/app_localizations_it.dart +++ b/lib/l10n/generated/app_localizations_it.dart @@ -1009,10 +1009,10 @@ class AppLocalizationsIt extends AppLocalizations { String get nfc_mode_title => 'Modalità NFC'; @override - String get nfc_mode_boltcard => 'Carica BoltCard'; + String get nfc_mode_boltcard => 'Addebita BoltCard'; @override - String get nfc_mode_boltcard_subtitle => 'Leggi carta e invia fattura'; + String get nfc_mode_boltcard_subtitle => 'Leggi carta e addebita fattura'; @override String get nfc_mode_hce => 'Emula HCE'; diff --git a/lib/l10n/generated/app_localizations_pt.dart b/lib/l10n/generated/app_localizations_pt.dart index c9554ed..17ece61 100644 --- a/lib/l10n/generated/app_localizations_pt.dart +++ b/lib/l10n/generated/app_localizations_pt.dart @@ -995,10 +995,10 @@ class AppLocalizationsPt extends AppLocalizations { String get nfc_mode_title => 'Modo NFC'; @override - String get nfc_mode_boltcard => 'Carregar BoltCard'; + String get nfc_mode_boltcard => 'Cobrar BoltCard'; @override - String get nfc_mode_boltcard_subtitle => 'Ler cartão e enviar fatura'; + String get nfc_mode_boltcard_subtitle => 'Ler cartão e cobrar fatura'; @override String get nfc_mode_hce => 'Emular HCE'; diff --git a/lib/l10n/generated/app_localizations_ru.dart b/lib/l10n/generated/app_localizations_ru.dart index aefa197..1cdd765 100644 --- a/lib/l10n/generated/app_localizations_ru.dart +++ b/lib/l10n/generated/app_localizations_ru.dart @@ -986,10 +986,10 @@ class AppLocalizationsRu extends AppLocalizations { String get nfc_mode_title => 'Режим NFC'; @override - String get nfc_mode_boltcard => 'Загрузить BoltCard'; + String get nfc_mode_boltcard => 'Списать с BoltCard'; @override - String get nfc_mode_boltcard_subtitle => 'Считать карту и отправить счёт'; + String get nfc_mode_boltcard_subtitle => 'Считать карту и списать средства'; @override String get nfc_mode_hce => 'Эмуляция HCE'; diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index 3bd26cb..fba9258 100644 --- a/lib/screens/9receive_screen.dart +++ b/lib/screens/9receive_screen.dart @@ -1021,13 +1021,8 @@ class _ReceiveScreenState extends State { if (modo == null) return; - if (modo == ModoNfcRecibir.hceWallet) { - _openNfcChargeSheet(_generatedInvoice!.paymentRequest, - modo: modo); - } else { - _openNfcChargeSheet(_generatedInvoice!.paymentRequest, - modo: modo); - } + _openNfcChargeSheet(_generatedInvoice!.paymentRequest, + modo: modo); } void _openNfcChargeSheet(String contenido, {required ModoNfcRecibir modo}) { @@ -1633,13 +1628,11 @@ class _NfcChargeSheetState extends State<_NfcChargeSheet> with SingleTickerProvi NfcChargeStatus _status = NfcChargeStatus.scanning; String? _errorMessage; bool _autoCloseScheduled = false; - late final TabController _tabController; bool _serviceInitialized = false; @override void initState() { super.initState(); - _tabController = TabController(length: 2, vsync: this); } @override @@ -1662,15 +1655,14 @@ class _NfcChargeSheetState extends State<_NfcChargeSheet> with SingleTickerProvi _status = result.status; _errorMessage = result.message; }); - if (result.status == NfcChargeStatus.success && !_autoCloseScheduled) { - _autoCloseScheduled = true; - Future.delayed(const Duration(milliseconds: 1400), () { - if (mounted) { - Navigator.of(context).pop(); - if (widget.onFinish != null) widget.onFinish!(); - } - }); - } + if (result.status == NfcChargeStatus.success && !_autoCloseScheduled) { + _autoCloseScheduled = true; + Future.delayed(const Duration(milliseconds: 1400), () { + if (mounted) { + if (widget.onFinish != null) widget.onFinish!(); + } + }); + } }, ); } diff --git a/lib/services/nfc_charge_service.dart b/lib/services/nfc_charge_service.dart index 30e3310..992639a 100644 --- a/lib/services/nfc_charge_service.dart +++ b/lib/services/nfc_charge_service.dart @@ -40,6 +40,7 @@ class NfcChargeService { final Dio _dio = Dio(); final FlutterNfcHce _hce = FlutterNfcHce(); bool _sessionActive = false; + bool _processingTag = false; final AppLocalizations _l10n; NfcChargeService(this._l10n) { @@ -101,6 +102,8 @@ class NfcChargeService { ); _debugLog('HCE: Emulación NFC iniciada correctamente'); onStatus(const NfcChargeResult(NfcChargeStatus.reading)); + // HCE queda activo esperando que un lector se conecte + // El usuario debe detener la sesión manualmente break; case ModoNfcRecibir.lectorBoltcard: @@ -111,6 +114,8 @@ class NfcChargeService { invalidateAfterFirstRead: false, alertMessage: 'Acerca la tarjeta', onDiscovered: (NfcTag tag) async { + if (_processingTag) return; + _processingTag = true; try { onStatus(const NfcChargeResult(NfcChargeStatus.reading)); @@ -176,6 +181,7 @@ class NfcChargeService { )); await _safeStop(errorMessage: _l10n.nfc_network_error); } finally { + _processingTag = false; _sessionActive = false; } }, @@ -194,7 +200,9 @@ class NfcChargeService { Future stopSession() async { await _safeStop(); + try { await _hce.stopNfcHce(); } catch (_) {} _sessionActive = false; + _processingTag = false; } Future _safeStop({String? alertMessage, String? errorMessage}) async { @@ -289,6 +297,7 @@ class NfcChargeService { } void dispose() { + try { _hce.stopNfcHce(); } catch (_) {} _dio.close(force: true); } } From afbb1fa4d3c4617722b55c2988f14feb06240b06 Mon Sep 17 00:00:00 2001 From: "@Delagado74" Date: Mon, 4 May 2026 01:27:42 -0400 Subject: [PATCH 12/13] feat(hce): expose Lightning Address without amount, invoice without lightning: prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sin monto: Exone Lightning Address (user@domain) en lugar de LNURL - Con monto: Expone invoice directo (lnbc1...) sin prefijo lightning: - Agrega logs HCE_DEBUG y NFC_CHARGE para depuración - Phoenix y otras wallets pueden leer ambos formatos --- lib/screens/9receive_screen.dart | 32 ++++++++++++++++++---------- lib/services/nfc_charge_service.dart | 6 ++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index fba9258..16d69ca 100644 --- a/lib/screens/9receive_screen.dart +++ b/lib/screens/9receive_screen.dart @@ -982,13 +982,18 @@ class _ReceiveScreenState extends State { final l10n = AppLocalizations.of(context)!; - // Sin factura → HCE directo (BoltCard requiere factura) + // Sin factura → HCE directo exponiendo Lightning Address (BoltCard requiere factura) if (_generatedInvoice == null) { final lnAddressProvider = context.read(); + final lightningAddress = lnAddressProvider.defaultAddress?.fullAddress; final lnurl = lnAddressProvider.defaultAddress?.lnurl; - if (lnurl != null) { - _openNfcChargeSheet(lnurl, modo: ModoNfcRecibir.hceWallet); + debugPrint('[HCE_DEBUG] Sin monto - Lightning Address: $lightningAddress'); + debugPrint('[HCE_DEBUG] Sin monto - LNURL (no usado): $lnurl'); + if (lightningAddress != null) { + debugPrint('[HCE_DEBUG] Iniciando HCE con Lightning Address: $lightningAddress'); + _openNfcChargeSheet(lightningAddress, modo: ModoNfcRecibir.hceWallet); } else { + debugPrint('[HCE_DEBUG] No hay Lightning Address, solicitando monto'); _showRequestAmountModal(autoStartNfcAfterGenerate: true); } return; @@ -1428,15 +1433,20 @@ class _ReceiveScreenState extends State { modo: ModoNfcRecibir.lectorBoltcard); } } else { - // HCE → usar LNURL o invoice según corresponda - final lnAddressProvider = context.read(); - final lnurl = lnAddressProvider.defaultAddress?.lnurl; - - if (lnurl != null) { - _openNfcChargeSheet(lnurl, modo: ModoNfcRecibir.hceWallet); - } else if (_generatedInvoice != null) { - _openNfcChargeSheet(_generatedInvoice!.paymentRequest, + // HCE → usar invoice generada (con monto) o Lightning Address (sin monto) + if (_generatedInvoice != null) { + final invoice = _generatedInvoice!.paymentRequest; + debugPrint('[HCE_DEBUG] Con monto - Exponiendo INVOICE: ${invoice.substring(0, 30)}...'); + debugPrint('[HCE_DEBUG] Con monto - Invoice completa: $invoice'); + _openNfcChargeSheet(invoice, modo: ModoNfcRecibir.hceWallet); + } else { + final lnAddressProvider = context.read(); + final lightningAddress = lnAddressProvider.defaultAddress?.fullAddress; + debugPrint('[HCE_DEBUG] Con monto pero sin invoice - Exponiendo Lightning Address: $lightningAddress'); + if (lightningAddress != null) { + _openNfcChargeSheet(lightningAddress, modo: ModoNfcRecibir.hceWallet); + } } } } diff --git a/lib/services/nfc_charge_service.dart b/lib/services/nfc_charge_service.dart index 992639a..073cdc4 100644 --- a/lib/services/nfc_charge_service.dart +++ b/lib/services/nfc_charge_service.dart @@ -95,6 +95,12 @@ class NfcChargeService { case ModoNfcRecibir.hceWallet: // HCE Mode: Emitir para wallet (Phoenix) _debugLog('HCE: Emitiendo para wallet: $lnurlOrInvoice'); + final isInvoice = lnurlOrInvoice.startsWith('lightning:lnbc') || lnurlOrInvoice.startsWith('lnbc'); + _debugLog('HCE: Tipo de contenido: ${isInvoice ? 'INVOICE' : (lnurlOrInvoice.contains('@') ? 'LIGHTNING_ADDRESS' : 'LNURL')}'); + _debugLog('HCE: Longitud del contenido: ${lnurlOrInvoice.length}'); + if (isInvoice) { + _debugLog('HCE: INVOICE detectada - primeros 30 chars: ${lnurlOrInvoice.substring(0, lnurlOrInvoice.length > 30 ? 30 : lnurlOrInvoice.length)}'); + } await _hce.startNfcHce( lnurlOrInvoice, mimeType: 'text/plain', From 249abf1b2f61d93599f57ab132497d682941e5b5 Mon Sep 17 00:00:00 2001 From: "@Delagado74" Date: Tue, 5 May 2026 01:00:44 -0400 Subject: [PATCH 13/13] fix(hce): use LNURL bech32 for HCE without amount, stop NfcManager before HCE start - Use lnurl (bech32) instead of lightning address for HCE without amount - Stop NfcManager and previous HCE before starting HCE to avoid conflicts - Change persistMessage to true for NDEF persistence - Add debug logs for HCE wallet mode - Fix double prefix in debug logs --- lib/screens/9receive_screen.dart | 18 +++++++------- lib/services/nfc_charge_service.dart | 36 +++++++++++++++++++++------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/screens/9receive_screen.dart b/lib/screens/9receive_screen.dart index 16d69ca..a6cc3a5 100644 --- a/lib/screens/9receive_screen.dart +++ b/lib/screens/9receive_screen.dart @@ -982,18 +982,16 @@ class _ReceiveScreenState extends State { final l10n = AppLocalizations.of(context)!; - // Sin factura → HCE directo exponiendo Lightning Address (BoltCard requiere factura) + // Sin factura → HCE directo exponiendo LNURL bech32 (funcionaba antes) if (_generatedInvoice == null) { final lnAddressProvider = context.read(); - final lightningAddress = lnAddressProvider.defaultAddress?.fullAddress; - final lnurl = lnAddressProvider.defaultAddress?.lnurl; - debugPrint('[HCE_DEBUG] Sin monto - Lightning Address: $lightningAddress'); - debugPrint('[HCE_DEBUG] Sin monto - LNURL (no usado): $lnurl'); - if (lightningAddress != null) { - debugPrint('[HCE_DEBUG] Iniciando HCE con Lightning Address: $lightningAddress'); - _openNfcChargeSheet(lightningAddress, modo: ModoNfcRecibir.hceWallet); + final lnurl = lnAddressProvider.defaultAddress?.lnurl; // LNURL1... bech32 + debugPrint('[HCE_DEBUG] Sin monto - LNURL bech32: $lnurl'); + if (lnurl != null && lnurl.isNotEmpty) { + debugPrint('[HCE_DEBUG] Iniciando HCE con LNURL: $lnurl'); + _openNfcChargeSheet(lnurl, modo: ModoNfcRecibir.hceWallet); } else { - debugPrint('[HCE_DEBUG] No hay Lightning Address, solicitando monto'); + debugPrint('[HCE_DEBUG] No hay LNURL, solicitando monto'); _showRequestAmountModal(autoStartNfcAfterGenerate: true); } return; @@ -1651,6 +1649,8 @@ class _NfcChargeSheetState extends State<_NfcChargeSheet> with SingleTickerProvi if (!_serviceInitialized) { _service = NfcChargeService(AppLocalizations.of(context)!); _serviceInitialized = true; + debugPrint('[HCE_SHEET] Modo: ${widget.modo}'); + debugPrint('[HCE_SHEET] Contenido: ${widget.invoice}'); _start(); } } diff --git a/lib/services/nfc_charge_service.dart b/lib/services/nfc_charge_service.dart index 073cdc4..9a884e5 100644 --- a/lib/services/nfc_charge_service.dart +++ b/lib/services/nfc_charge_service.dart @@ -11,7 +11,7 @@ import '../l10n/generated/app_localizations.dart'; void _debugLog(String message) { if (kDebugMode) { - print('[NFC_CHARGE] $message'); + print('[HCE_WALLET] $message'); } } @@ -94,19 +94,39 @@ class NfcChargeService { switch (modo) { case ModoNfcRecibir.hceWallet: // HCE Mode: Emitir para wallet (Phoenix) - _debugLog('HCE: Emitiendo para wallet: $lnurlOrInvoice'); + _debugLog('Emitiendo para wallet: $lnurlOrInvoice'); final isInvoice = lnurlOrInvoice.startsWith('lightning:lnbc') || lnurlOrInvoice.startsWith('lnbc'); - _debugLog('HCE: Tipo de contenido: ${isInvoice ? 'INVOICE' : (lnurlOrInvoice.contains('@') ? 'LIGHTNING_ADDRESS' : 'LNURL')}'); - _debugLog('HCE: Longitud del contenido: ${lnurlOrInvoice.length}'); + _debugLog('Tipo de contenido: ${isInvoice ? 'INVOICE' : 'LNURL'}'); + _debugLog('Longitud del contenido: ${lnurlOrInvoice.length}'); if (isInvoice) { - _debugLog('HCE: INVOICE detectada - primeros 30 chars: ${lnurlOrInvoice.substring(0, lnurlOrInvoice.length > 30 ? 30 : lnurlOrInvoice.length)}'); + _debugLog('INVOICE detectada - primeros 30 chars: ${lnurlOrInvoice.substring(0, lnurlOrInvoice.length > 30 ? 30 : lnurlOrInvoice.length)}'); } - await _hce.startNfcHce( + + // CRÍTICO: Detener NfcManager y HCE previo antes de iniciar HCE + try { + await NfcManager.instance.stopSession(); + _debugLog('NfcManager detenido correctamente'); + } catch (_) {} + try { + await _hce.stopNfcHce(); + _debugLog('HCE previo detenido'); + } catch (_) {} + await Future.delayed(const Duration(milliseconds: 500)); + + // Verificar contenido no vacío + if (lnurlOrInvoice.isEmpty) { + _debugLog('ERROR: Contenido vacío'); + onStatus(const NfcChargeResult(NfcChargeStatus.invalidTag)); + break; + } + + final result = await _hce.startNfcHce( lnurlOrInvoice, mimeType: 'text/plain', - persistMessage: false, + persistMessage: true, // true para persistir el mensaje NDEF ); - _debugLog('HCE: Emulación NFC iniciada correctamente'); + _debugLog('Resultado startNfcHce: $result'); + _debugLog('Emulación NFC iniciada correctamente'); onStatus(const NfcChargeResult(NfcChargeStatus.reading)); // HCE queda activo esperando que un lector se conecte // El usuario debe detener la sesión manualmente