Skip to content

Commit 13f5baf

Browse files
authored
Merge pull request #813 from egovernments/scanner-crash-fix
Scanner Crash Fixes, And Checks added.
2 parents 56420e4 + a1b03ec commit 13f5baf

File tree

7 files changed

+286
-141
lines changed

7 files changed

+286
-141
lines changed

packages/digit_scanner/lib/blocs/scanner.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,11 @@ class DigitScannerBloc extends Bloc<DigitScannerEvent, DigitScannerState> {
9090
scannerId: event.scannerId,
9191
));
9292
} catch (error) {
93-
rethrow;
93+
emit(state.copyWith(
94+
error: 'Unable to scan: ${error.toString()}',
95+
loading: false,
96+
));
97+
return;
9498
} finally {
9599
emit(state.copyWith(loading: false));
96100
}

packages/digit_scanner/lib/models/scanner_validation.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ class ScannerValidation {
2222

2323
factory ScannerValidation.fromJson(Map<String, dynamic> json) {
2424
return ScannerValidation(
25-
type: json['type'] as String,
25+
type: json['type']?.toString() ?? '',
2626
value: json['value'],
27-
message: json['message'] as String?,
27+
message: json['message']?.toString(),
2828
);
2929
}
3030

@@ -63,7 +63,11 @@ extension ScannerValidationListExt on List<ScannerValidation>? {
6363

6464
/// Gets the regex pattern for validation
6565
String? get pattern {
66-
return this?.firstWhereOrNull((v) => v.type == 'pattern')?.value as String?;
66+
final validation = this?.firstWhereOrNull((v) => v.type == 'pattern');
67+
final value = validation?.value;
68+
if (value == null) return null;
69+
if (value is String) return value;
70+
return value.toString();
6771
}
6872

6973
/// Gets the scan limit validation message

packages/digit_scanner/lib/pages/qr_scanner.dart

Lines changed: 169 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,58 @@ class DigitScannerPageState extends LocalizedState<DigitScannerPage>
110110
static const _manualSerialNoFormKey = 'serialNoCode';
111111
static const _manualExpiryDateFormKey = 'expiryDate';
112112

113+
/// Safely parses DateTime from form control value
114+
/// Handles String, DateTime, int (milliseconds), and null values
115+
DateTime _parseExpiryDate(dynamic value) {
116+
// If already a DateTime, return it
117+
if (value is DateTime) return value;
118+
119+
// If it's a String, try to parse with different formats
120+
if (value is String) {
121+
final trimmed = value.trim();
122+
if (trimmed.isEmpty) return DateTime.now();
123+
124+
// Try common date formats
125+
final formats = [
126+
"dd/MM/yyyy",
127+
"dd/MM/yy",
128+
"dd MMM yyyy",
129+
"yyyy-MM-dd",
130+
"MM/dd/yyyy",
131+
];
132+
133+
for (final format in formats) {
134+
try {
135+
return DateFormat(format).parse(trimmed);
136+
} catch (_) {
137+
// Try next format
138+
}
139+
}
140+
141+
// Try ISO8601 as fallback
142+
try {
143+
return DateTime.parse(trimmed);
144+
} catch (_) {
145+
debugPrint('Failed to parse date string: $trimmed');
146+
return DateTime.now();
147+
}
148+
}
149+
150+
// If it's an int, treat as milliseconds since epoch
151+
if (value is int) {
152+
try {
153+
return DateTime.fromMillisecondsSinceEpoch(value);
154+
} catch (_) {
155+
debugPrint('Failed to parse date from milliseconds: $value');
156+
return DateTime.now();
157+
}
158+
}
159+
160+
// For any other type or null, return current date
161+
debugPrint('Unexpected date type: ${value.runtimeType}');
162+
return DateTime.now();
163+
}
164+
113165
@override
114166
void initState() {
115167
super.initState();
@@ -135,40 +187,47 @@ class DigitScannerPageState extends LocalizedState<DigitScannerPage>
135187
widget.initialBarcodeData!.isNotEmpty) {
136188
// Initialize with existing barcode (GS1) data for edit mode
137189
// Parse the comma-separated string (GTIN,SERIAL,BATCH,EXPIRY) back to GS1Barcode
138-
final parts = widget.initialBarcodeData!.split(',');
139-
if (parts.length >= 4) {
140-
final gtin = parts[0].trim();
141-
final serial = parts[1].trim();
142-
final batch = parts[2].trim();
143-
final expiryStr = parts[3].trim();
190+
try {
191+
final parts = widget.initialBarcodeData!.split(',');
192+
if (parts.length >= 4 &&
193+
parts.every((part) => part.trim().isNotEmpty)) {
194+
final gtin = parts[0].trim();
195+
final serial = parts[1].trim();
196+
final batch = parts[2].trim();
197+
final expiryStr = parts[3].trim();
144198

145-
// Parse expiry date (format: dd MMM yyyy)
146-
DateTime? expiryDate;
147-
try {
148-
expiryDate = DateFormat('dd MMM yyyy').parse(expiryStr);
149-
} catch (_) {
150-
expiryDate = DateTime.now().add(const Duration(days: 365));
151-
}
199+
// Parse expiry date (format: dd MMM yyyy)
200+
DateTime? expiryDate;
201+
try {
202+
expiryDate = DateFormat('dd MMM yyyy').parse(expiryStr);
203+
} catch (_) {
204+
expiryDate = DateTime.now().add(const Duration(days: 365));
205+
}
152206

153-
// Generate GS1 barcode string and parse it
154-
final barcodeString = DigitScannerUtils().generateGS1Barcode(
155-
serialNumber: serial,
156-
expiryDate: expiryDate,
157-
batchNumber: batch,
158-
gtin: gtin,
159-
);
207+
// Generate GS1 barcode string and parse it
208+
final barcodeString = DigitScannerUtils().generateGS1Barcode(
209+
serialNumber: serial,
210+
expiryDate: expiryDate,
211+
batchNumber: batch,
212+
gtin: gtin,
213+
);
160214

161-
final parser = GS1BarcodeParser.defaultParser();
162-
final parsed = parser.parse(barcodeString);
215+
final parser = GS1BarcodeParser.defaultParser();
216+
final parsed = parser.parse(barcodeString);
163217

164-
result = [parsed];
165-
context.read<DigitScannerBloc>().add(
166-
DigitScannerEvent.handleScanner(
167-
qrCode: [],
168-
barCode: [parsed],
169-
scannerId: widget.scannerId,
170-
),
171-
);
218+
result = [parsed];
219+
context.read<DigitScannerBloc>().add(
220+
DigitScannerEvent.handleScanner(
221+
qrCode: [],
222+
barCode: [parsed],
223+
scannerId: widget.scannerId,
224+
),
225+
);
226+
}
227+
} catch (e) {
228+
debugPrint('Error parsing initial barcode data: $e');
229+
// Initialize with empty result on parse error
230+
result = [];
172231
}
173232
}
174233
}
@@ -461,57 +520,68 @@ class DigitScannerPageState extends LocalizedState<DigitScannerPage>
461520

462521
final bloc = context.read<DigitScannerBloc>();
463522
codes.add(form.control(_manualCodeFormKey).value);
464-
final barcodeString =
465-
DigitScannerUtils().generateGS1Barcode(
466-
serialNumber: form
467-
.control(_manualSerialNoFormKey)
468-
.value
469-
.toString()
470-
.trim(),
471-
expiryDate: form
472-
.control(_manualExpiryDateFormKey)
473-
.value as DateTime,
474-
batchNumber: form
475-
.control(_manualCodeFormKey)
476-
.value
477-
.toString()
478-
.trim(),
479-
);
480523

481-
// Now parse it using your existing model
482-
final parser = GS1BarcodeParser.defaultParser();
483-
final parsed = parser.parse(barcodeString);
484-
// ✅ Append to existing barcodes; DO NOT touch qrCodes in GS1 mode
485-
final updatedBarcodes =
486-
List<GS1Barcode>.from(state.barCodes)
487-
..add(parsed);
524+
try {
525+
final barcodeString =
526+
DigitScannerUtils().generateGS1Barcode(
527+
serialNumber: form
528+
.control(_manualSerialNoFormKey)
529+
.value
530+
.toString()
531+
.trim(),
532+
expiryDate: _parseExpiryDate(form
533+
.control(_manualExpiryDateFormKey)
534+
.value),
535+
batchNumber: form
536+
.control(_manualCodeFormKey)
537+
.value
538+
.toString()
539+
.trim(),
540+
);
488541

489-
// Keep local mirror in sync (used by UI)
490-
setState(() {
491-
result = updatedBarcodes;
492-
manualCode = false;
493-
});
542+
// Now parse it using your existing model
543+
final parser = GS1BarcodeParser.defaultParser();
544+
final parsed = parser.parse(barcodeString);
545+
// ✅ Append to existing barcodes; DO NOT touch qrCodes in GS1 mode
546+
final updatedBarcodes =
547+
List<GS1Barcode>.from(state.barCodes)
548+
..add(parsed);
494549

495-
bloc.add(
496-
DigitScannerEvent.handleScanner(
497-
barCode: updatedBarcodes,
498-
qrCode: state.qrCodes,
499-
regex: widget.effectiveRegex,
500-
patternMessage: widget.patternMessage,
501-
scannerId: widget.scannerId,
502-
),
503-
);
504-
if (updatedBarcodes.length <
505-
widget.effectiveQuantity) {
506-
DigitScannerUtils().buildDialog(context,
507-
localizations, widget.effectiveQuantity);
508-
}
509-
setState(() {
510-
manualCode = false;
511-
});
512-
// Quantity gate for GS1
550+
// Keep local mirror in sync (used by UI)
551+
setState(() {
552+
result = updatedBarcodes;
553+
manualCode = false;
554+
});
513555

514-
initializeCameras();
556+
bloc.add(
557+
DigitScannerEvent.handleScanner(
558+
barCode: updatedBarcodes,
559+
qrCode: state.qrCodes,
560+
regex: widget.effectiveRegex,
561+
patternMessage: widget.patternMessage,
562+
scannerId: widget.scannerId,
563+
),
564+
);
565+
if (updatedBarcodes.length <
566+
widget.effectiveQuantity) {
567+
DigitScannerUtils().buildDialog(context,
568+
localizations, widget.effectiveQuantity);
569+
}
570+
setState(() {
571+
manualCode = false;
572+
});
573+
574+
initializeCameras();
575+
} catch (e) {
576+
debugPrint('Error parsing manual GS1 barcode: $e');
577+
Toast.showToast(
578+
context,
579+
type: ToastType.error,
580+
message: localizations.translate(
581+
i18.scanner.resourcesScanFailed),
582+
sentenceCaseEnabled: false,
583+
);
584+
}
515585
} else {
516586
if (form.control(_manualCodeFormKey).value ==
517587
null ||
@@ -526,7 +596,6 @@ class DigitScannerPageState extends LocalizedState<DigitScannerPage>
526596
type: ToastType.error,
527597
message: localizations
528598
.translate(i18.scanner.enterManualCode),
529-
sentenceCaseEnabled: false,
530599
);
531600
} else {
532601
final bloc = context.read<DigitScannerBloc>();
@@ -663,15 +732,23 @@ class DigitScannerPageState extends LocalizedState<DigitScannerPage>
663732
cancelText: localizations.translate(
664733
i18.common.coreCommonCancel,
665734
),
666-
initialValue: DateFormat('dd/MM/yy')
667-
.format(field.control.value),
735+
initialValue: DateFormat('dd/MM/yyyy')
736+
.format(_parseExpiryDate(
737+
field.control.value)),
668738
readOnly: false,
669739
onChange: (value) {
670-
form
671-
.control(_manualExpiryDateFormKey)
672-
.value = DateFormat(
673-
"dd/MM/yyyy")
674-
.parse(value);
740+
try {
741+
form
742+
.control(_manualExpiryDateFormKey)
743+
.value = DateFormat(
744+
"dd/MM/yyyy")
745+
.parse(value);
746+
} catch (e) {
747+
debugPrint('Error parsing date: $e');
748+
form
749+
.control(_manualExpiryDateFormKey)
750+
.value = DateTime.now();
751+
}
675752
},
676753
),
677754
);
@@ -717,11 +794,13 @@ class DigitScannerPageState extends LocalizedState<DigitScannerPage>
717794
.toString()
718795
.trim()
719796
.isEmpty) {
720-
Toast.showToast(context,
721-
type: ToastType.error,
722-
message: localizations
723-
.translate(i18.scanner.enterManualCode),
724-
sentenceCaseEnabled: false);
797+
Toast.showToast(
798+
context,
799+
type: ToastType.error,
800+
message: localizations
801+
.translate(i18.scanner.enterManualCode),
802+
sentenceCaseEnabled: false,
803+
);
725804
} else {
726805
final bloc = context.read<DigitScannerBloc>();
727806
final updatedQRCodes =

packages/digit_scanner/lib/utils/i18_key_constants.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,5 @@ class Scanner {
179179
String get cameraPermissionDeniedDesc => 'CAMERA_PERMISSION_DENIED_DESC';
180180
String get openSettings => 'OPEN_SETTINGS';
181181
String get cameraPermissionRequired => 'CAMERA_PERMISSION_REQUIRED';
182+
String get unableToScan => 'UNABLE_TO_SCAN';
182183
}

0 commit comments

Comments
 (0)