fix: address CodeRabbit review — await NFC sessions, log errors, validate tokens

This commit is contained in:
Forte11Cuba
2026-03-13 12:30:46 -06:00
parent 560d6fc683
commit 713f13a46d
2 changed files with 101 additions and 87 deletions
@@ -3,6 +3,7 @@ package me.elcaju
import android.content.ComponentName import android.content.ComponentName
import android.nfc.NfcAdapter import android.nfc.NfcAdapter
import android.nfc.cardemulation.CardEmulation import android.nfc.cardemulation.CardEmulation
import android.util.Log
import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
@@ -30,7 +31,9 @@ class MainActivity : FlutterActivity() {
ComponentName(this, NfcHceService::class.java) ComponentName(this, NfcHceService::class.java)
) )
} }
} catch (_: Exception) {} } catch (e: Exception) {
Log.w("MainActivity", "setPreferredService failed (non-critical)", e)
}
result.success(true) result.success(true)
} }
@@ -44,7 +47,9 @@ class MainActivity : FlutterActivity() {
val cardEmulation = CardEmulation.getInstance(adapter) val cardEmulation = CardEmulation.getInstance(adapter)
cardEmulation.unsetPreferredService(this) cardEmulation.unsetPreferredService(this)
} }
} catch (_: Exception) {} } catch (e: Exception) {
Log.w("MainActivity", "unsetPreferredService failed (non-critical)", e)
}
result.success(true) result.success(true)
} }
+94 -85
View File
@@ -99,55 +99,59 @@ class NfcService {
required void Function() onSuccess, required void Function() onSuccess,
required void Function(String error) onError, required void Function(String error) onError,
}) async { }) async {
NfcManager.instance.startSession( try {
pollingOptions: {NfcPollingOption.iso14443}, await NfcManager.instance.startSession(
onDiscovered: (NfcTag tag) async { pollingOptions: {NfcPollingOption.iso14443},
final ndef = NdefAndroid.from(tag); onDiscovered: (NfcTag tag) async {
if (ndef == null || !ndef.isWritable) { final ndef = NdefAndroid.from(tag);
onError('Tag is not writable'); if (ndef == null || !ndef.isWritable) {
NfcManager.instance.stopSession(); onError('Tag is not writable');
return; await NfcManager.instance.stopSession();
} return;
}
// Build NDEF Text Record: [status byte][language code][text] // Build NDEF Text Record: [status byte][language code][text]
final textBytes = Uint8List.fromList(token.codeUnits); final textBytes = Uint8List.fromList(token.codeUnits);
final languageCode = Uint8List.fromList('en'.codeUnits); final languageCode = Uint8List.fromList('en'.codeUnits);
final payload = Uint8List(1 + languageCode.length + textBytes.length); final payload = Uint8List(1 + languageCode.length + textBytes.length);
payload[0] = languageCode.length; // status byte (UTF-8, no length) payload[0] = languageCode.length; // status byte (UTF-8, no length)
payload.setRange(1, 1 + languageCode.length, languageCode); payload.setRange(1, 1 + languageCode.length, languageCode);
payload.setRange(1 + languageCode.length, payload.length, textBytes); payload.setRange(1 + languageCode.length, payload.length, textBytes);
// Check size // Check size
if (payload.length + 7 > ndef.maxSize) { if (payload.length + 7 > ndef.maxSize) {
onError('Token too large for this NFC tag ' onError('Token too large for this NFC tag '
'(${payload.length + 7}B > ${ndef.maxSize}B)'); '(${payload.length + 7}B > ${ndef.maxSize}B)');
NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
return; return;
} }
try { try {
final message = NdefMessage(records: [ final message = NdefMessage(records: [
NdefRecord( NdefRecord(
typeNameFormat: TypeNameFormat.wellKnown, typeNameFormat: TypeNameFormat.wellKnown,
type: Uint8List.fromList([0x54]), // 'T' = Text Record type: Uint8List.fromList([0x54]), // 'T' = Text Record
identifier: Uint8List(0), identifier: Uint8List(0),
payload: payload, payload: payload,
), ),
]); ]);
await ndef.writeNdefMessage(message); await ndef.writeNdefMessage(message);
onSuccess(); onSuccess();
NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
} catch (e) { } catch (e) {
onError(e.toString()); onError(e.toString());
NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
} }
}, },
); );
} catch (e) {
onError(e.toString());
}
} }
/// Stop any active NFC session. /// Stop any active NFC session.
static void stopWrite() { static Future<void> stopWrite() async {
NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
} }
/// Start reading NFC tags for Cashu tokens. /// Start reading NFC tags for Cashu tokens.
@@ -157,53 +161,57 @@ class NfcService {
required void Function(String token) onTokenRead, required void Function(String token) onTokenRead,
required void Function(String error) onError, required void Function(String error) onError,
}) async { }) async {
NfcManager.instance.startSession( try {
pollingOptions: {NfcPollingOption.iso14443}, await NfcManager.instance.startSession(
onDiscovered: (NfcTag tag) async { pollingOptions: {NfcPollingOption.iso14443},
try { onDiscovered: (NfcTag tag) async {
final diagnostics = <String>[]; try {
final diagnostics = <String>[];
// 1. Try IsoDep first (HCE phone-to-phone) // 1. Try IsoDep first (HCE phone-to-phone)
final isoDep = IsoDepAndroid.from(tag); final isoDep = IsoDepAndroid.from(tag);
if (isoDep != null) { if (isoDep != null) {
final (token, isoInfo) = await _readViaIsoDep(isoDep); final (token, isoInfo) = await _readViaIsoDep(isoDep);
if (token != null) {
onTokenRead(token);
NfcManager.instance.stopSession();
return;
}
diagnostics.add('IsoDep: $isoInfo');
} else {
diagnostics.add('IsoDep: not available');
}
// 2. Fallback: NDEF (physical tags)
final ndef = NdefAndroid.from(tag);
if (ndef != null) {
final message = ndef.cachedNdefMessage ?? await ndef.getNdefMessage();
if (message != null) {
final token = _extractToken(message);
if (token != null) { if (token != null) {
onTokenRead(token); onTokenRead(token);
NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
return; return;
} }
diagnostics.add('NDEF: ${message.records.length} records, no Cashu token'); diagnostics.add('IsoDep: $isoInfo');
} else { } else {
diagnostics.add('NDEF: no message'); diagnostics.add('IsoDep: not available');
} }
} else {
diagnostics.add('NDEF: not available');
}
onError('No Cashu token found [${diagnostics.join('; ')}]'); // 2. Fallback: NDEF (physical tags)
NfcManager.instance.stopSession(); final ndef = NdefAndroid.from(tag);
} catch (e) { if (ndef != null) {
onError(e.toString()); final message = ndef.cachedNdefMessage ?? await ndef.getNdefMessage();
NfcManager.instance.stopSession(); if (message != null) {
} final token = _extractToken(message);
}, if (token != null) {
); onTokenRead(token);
await NfcManager.instance.stopSession();
return;
}
diagnostics.add('NDEF: ${message.records.length} records, no Cashu token');
} else {
diagnostics.add('NDEF: no message');
}
} else {
diagnostics.add('NDEF: not available');
}
onError('No Cashu token found [${diagnostics.join('; ')}]');
await NfcManager.instance.stopSession();
} catch (e) {
onError(e.toString());
await NfcManager.instance.stopSession();
}
},
);
} catch (e) {
onError(e.toString());
}
} }
/// Read NDEF from HCE via IsoDep APDU commands (like Numo). /// Read NDEF from HCE via IsoDep APDU commands (like Numo).
@@ -387,8 +395,8 @@ class NfcService {
} }
/// Stop any active NFC read session. /// Stop any active NFC read session.
static void stopRead() { static Future<void> stopRead() async {
NfcManager.instance.stopSession(); await NfcManager.instance.stopSession();
} }
/// Extract a Cashu token from an NDEF message. /// Extract a Cashu token from an NDEF message.
@@ -449,7 +457,8 @@ class NfcService {
final parsed = Uri.parse(uri); final parsed = Uri.parse(uri);
final fragment = parsed.fragment; final fragment = parsed.fragment;
if (fragment.startsWith('token=')) { if (fragment.startsWith('token=')) {
return fragment.substring(6); final token = fragment.substring(6);
if (_isCashuToken(token)) return token;
} }
final tokenParam = parsed.queryParameters['token']; final tokenParam = parsed.queryParameters['token'];
if (tokenParam != null && _isCashuToken(tokenParam)) { if (tokenParam != null && _isCashuToken(tokenParam)) {