Skip to content

Commit e25aafa

Browse files
committed
fix(core): improve nfc session failure handling
1 parent 78d6e0b commit e25aafa

2 files changed

Lines changed: 109 additions & 13 deletions

File tree

lib/core/widgets/nfc_bottom_sheet.dart

Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,28 @@ import 'package:flutter/material.dart';
1313
import 'package:flutter_nfc_kit/flutter_nfc_kit.dart';
1414
import 'package:ndef/ndef.dart' as ndef;
1515

16+
enum _NfcOperation { read, write }
17+
1618
class 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
}

localization/app_en.arb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6339,6 +6339,14 @@
63396339
"@nfcInvalidData": {
63406340
"description": "Error message shown when an NFC tag is read but no supported payload can be parsed"
63416341
},
6342+
"nfcConnectionLost": "NFC connection lost. Hold your device near the NFC tag and try again.",
6343+
"@nfcConnectionLost": {
6344+
"description": "Error message shown when an NFC tag connection is lost before the read or write completes"
6345+
},
6346+
"nfcWriteFailed": "NFC transfer failed. Please try again.",
6347+
"@nfcWriteFailed": {
6348+
"description": "Error message shown when an NFC write to a hardware wallet fails"
6349+
},
63426350
"nfcError": "NFC error: {error}",
63436351
"@nfcError": {
63446352
"description": "Error message shown when an NFC operation fails",

0 commit comments

Comments
 (0)