Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />

<application
android:label="Ecash App"
Expand Down
12 changes: 12 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,18 @@
"pasteFromClipboard": "Paste from Clipboard",
"@pasteFromClipboard": {},

"readNfc": "Read NFC",
"@readNfc": {},

"readingNfc": "Reading...",
"@readingNfc": {},

"nfcNotAvailable": "NFC is not available on this device",
"@nfcNotAvailable": {},

"nfcNoTextRecord": "No readable text found on NFC tag",
"@nfcNoTextRecord": {},

"gettingChangeFromMint": "Getting change from mint...",
"@gettingChangeFromMint": {},

Expand Down
4 changes: 4 additions & 0 deletions lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@
"clipboardIsEmpty": "El portapapeles está vacío",
"pasting": "Pegando...",
"pasteFromClipboard": "Pegar del portapapeles",
"readNfc": "Leer NFC",
"readingNfc": "Leyendo...",
"nfcNotAvailable": "NFC no está disponible en este dispositivo",
"nfcNoTextRecord": "No se encontró texto legible en la etiqueta NFC",
"gettingChangeFromMint": "Obteniendo cambio del mint...",
"failedToLoadEcash": "Error al cargar Ecash",
"ecashWithdrawn": "Ecash retirado",
Expand Down
161 changes: 143 additions & 18 deletions lib/scan.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:crypto/crypto.dart';
import 'package:ecashapp/extensions/build_context_l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:qr_code_scanner_plus/qr_code_scanner_plus.dart';

Expand All @@ -43,6 +44,8 @@ class ScanQRPage extends StatefulWidget {
class _ScanQRPageState extends State<ScanQRPage> {
bool _scanned = false;
bool _isPasting = false;
bool _isReadingNfc = false;
bool _nfcAvailable = false;
bool _permissionDenied = false;
_QrLoopSession? _currentSession;

Expand All @@ -61,6 +64,7 @@ class _ScanQRPageState extends State<ScanQRPage> {
void initState() {
super.initState();
_requestCameraPermission();
_checkNfcAvailability();
}

Future<void> _requestCameraPermission() async {
Expand All @@ -87,9 +91,20 @@ class _ScanQRPageState extends State<ScanQRPage> {
_qrController?.resumeCamera();
}

Future<void> _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();
}

Expand Down Expand Up @@ -613,6 +628,85 @@ class _ScanQRPageState extends State<ScanQRPage> {
setState(() => _isPasting = false);
}

Future<void> _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;
Expand Down Expand Up @@ -760,24 +854,55 @@ class _ScanQRPageState extends State<ScanQRPage> {
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,
),
),
),
],
],
),
),
),
Expand Down
26 changes: 17 additions & 9 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading