fix(errors): sanitize user-facing failures and centralize safe error messaging
This commit is contained in:
parent
947992e8b9
commit
1dc440cd25
8 changed files with 172 additions and 31 deletions
|
|
@ -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<T?> showGlobalDialog<T>({required WidgetBuilder builder}) {
|
||||
final context = navigatorKey.currentContext;
|
||||
|
|
@ -140,7 +152,10 @@ class _AuthGateState extends State<AuthGate> {
|
|||
}
|
||||
},
|
||||
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<AuthGate> {
|
|||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<ScannerScreen>
|
|||
_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<ScannerScreen>
|
|||
} 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<ScannerScreen>
|
|||
}
|
||||
} 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CollectionsScreen>
|
|||
} 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<CollectionsScreen>
|
|||
showGlobalSnackBar('Collection created!');
|
||||
_load();
|
||||
} catch (e) {
|
||||
showGlobalSnackBar('Failed: $e', isError: true);
|
||||
showGlobalError(
|
||||
e,
|
||||
fallback: 'Could not create collection. Please try again.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<GarageScreen> {
|
|||
} 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<GarageScreen> {
|
|||
_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<GarageScreen> {
|
|||
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<GarageScreen> {
|
|||
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<GarageScreen> {
|
|||
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<GarageScreen> {
|
|||
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<GarageScreen> {
|
|||
|
||||
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<GarageScreen> {
|
|||
: '${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<GarageScreen> {
|
|||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,10 @@ class _ManageCollectionScreenState extends State<ManageCollectionScreen> {
|
|||
} 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<ManageCollectionScreen> {
|
|||
});
|
||||
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<ManageCollectionScreen> {
|
|||
);
|
||||
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<ManageCollectionScreen> {
|
|||
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<ManageCollectionScreen> {
|
|||
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<ManageCollectionScreen> {
|
|||
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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<MyReportsScreen> {
|
|||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,7 +76,10 @@ class ScanTabState extends State<ScanTab> {
|
|||
} 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<ScanTab> {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
lib/utils/error_utils.dart
Normal file
37
lib/utils/error_utils.dart
Normal file
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue