# Title: feat(hce): Add HCE support for receiving payments via NFC#97
# Title: feat(hce): Add HCE support for receiving payments via NFC#97Delgado74 wants to merge 14 commits into
Conversation
… intent filters - Add mode selector dialog when invoice exists (BoltCard vs HCE) - Skip dialog when no invoice (HCE direct, BoltCard needs invoice) - Stop HCE before reader mode to prevent system interception - Add NFC intent-filters to Manifest for foreground dispatch - Create nfc_tech_filter.xml for TECH_DISCOVERED - Use invalidateAfterFirstRead:false for BoltCard mode - Fix lnurlOrInvoice parameter name in charge service
- 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
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds selectable NFC receive modes (HCE emission vs BoltCard reader). Registers an Android HCE service and NFC tech filters in the manifest and resources, adds the flutter_nfc_hce dependency, extends the NFC charge service and ReceiveScreen to handle a modo selector, and adds localized strings across languages. ChangesNFC Mode Support & Android HCE Integration
Sequence DiagramsequenceDiagram
participant User as User
participant UI as ReceiveScreen
participant Service as NfcChargeService
participant HceLib as FlutterNfcHce
participant NfcSystem as Android NFC / Reader
participant Server as LnurlServer
User->>UI: Tap Receive / auto-start
UI->>Service: startChargeSession(lnurlOrInvoice, modo)
alt modo == hceWallet
Service->>HceLib: startNfcHce(lnurlOrInvoice)
HceLib->>NfcSystem: Advertise HCE (IsoDep/AID)
NfcSystem->>Server: External reader interacts with emulated card
Server-->>NfcSystem: Confirm/handle payment
NfcSystem-->>Service: notify interaction/result
Service-->>UI: notify success/error
else modo == lectorBoltcard
Service->>NfcSystem: start listening (poll iso14443)
NfcSystem->>Service: Tag discovered
Service->>Server: POST claim using extracted tag and lnurlOrInvoice
Server-->>Service: claim result
Service-->>UI: notify success/error
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/services/nfc_charge_service.dart (1)
193-207:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
stopSession()does not stop HCE emulation.When HCE mode is active and
stopSession()is called (e.g., when dismissing the sheet), it only stopsNfcManagersessions but does not call_hce.stopNfcHce(). This leaves HCE emitting the LNURL/invoice even after the UI is closed.🐛 Proposed fix
Future<void> stopSession() async { + try { await _hce.stopNfcHce(); } catch (_) {} await _safeStop(); _sessionActive = false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/services/nfc_charge_service.dart` around lines 193 - 207, stopSession() currently only stops the NfcManager session and doesn't stop HCE emulation; update stopSession() to also stop HCE by invoking _hce.stopNfcHce() when HCE mode is active (wrap the call in a try/catch similar to _safeStop to avoid throwing), ensuring _hce.stopNfcHce() is called before or alongside await _safeStop(...) and that _sessionActive is still set to false after both stop attempts; reference stopSession, _safeStop, and _hce.stopNfcHce in your changes.
🧹 Nitpick comments (2)
lib/services/nfc_charge_service.dart (1)
91-102: ⚡ Quick winHCE mode lacks session completion handling.
After
startNfcHceis called, the method returns but_sessionActiveremainstrueindefinitely. Unlike BoltCard mode which sets_sessionActive = falsein the finally block, HCE mode has no mechanism to mark the session as complete or detect payment success.Consider adding a way to stop HCE and reset
_sessionActivewhen the charge sheet is dismissed, or rely onstopSession()being called externally.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/services/nfc_charge_service.dart` around lines 91 - 102, The HCE branch never clears the session flag or stops HCE, so _sessionActive stays true after _hce.startNfcHce completes; update the ModoNfcRecibir.hceWallet case to ensure the session is ended: after calling _hce.startNfcHce (or in a finally/when-dismissed handler) call the existing stopSession() or _hce.stop... method and set _sessionActive = false, and emit the final status via onStatus(NfcChargeResult(...)) as done in the BoltCard flow so the charge sheet dismissal or external stopSession() reliably resets the session state.lib/screens/9receive_screen.dart (1)
1024-1030: 💤 Low valueSimplify duplicated code.
Both branches perform the same operation. The conditional is unnecessary.
♻️ Suggested simplification
if (modo == null) return; - if (modo == ModoNfcRecibir.hceWallet) { - _openNfcChargeSheet(_generatedInvoice!.paymentRequest, - modo: modo); - } else { - _openNfcChargeSheet(_generatedInvoice!.paymentRequest, - modo: modo); - } + _openNfcChargeSheet(_generatedInvoice!.paymentRequest, modo: modo);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/screens/9receive_screen.dart` around lines 1024 - 1030, The if/else around ModoNfcRecibir.hceWallet is redundant because both branches call _openNfcChargeSheet with the same arguments; remove the conditional and replace it with a single call to _openNfcChargeSheet(_generatedInvoice!.paymentRequest, modo: modo) (referencing the symbols ModoNfcRecibir, _openNfcChargeSheet, and _generatedInvoice.paymentRequest) so the logic is simplified and identical behavior preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@android/app/src/main/res/xml/apduservice.xml`:
- Line 3: The APDU service currently allows handling while the device is locked
because android:requireDeviceUnlock="false"; update the APDU service declaration
in apduservice.xml (the <service> or <host-apdu-service> element that contains
android:requireDeviceUnlock) to set android:requireDeviceUnlock="true" so the
HCE service requires the device to be unlocked before responding to ISO-DEP
readers, preventing locked-device leakage of LNURL/BOLT11 data.
- Line 3: The apduservice XML currently sets
android:requireDeviceUnlock="false", which allows HCE responses while the device
is locked; update the apduservice configuration to set
android:requireDeviceUnlock="true" so the HostApduService only responds when the
device is unlocked (locate the HostApduService / <service> / <host-apdu-service>
declaration in apduservice.xml and change the android:requireDeviceUnlock
attribute value accordingly).
- Line 5: The AID in apduservice.xml is wrong: update the <aid-filter
android:name="..."> value to match the flutter_nfc_hce native default AID
("D2760000850101") or, if you intentionally want "F0010203040506", configure the
flutter_nfc_hce native implementation to use that AID instead; locate the
<aid-filter> entry in apduservice.xml and replace its android:name with
"D2760000850101" (or change the plugin's native AID configuration in the
flutter_nfc_hce source if AID customization is supported), then verify the AID
matches the SELECT command your BoltCard terminal sends.
- Line 5: Update the registered AID in apduservice.xml so it matches the
hardcoded AID used by the flutter_nfc_hce plugin (D2760000850101); specifically
replace the current aid-filter value F0010203040506 with D2760000850101 so
Android will route SELECT commands to KHostApduService correctly and match the
AID expected by the plugin at runtime.
In `@lib/l10n/app_en.arb`:
- Line 394: The "nfc_mode_boltcard" message value is semantically wrong — it
implies adding funds rather than collecting a Lightning payment; update the
value for the key "nfc_mode_boltcard" in lib/l10n/app_en.arb to a clearer phrase
such as "Charge BoltCard" (or "Collect from BoltCard") and apply equivalent
corrected translations across all other locale files where the same key appears
(e.g., replace "Charger BoltCard", "Carica BoltCard", "BoltCard laden",
"Carregar BoltCard" with the appropriate localized phrasing). Ensure only the
string values change, keeping the key "nfc_mode_boltcard" intact so code
references remain unchanged.
In `@lib/screens/9receive_screen.dart`:
- Around line 1636-1642: The _tabController field is declared and initialized in
initState but never used and not disposed, leaking a ticker because the State
mixes in SingleTickerProviderStateMixin; either remove the unused TabController
entirely (delete the late final _tabController declaration and its
initialization in initState) and also remove the SingleTickerProviderStateMixin
from the State class if it's no longer needed, or keep it and add a dispose
method that calls _tabController.dispose() (implement dispose() =>
_tabController.dispose(); super.dispose()) to properly clean up; locate symbols
_tabController, initState, dispose, and SingleTickerProviderStateMixin to apply
the change.
- Around line 1666-1671: Auto-close block currently calls
Navigator.of(context).pop() and then invokes widget.onFinish, but
widget.onFinish also pops the modal, causing a double-pop; remove the
Navigator.of(context).pop() invocation from the Future.delayed auto-close path
(leave setting _autoCloseScheduled and invoking widget.onFinish), and update the
onFinish implementation (the callback invoked by widget.onFinish) to always
perform the pop (e.g., ensure it calls Navigator.pop(sheetContext)
unconditionally) so the modal is only dismissed once by the onFinish handler.
In `@lib/services/nfc_charge_service.dart`:
- Around line 38-45: The class NfcChargeService references _hce.startNfcHce(...)
and _hce.stopNfcHce() but never declares or imports the NFC HCE API; add the
missing import for the flutter_nfc_hce package
(package:flutter_nfc_hce/flutter_nfc_hce.dart) and declare a private field
inside NfcChargeService (e.g., final FlutterNfcHce _hce) and initialize it
(either inline or in the constructor) so the calls to _hce.startNfcHce and
_hce.stopNfcHce compile; update the constructor NfcChargeService(...) if you
choose constructor initialization.
---
Outside diff comments:
In `@lib/services/nfc_charge_service.dart`:
- Around line 193-207: stopSession() currently only stops the NfcManager session
and doesn't stop HCE emulation; update stopSession() to also stop HCE by
invoking _hce.stopNfcHce() when HCE mode is active (wrap the call in a try/catch
similar to _safeStop to avoid throwing), ensuring _hce.stopNfcHce() is called
before or alongside await _safeStop(...) and that _sessionActive is still set to
false after both stop attempts; reference stopSession, _safeStop, and
_hce.stopNfcHce in your changes.
---
Nitpick comments:
In `@lib/screens/9receive_screen.dart`:
- Around line 1024-1030: The if/else around ModoNfcRecibir.hceWallet is
redundant because both branches call _openNfcChargeSheet with the same
arguments; remove the conditional and replace it with a single call to
_openNfcChargeSheet(_generatedInvoice!.paymentRequest, modo: modo) (referencing
the symbols ModoNfcRecibir, _openNfcChargeSheet, and
_generatedInvoice.paymentRequest) so the logic is simplified and identical
behavior preserved.
In `@lib/services/nfc_charge_service.dart`:
- Around line 91-102: The HCE branch never clears the session flag or stops HCE,
so _sessionActive stays true after _hce.startNfcHce completes; update the
ModoNfcRecibir.hceWallet case to ensure the session is ended: after calling
_hce.startNfcHce (or in a finally/when-dismissed handler) call the existing
stopSession() or _hce.stop... method and set _sessionActive = false, and emit
the final status via onStatus(NfcChargeResult(...)) as done in the BoltCard flow
so the charge sheet dismissal or external stopSession() reliably resets the
session state.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 92ea70ee-4c8c-4bcf-9297-f5797d4ac7e1
⛔ Files ignored due to path filters (9)
lib/l10n/generated/app_localizations.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_de.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_en.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_es.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_fr.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_it.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_pt.dartis excluded by!**/generated/**lib/l10n/generated/app_localizations_ru.dartis excluded by!**/generated/**pubspec.lockis excluded by!**/*.lock
📒 Files selected for processing (13)
android/app/src/main/AndroidManifest.xmlandroid/app/src/main/res/values/strings.xmlandroid/app/src/main/res/xml/apduservice.xmlandroid/app/src/main/res/xml/nfc_tech_filter.xmllib/l10n/app_de.arblib/l10n/app_en.arblib/l10n/app_es.arblib/l10n/app_fr.arblib/l10n/app_it.arblib/l10n/app_pt.arblib/l10n/app_ru.arblib/screens/9receive_screen.dartlib/services/nfc_charge_service.dart
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@lib/services/nfc_charge_service.dart`:
- Around line 97-104: The HCE emulation started by _hce.startNfcHce (called in
the NFC flow) is never stopped; update stopSession() and dispose() to also stop
HCE: when stopping the session (stopSession) call the corresponding HCE stop
method on _hce (e.g., _hce.stopNfcHce or _hce.stop) with a null-check and await
it if async, and likewise call/await that same stop in dispose() before cleaning
up; wrap the call in try/catch to swallow/log errors and ensure idempotence so
multiple stops are safe.
- Around line 109-113: The onDiscovered callback can re-enter while a previous
tag is being processed; add a private boolean field (e.g., _processingTag) to
guard the callback: at the start of the onDiscovered handler check if
(_processingTag) return immediately, otherwise set _processingTag = true, run
the existing tag handling and withdrawal request code (the block that performs
the claim/withdraw request), and ensure you reset _processingTag = false in a
finally block so it always clears even on errors; keep the existing
_sessionActive logic for session lifecycle but use _processingTag to prevent
concurrent execution of onDiscovered.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0c955ee9-192f-4419-8c51-1dd0323690ae
📒 Files selected for processing (2)
lib/services/nfc_charge_service.dartpubspec.yaml
…nup and session handling
…ightning: prefix - 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
…fore 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
# Conflicts: # lib/l10n/app_de.arb # lib/l10n/app_en.arb # lib/l10n/app_es.arb # lib/l10n/app_fr.arb # lib/l10n/app_it.arb # lib/l10n/app_pt.arb # lib/l10n/app_ru.arb # lib/l10n/generated/app_localizations.dart # lib/l10n/generated/app_localizations_de.dart # lib/l10n/generated/app_localizations_en.dart # lib/l10n/generated/app_localizations_es.dart # lib/l10n/generated/app_localizations_fr.dart # lib/l10n/generated/app_localizations_it.dart # lib/l10n/generated/app_localizations_pt.dart # lib/l10n/generated/app_localizations_ru.dart
Description
This PR adds HCE (Host Card Emulation) support to LaChispa, enabling users to receive Lightning payments by emulating an NFC card. BoltCard reader mode is already working in production; this PR focuses on HCE.
Goal
Allow LaChispa users to receive payments by tapping their phone against another device running a Lightning wallet with HCE support.
Modes Available (After This PR)
HCE Mode (New) - Emulate a contactless card
BoltCard Reader Mode - Read physical BoltCards (Already in production)
Technical Implementation
Core Service:
lib/services/nfc_charge_service.dartstartChargeSession()method{ hceWallet, lectorBoltcard }FlutterNfcHceto emulate NFC cardKHostApduServiceconfigured in AndroidManifest.xmlapduservice.xmlNfcManagerto read physical cardstag: 'withdrawRequest')callbackwithk1parameterUI Integration:
lib/screens/9receive_screen.dartSingleTickerProviderStateMixinfor animationsAppLocalizationsaccessed indidChangeDependencies()(fixes InheritedWidget error)Android Configuration (HCE Support)
android.permission.NFCcom.novice.flutter_nfc_hce.KHostApduServiceNDEF_DISCOVERED,TECH_DISCOVEREDnfc_tech_filter.xmlfor TECH_DISCOVEREDF0010203040506)Internationalization
nfc_mode_title,nfc_mode_boltcard,nfc_mode_hce, error messagesBug Fixes
InheritedWidgeterror by movingAppLocalizationstodidChangeDependencies()_tabControllernot defined in_NfcChargeSheetStateSingleTickerProviderStateMixinfor proper TickerProviderinvalidateAfterFirstRead: falsefor BoltCard modeDependencies Added
flutter_nfc_hce: ^0.1.8- HCE supportnfc_manager: ^3.5.0- NFC reader supportTesting Checklist
Please test and check the following:
HCE Mode without amount (uses LNURL):
HCE Mode with fixed amount (BOLT11):
BoltCard Reader Mode (Already in production - verify still works):
UI/UX:
Internationalization:
Related Issues
Pre-merge Checklist
flutter build apk --debug)lachispame/main)Note: BoltCard reader mode is already working in production. This PR adds HCE support to complement it. The commit history is clean with 9 commits implementing the full feature.
Summary by CodeRabbit
New Features
Platform
Internationalization