diff --git a/lib/main.dart b/lib/main.dart index c041243..24e299e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'services/collection_service.dart'; import 'services/main_collection_sync.dart'; import 'screens/login_screen.dart'; import 'screens/home_shell.dart'; +import 'utils/error_utils.dart'; // Re-export so other files can `import '../main.dart'` for these. export 'package:supabase_flutter/supabase_flutter.dart' @@ -66,6 +67,17 @@ void showGlobalSnackBar(String message, {bool isError = false}) { ); } +void showGlobalError( + Object error, { + String fallback = 'Something went wrong. Please try again.', +}) { + logError('ui', error); + showGlobalSnackBar( + userMessageForError(error, fallback: fallback), + isError: true, + ); +} + /// Show a dialog safely through the global navigator key. Future showGlobalDialog({required WidgetBuilder builder}) { final context = navigatorKey.currentContext; @@ -140,7 +152,10 @@ class _AuthGateState extends State { } }, onError: (error) { - showGlobalSnackBar('Auth error: $error', isError: true); + showGlobalError( + error, + fallback: 'Authentication error. Please sign in again.', + ); }, ); @@ -156,7 +171,10 @@ class _AuthGateState extends State { final defaultCollectionId = await CollectionService.ensureDefaultCollection(); await _ensureMainCollectionPreference(defaultCollectionId); } catch (e) { - showGlobalSnackBar('Collection setup failed: $e', isError: true); + showGlobalError( + e, + fallback: 'Collection setup failed. Please try again.', + ); } } diff --git a/lib/scanner_screen.dart b/lib/scanner_screen.dart index e42e89c..00e9ad9 100644 --- a/lib/scanner_screen.dart +++ b/lib/scanner_screen.dart @@ -6,6 +6,7 @@ import 'package:camera/camera.dart'; import 'package:google_mlkit_text_recognition/google_mlkit_text_recognition.dart'; import 'services/collection_service.dart'; import 'theme/app_colors.dart'; +import 'utils/error_utils.dart'; import 'utils/scanner_utils.dart'; /// Screen that uses the camera to scan text (OCR) from a Hot Wheels package @@ -126,9 +127,15 @@ class _ScannerScreenState extends State _cameraError = 'Camera unavailable. Please retry.'; _statusText = 'Camera unavailable'; }); + logError('scanner.initCamera', e); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Camera init failed: $e'), + content: Text( + userMessageForError( + e, + fallback: 'Failed to start camera. Please try again.', + ), + ), backgroundColor: Colors.red, ), ); @@ -211,8 +218,17 @@ class _ScannerScreenState extends State } catch (e) { _nextScanAllowedAt = DateTime.now().add(_scanCooldownError); if (!mounted) return; + logError('scanner.capture', e); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Scan error: $e'), backgroundColor: Colors.red), + SnackBar( + content: Text( + userMessageForError( + e, + fallback: 'Scan failed. Please try again.', + ), + ), + backgroundColor: Colors.red, + ), ); setState(() => _statusText = 'Scan failed, try again'); } finally { @@ -258,8 +274,17 @@ class _ScannerScreenState extends State } } catch (e) { if (!mounted) return; + logError('scanner.submitDetected', e); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Process error: $e'), backgroundColor: Colors.red), + SnackBar( + content: Text( + userMessageForError( + e, + fallback: 'Could not process this scan. Please try again.', + ), + ), + backgroundColor: Colors.red, + ), ); } } diff --git a/lib/screens/collections_screen.dart b/lib/screens/collections_screen.dart index 6e387aa..276c902 100644 --- a/lib/screens/collections_screen.dart +++ b/lib/screens/collections_screen.dart @@ -4,6 +4,7 @@ import '../main.dart'; import '../services/collection_service.dart'; import '../services/main_collection_sync.dart'; import '../theme/app_colors.dart'; +import '../utils/error_utils.dart'; import 'garage_screen.dart'; import 'manage_collection_screen.dart'; @@ -103,9 +104,13 @@ class CollectionsScreenState extends State } catch (e) { if (!mounted) return; setState(() { - _error = e.toString(); + _error = userMessageForError( + e, + fallback: 'Failed to load collections. Please try again.', + ); _isLoading = false; }); + logError('collections.load', e); } } @@ -185,7 +190,10 @@ class CollectionsScreenState extends State showGlobalSnackBar('Collection created!'); _load(); } catch (e) { - showGlobalSnackBar('Failed: $e', isError: true); + showGlobalError( + e, + fallback: 'Could not create collection. Please try again.', + ); } } diff --git a/lib/screens/garage_screen.dart b/lib/screens/garage_screen.dart index cb59788..55a186f 100644 --- a/lib/screens/garage_screen.dart +++ b/lib/screens/garage_screen.dart @@ -7,6 +7,7 @@ import '../main.dart'; import '../services/collection_service.dart'; import '../services/storage_service.dart'; import '../theme/app_colors.dart'; +import '../utils/error_utils.dart'; import '../widgets/car_card.dart'; /// The "My Garage" screen — shows a collection's cars in a grid. @@ -130,10 +131,14 @@ class GarageScreenState extends State { } catch (e) { if (!mounted) return; setState(() { - _error = e.toString(); + _error = userMessageForError( + e, + fallback: 'Failed to load cars. Please try again.', + ); _isLoading = false; _isLoadingMore = false; }); + logError('garage.loadCars', e); } } @@ -519,9 +524,11 @@ class GarageScreenState extends State { _toggleSelectionMode(false); await _loadCars(reset: true); } catch (e) { - showGlobalSnackBar( - widget.isOwner ? 'Failed to move cars: $e' : 'Failed to copy cars: $e', - isError: true, + showGlobalError( + e, + fallback: widget.isOwner + ? 'Failed to move cars. Please try again.' + : 'Failed to copy cars. Please try again.', ); } } @@ -833,9 +840,9 @@ class GarageScreenState extends State { oldPath: oldPath, ); } catch (e) { - showGlobalSnackBar( - 'Failed to upload photo. Please try a smaller/clearer image. ($e)', - isError: true, + showGlobalError( + e, + fallback: 'Failed to upload photo. Please try a smaller image.', ); return; } @@ -850,7 +857,10 @@ class GarageScreenState extends State { if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); // refresh grid } catch (e) { - showGlobalSnackBar('Failed to save: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to save photo. Please try again.', + ); } } @@ -878,7 +888,10 @@ class GarageScreenState extends State { if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); } catch (e) { - showGlobalSnackBar('Failed to update: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to update car. Please try again.', + ); } } @@ -908,7 +921,10 @@ class GarageScreenState extends State { if (sheetContext.mounted) Navigator.pop(sheetContext); _loadCars(reset: true); } catch (e) { - showGlobalSnackBar('Failed to submit validation vote: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to submit validation vote. Please try again.', + ); } } @@ -958,7 +974,10 @@ class GarageScreenState extends State { showGlobalSnackBar('Thanks for reporting. We will review this entry.'); } catch (e) { - showGlobalSnackBar('Failed to submit report: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to submit report. Please try again.', + ); } } @@ -1057,9 +1076,11 @@ class GarageScreenState extends State { : '${car['hw_id']} copied to another collection.'); await _loadCars(reset: true); } catch (e) { - showGlobalSnackBar( - widget.isOwner ? 'Failed to move car: $e' : 'Failed to copy car: $e', - isError: true, + showGlobalError( + e, + fallback: widget.isOwner + ? 'Failed to move car. Please try again.' + : 'Failed to copy car. Please try again.', ); } } @@ -1110,7 +1131,10 @@ class GarageScreenState extends State { showGlobalSnackBar('${car['hw_id']} removed from your garage.'); _loadCars(reset: true); } catch (e) { - showGlobalSnackBar('Failed to remove: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to remove car. Please try again.', + ); } } } diff --git a/lib/screens/manage_collection_screen.dart b/lib/screens/manage_collection_screen.dart index cc3923b..2e519a7 100644 --- a/lib/screens/manage_collection_screen.dart +++ b/lib/screens/manage_collection_screen.dart @@ -40,7 +40,10 @@ class _ManageCollectionScreenState extends State { } catch (e) { if (!mounted) return; setState(() => _isLoading = false); - showGlobalSnackBar('Failed to load members: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to load members. Please try again.', + ); } } @@ -124,7 +127,10 @@ class _ManageCollectionScreenState extends State { }); showGlobalSnackBar('Collection renamed!'); } catch (e) { - showGlobalSnackBar('Failed: $e', isError: true); + showGlobalError( + e, + fallback: 'Could not rename collection. Please try again.', + ); } } @@ -243,7 +249,10 @@ class _ManageCollectionScreenState extends State { ); await _loadMembers(); } catch (e) { - showGlobalSnackBar('$e', isError: true); + showGlobalError( + e, + fallback: 'Could not send invitation. Please try again.', + ); } finally { if (mounted) setState(() => _isInviting = false); } @@ -280,7 +289,10 @@ class _ManageCollectionScreenState extends State { showGlobalSnackBar('Member removed.'); await _loadMembers(); } catch (e) { - showGlobalSnackBar('Failed: $e', isError: true); + showGlobalError( + e, + fallback: 'Could not remove member. Please try again.', + ); } } @@ -313,7 +325,10 @@ class _ManageCollectionScreenState extends State { showGlobalSnackBar('Left "${_collection.name}".'); if (mounted) Navigator.pop(context); } catch (e) { - showGlobalSnackBar('Failed: $e', isError: true); + showGlobalError( + e, + fallback: 'Could not leave collection. Please try again.', + ); } } @@ -346,7 +361,10 @@ class _ManageCollectionScreenState extends State { showGlobalSnackBar('Collection deleted.'); if (mounted) Navigator.pop(context); } catch (e) { - showGlobalSnackBar('Failed: $e', isError: true); + showGlobalError( + e, + fallback: 'Could not delete collection. Please try again.', + ); } } diff --git a/lib/screens/my_reports_screen.dart b/lib/screens/my_reports_screen.dart index 171bb5d..61025b0 100644 --- a/lib/screens/my_reports_screen.dart +++ b/lib/screens/my_reports_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../main.dart'; import '../theme/app_colors.dart'; +import '../utils/error_utils.dart'; import '../utils/reporting_utils.dart'; class MyReportsScreen extends StatefulWidget { @@ -45,9 +46,13 @@ class _MyReportsScreenState extends State { } catch (e) { if (!mounted) return; setState(() { - _error = e.toString(); + _error = userMessageForError( + e, + fallback: 'Failed to load reports. Please try again.', + ); _isLoading = false; }); + logError('reports.load', e); } } diff --git a/lib/screens/scan_tab.dart b/lib/screens/scan_tab.dart index 90c2f30..70efc99 100644 --- a/lib/screens/scan_tab.dart +++ b/lib/screens/scan_tab.dart @@ -76,7 +76,10 @@ class ScanTabState extends State { } catch (e) { if (!mounted) return; setState(() => _loadingCollections = false); - showGlobalSnackBar('Failed to load collections: $e', isError: true); + showGlobalError( + e, + fallback: 'Failed to load collections. Please try again.', + ); } } @@ -409,7 +412,10 @@ class ScanTabState extends State { return true; } catch (e) { if (mounted) setState(() => _isBusy = false); - showGlobalSnackBar('DB error: $e', isError: true); + showGlobalError( + e, + fallback: 'Could not save this car right now. Please try again.', + ); return true; } } diff --git a/lib/utils/error_utils.dart b/lib/utils/error_utils.dart new file mode 100644 index 0000000..3beee1d --- /dev/null +++ b/lib/utils/error_utils.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +String userMessageForError( + Object error, { + String fallback = 'Something went wrong. Please try again.', +}) { + if (error is AuthException) { + return error.message; + } + + final raw = error.toString(); + final normalized = raw.toLowerCase(); + + if (normalized.contains('socket') || + normalized.contains('network') || + normalized.contains('timeout')) { + return 'Network issue. Please check your connection and try again.'; + } + + if (normalized.contains('permission') || normalized.contains('not allowed')) { + return 'You do not have permission for this action.'; + } + + if (normalized.contains('signed in')) { + return 'Please sign in again and retry.'; + } + + return fallback; +} + +void logError(String scope, Object error, [StackTrace? stackTrace]) { + debugPrint('[$scope] $error'); + if (stackTrace != null) { + debugPrint('$stackTrace'); + } +}