diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 298c712..d169f2a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,6 +5,8 @@
+
+
{
bool _scanned = false;
bool _isPasting = false;
+ bool _isReadingNfc = false;
+ bool _nfcAvailable = false;
bool _permissionDenied = false;
_QrLoopSession? _currentSession;
@@ -61,6 +64,7 @@ class _ScanQRPageState extends State {
void initState() {
super.initState();
_requestCameraPermission();
+ _checkNfcAvailability();
}
Future _requestCameraPermission() async {
@@ -87,9 +91,20 @@ class _ScanQRPageState extends State {
_qrController?.resumeCamera();
}
+ Future _checkNfcAvailability() async {
+ if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) return;
+ final available = await NfcManager.instance.isAvailable();
+ if (mounted) {
+ setState(() => _nfcAvailable = available);
+ }
+ }
+
@override
void dispose() {
_qrController?.dispose();
+ if (_isReadingNfc) {
+ NfcManager.instance.stopSession();
+ }
super.dispose();
}
@@ -613,6 +628,85 @@ class _ScanQRPageState extends State {
setState(() => _isPasting = false);
}
+ Future _readNfcTag() async {
+ setState(() => _isReadingNfc = true);
+
+ try {
+ await NfcManager.instance.startSession(
+ onDiscovered: (NfcTag tag) async {
+ try {
+ final ndef = Ndef.from(tag);
+ if (ndef == null || ndef.cachedMessage == null) {
+ ToastService().show(
+ message: context.l10n.nfcNoTextRecord,
+ duration: const Duration(seconds: 5),
+ onTap: () {},
+ icon: Icon(Icons.warning),
+ );
+ await NfcManager.instance.stopSession();
+ if (mounted) setState(() => _isReadingNfc = false);
+ return;
+ }
+
+ String? payload;
+ for (final record in ndef.cachedMessage!.records) {
+ if (record.typeNameFormat == NdefTypeNameFormat.nfcWellknown &&
+ String.fromCharCodes(record.type) == 'T') {
+ final languageCodeLength = record.payload.first & 0x3F;
+ payload = String.fromCharCodes(
+ record.payload.sublist(1 + languageCodeLength),
+ );
+ break;
+ }
+ if (record.typeNameFormat == NdefTypeNameFormat.nfcWellknown &&
+ String.fromCharCodes(record.type) == 'U') {
+ payload = String.fromCharCodes(record.payload.sublist(1));
+ break;
+ }
+ }
+
+ await NfcManager.instance.stopSession();
+
+ if (payload == null || payload.isEmpty) {
+ ToastService().show(
+ message: context.l10n.nfcNoTextRecord,
+ duration: const Duration(seconds: 5),
+ onTap: () {},
+ icon: Icon(Icons.warning),
+ );
+ if (mounted) setState(() => _isReadingNfc = false);
+ return;
+ }
+
+ final parsed = await _handleText(payload);
+ if (!parsed) {
+ AppLogger.instance.warn("NFC payload cannot be parsed: $payload");
+ ToastService().show(
+ message: context.l10n.cannotBeParsed,
+ duration: const Duration(seconds: 5),
+ onTap: () {},
+ icon: Icon(Icons.error),
+ );
+ }
+ } catch (e) {
+ AppLogger.instance.warn("Error reading NFC tag: $e");
+ await NfcManager.instance.stopSession(errorMessage: e.toString());
+ }
+ if (mounted) setState(() => _isReadingNfc = false);
+ },
+ );
+ } catch (e) {
+ AppLogger.instance.warn("Failed to start NFC session: $e");
+ ToastService().show(
+ message: context.l10n.nfcNotAvailable,
+ duration: const Duration(seconds: 5),
+ onTap: () {},
+ icon: Icon(Icons.error),
+ );
+ if (mounted) setState(() => _isReadingNfc = false);
+ }
+ }
+
double? get _progress {
final session = _currentSession;
if (session == null || session.totalFrames <= 1) return null;
@@ -760,24 +854,55 @@ class _ScanQRPageState extends State {
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.all(24.0),
- child: ElevatedButton.icon(
- onPressed: _isPasting ? null : _pasteFromClipboard,
- icon:
- _isPasting
- ? const SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(
- color: Colors.white,
- strokeWidth: 2.0,
- ),
- )
- : const Icon(Icons.paste),
- label: Text(
- _isPasting
- ? context.l10n.pasting
- : context.l10n.pasteFromClipboard,
- ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Expanded(
+ child: ElevatedButton.icon(
+ onPressed: _isPasting ? null : _pasteFromClipboard,
+ icon:
+ _isPasting
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2.0,
+ ),
+ )
+ : const Icon(Icons.paste),
+ label: Text(
+ _isPasting
+ ? context.l10n.pasting
+ : context.l10n.pasteFromClipboard,
+ ),
+ ),
+ ),
+ if (_nfcAvailable) ...[
+ const SizedBox(width: 12),
+ Expanded(
+ child: ElevatedButton.icon(
+ onPressed: _isReadingNfc ? null : _readNfcTag,
+ icon:
+ _isReadingNfc
+ ? const SizedBox(
+ width: 20,
+ height: 20,
+ child: CircularProgressIndicator(
+ color: Colors.white,
+ strokeWidth: 2.0,
+ ),
+ )
+ : const Icon(Icons.nfc),
+ label: Text(
+ _isReadingNfc
+ ? context.l10n.readingNfc
+ : context.l10n.readNfc,
+ ),
+ ),
+ ),
+ ],
+ ],
),
),
),
diff --git a/pubspec.lock b/pubspec.lock
index d87a8e6..54bdf61 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -157,10 +157,10 @@ packages:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -497,18 +497,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
@@ -541,6 +541,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
+ nfc_manager:
+ dependency: "direct main"
+ description:
+ name: nfc_manager
+ sha256: "071faa6e43317f9feeef316067456ae6e61a40f7748ed6e93470a812e103c326"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.5.1"
numpad_layout:
dependency: "direct main"
description:
@@ -926,10 +934,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
- version: "0.7.7"
+ version: "0.7.10"
timing:
dependency: transitive
description:
@@ -1099,5 +1107,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.8.0-0 <4.0.0"
+ dart: ">=3.9.0-0 <4.0.0"
flutter: ">=3.29.0"
diff --git a/pubspec.yaml b/pubspec.yaml
index b37beb1..d2025ce 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -33,6 +33,7 @@ dependencies:
flutter_foreground_task: ^8.13.0
permission_handler: ^11.3.1
app_links: ^6.3.3
+ nfc_manager: ^3.5.0
dev_dependencies:
flutter_test: