fix(errors): sanitize user-facing failures and centralize safe error messaging

This commit is contained in:
Lukas Müllner 2026-03-05 14:40:37 +01:00
parent 947992e8b9
commit 1dc440cd25
8 changed files with 172 additions and 31 deletions

View file

@ -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.',
);
}
}

View file

@ -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,
),
);
}
}

View file

@ -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.',
);
}
}

View file

@ -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.',
);
}
}
}

View file

@ -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.',
);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View 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');
}
}