@@ -13,18 +13,28 @@ import 'package:flutter/material.dart';
1313import 'package:flutter_nfc_kit/flutter_nfc_kit.dart' ;
1414import 'package:ndef/ndef.dart' as ndef;
1515
16+ enum _NfcOperation { read, write }
17+
1618class NfcBottomSheet {
19+ static const _iosReadAttempts = 3 ;
20+
1721 static Future <void > showReadNfc ({
1822 required BuildContext context,
1923 required String title,
2024 required FutureOr <void > Function (String payload) onDataReceived,
2125 }) async {
2226 var didReadRecords = false ;
27+ final connectionLostMessage = context.loc.nfcConnectionLost;
2328 final payload = await _runWithNfcSession <String ?>(
2429 context: context,
2530 title: title,
31+ operation: _NfcOperation .read,
2632 action: (_) async {
27- final payload = await _readNfcPayload ();
33+ final payload = Platform .isIOS
34+ ? await _readNfcPayloadWithIosRetry (
35+ connectionLostMessage: connectionLostMessage,
36+ )
37+ : await _readNfcPayload ();
2838 didReadRecords = true ;
2939 return payload;
3040 },
@@ -44,10 +54,12 @@ class NfcBottomSheet {
4454 required String data,
4555 required FutureOr <void > Function () onSuccess,
4656 }) async {
57+ final writeFailedMessage = context.loc.nfcWriteFailed;
4758 final didWrite = await _runWithNfcSession <bool >(
4859 context: context,
4960 title: title,
50- action: (_) => _writeNfcData (data),
61+ operation: _NfcOperation .write,
62+ action: (_) => _writeNfcData (data, iosErrorMessage: writeFailedMessage),
5163 );
5264
5365 if (didWrite == true ) {
@@ -58,6 +70,7 @@ class NfcBottomSheet {
5870 static Future <T ?> _runWithNfcSession <T >({
5971 required BuildContext context,
6072 required String title,
73+ required _NfcOperation operation,
6174 required Future <T ?> Function (NFCTag tag) action,
6275 }) async {
6376 final availability = await _nfcAvailability (context);
@@ -71,18 +84,26 @@ class NfcBottomSheet {
7184 if (! context.mounted) return null ;
7285
7386 if (Platform .isIOS) {
87+ final NFCTag tag;
7488 try {
7589 // Coldcard uses NFC-V / ISO-15693.
76- final tag = await FlutterNfcKit .poll (
90+ tag = await FlutterNfcKit .poll (
7791 iosAlertMessage: title,
7892 readIso15693: true ,
7993 readIso18092: false ,
8094 );
81- return await action (tag);
8295 } catch (e) {
96+ await _finishNfcSession ();
8397 _handleNfcError (context, e);
8498 return null ;
8599 }
100+
101+ try {
102+ return await action (tag);
103+ } catch (e) {
104+ _handleNfcError (context, e, operation: operation);
105+ return null ;
106+ }
86107 }
87108
88109 T ? result;
@@ -112,12 +133,14 @@ class NfcBottomSheet {
112133 child: NfcScannerWidget (
113134 onError: (error) => _handleNfcError (sheetContext, error),
114135 onScanned: (tag) async {
136+ var didCompleteAction = false ;
115137 try {
116138 result = await action (tag);
139+ didCompleteAction = true ;
117140 } catch (e) {
118- _handleNfcError (sheetContext, e);
141+ _handleNfcError (sheetContext, e, operation : operation );
119142 }
120- if (sheetContext.mounted) {
143+ if (didCompleteAction && sheetContext.mounted) {
121144 Navigator .of (sheetContext).pop ();
122145 }
123146 },
@@ -151,7 +174,44 @@ class NfcBottomSheet {
151174 }
152175 }
153176
154- static Future <bool > _writeNfcData (String data) async {
177+ static Future <String ?> _readNfcPayloadWithIosRetry ({
178+ required String connectionLostMessage,
179+ }) async {
180+ String ? iosErrorMessage;
181+ try {
182+ for (var attempt = 1 ; attempt <= _iosReadAttempts; attempt++ ) {
183+ try {
184+ final records = await FlutterNfcKit .readNDEFRecords ();
185+ return payloadFromNdefRecords (records);
186+ } catch (e) {
187+ if (_isUserCancelled (e)) rethrow ;
188+
189+ final isReadInterrupted = _isNfcReadInterrupted (e);
190+ if (! isReadInterrupted || attempt == _iosReadAttempts) {
191+ if (isReadInterrupted) {
192+ iosErrorMessage = connectionLostMessage;
193+ }
194+ rethrow ;
195+ }
196+
197+ log.warning ('NFC read interrupted; restarting polling' , error: e);
198+ iosErrorMessage = connectionLostMessage;
199+ await _setIosAlertMessage (connectionLostMessage);
200+ await FlutterNfcKit .iosRestartPolling ();
201+ iosErrorMessage = null ;
202+ }
203+ }
204+
205+ return null ;
206+ } finally {
207+ await _finishNfcSession (iosErrorMessage: iosErrorMessage);
208+ }
209+ }
210+
211+ static Future <bool > _writeNfcData (
212+ String data, {
213+ String ? iosErrorMessage,
214+ }) async {
155215 try {
156216 await FlutterNfcKit .writeNDEFRecords ([
157217 ndef.TextRecord (
@@ -160,20 +220,32 @@ class NfcBottomSheet {
160220 encoding: ndef.TextEncoding .UTF8 ,
161221 ),
162222 ]);
163- return true ;
164- } finally {
165223 await _finishNfcSession ();
224+ return true ;
225+ } catch (e) {
226+ await _finishNfcSession (
227+ iosErrorMessage: _isUserCancelled (e) ? null : iosErrorMessage,
228+ );
229+ rethrow ;
166230 }
167231 }
168232
169- static Future <void > _finishNfcSession () async {
233+ static Future <void > _finishNfcSession ({ String ? iosErrorMessage} ) async {
170234 try {
171- await FlutterNfcKit .finish ();
235+ await FlutterNfcKit .finish (iosErrorMessage : iosErrorMessage );
172236 } catch (e) {
173237 log.warning ('Failed to finish NFC session' , error: e);
174238 }
175239 }
176240
241+ static Future <void > _setIosAlertMessage (String message) async {
242+ try {
243+ await FlutterNfcKit .setIosAlertMessage (message);
244+ } catch (e) {
245+ log.warning ('Failed to update iOS NFC alert message' , error: e);
246+ }
247+ }
248+
177249 static void _showAvailabilityError (
178250 BuildContext context,
179251 NFCAvailability availability,
@@ -188,10 +260,20 @@ class NfcBottomSheet {
188260 SnackBarUtils .showSnackBar (context, message);
189261 }
190262
191- static void _handleNfcError (BuildContext context, Object error) {
263+ static void _handleNfcError (
264+ BuildContext context,
265+ Object error, {
266+ _NfcOperation ? operation,
267+ }) {
192268 log.warning ('NFC operation failed' , error: error);
193269 if (! context.mounted || _isUserCancelled (error)) return ;
194- SnackBarUtils .showSnackBar (context, context.loc.nfcError (error.toString ()));
270+
271+ final message = operation == _NfcOperation .write
272+ ? context.loc.nfcWriteFailed
273+ : _isNfcReadInterrupted (error)
274+ ? context.loc.nfcConnectionLost
275+ : context.loc.nfcError (error.toString ());
276+ SnackBarUtils .showSnackBar (context, message);
195277 }
196278
197279 static void _showInvalidDataError (BuildContext context) {
@@ -202,4 +284,10 @@ class NfcBottomSheet {
202284
203285 static bool _isUserCancelled (Object error) =>
204286 error.toString ().contains ('Session invalidated by user' );
287+
288+ static bool _isNfcReadInterrupted (Object error) {
289+ final message = error.toString ();
290+ return message.contains ('Tag connection lost' ) ||
291+ message.contains ('Read NDEF error' );
292+ }
205293}
0 commit comments