fix: address CodeRabbit review — await NFC sessions, log errors, validate tokens
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user