feat(mobile): implement centralized error handling and project cleanup
- Implemented centralized error handling system (#377) - Unified UIErrorSnackbar and BlocErrorHandler mixin - Migrated ClientAuthBloc and ClientHubsBloc - Consolidated documentation - Addresses Mobile Apps: Project Cleanup (#378)
This commit is contained in:
@@ -10,3 +10,5 @@ export 'src/widgets/ui_step_indicator.dart';
|
||||
export 'src/widgets/ui_icon_button.dart';
|
||||
export 'src/widgets/ui_button.dart';
|
||||
export 'src/widgets/ui_chip.dart';
|
||||
export 'src/widgets/ui_error_snackbar.dart';
|
||||
export 'src/widgets/ui_success_snackbar.dart';
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_core_localization/krow_core_localization.dart';
|
||||
import '../ui_colors.dart';
|
||||
import '../ui_typography.dart';
|
||||
|
||||
/// Centralized error snackbar for consistent error presentation across the app.
|
||||
///
|
||||
/// This widget automatically resolves localization keys and displays
|
||||
/// user-friendly error messages with optional error codes for support.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UiErrorSnackbar.show(
|
||||
/// context,
|
||||
/// messageKey: 'errors.auth.invalid_credentials',
|
||||
/// errorCode: 'AUTH_001',
|
||||
/// );
|
||||
/// ```
|
||||
class UiErrorSnackbar {
|
||||
/// Shows an error snackbar with a localized message.
|
||||
///
|
||||
/// [messageKey] should be a dot-separated path like 'errors.auth.invalid_credentials'
|
||||
/// [errorCode] is optional and will be shown in smaller text for support reference
|
||||
/// [duration] controls how long the snackbar is visible
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String messageKey,
|
||||
String? errorCode,
|
||||
Duration duration = const Duration(seconds: 4),
|
||||
}) {
|
||||
final texts = Texts.of(context);
|
||||
final message = _getMessageFromKey(texts, messageKey);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.error_outline, color: UiColors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
message,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.white),
|
||||
),
|
||||
if (errorCode != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Error Code: $errorCode',
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: UiColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: duration,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolves a localization key path to the actual translated message.
|
||||
///
|
||||
/// Supports keys like:
|
||||
/// - errors.auth.invalid_credentials
|
||||
/// - errors.hub.has_orders
|
||||
/// - errors.generic.unknown
|
||||
static String _getMessageFromKey(Texts texts, String key) {
|
||||
// Parse key like "errors.auth.invalid_credentials"
|
||||
final parts = key.split('.');
|
||||
if (parts.length < 2) return texts.errors.generic.unknown;
|
||||
|
||||
try {
|
||||
switch (parts[1]) {
|
||||
case 'auth':
|
||||
return _getAuthError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'hub':
|
||||
return _getHubError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'order':
|
||||
return _getOrderError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'profile':
|
||||
return _getProfileError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'shift':
|
||||
return _getShiftError(texts, parts.length > 2 ? parts[2] : '');
|
||||
case 'generic':
|
||||
return _getGenericError(texts, parts.length > 2 ? parts[2] : '');
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
} catch (_) {
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getAuthError(Texts texts, String key) {
|
||||
switch (key) {
|
||||
case 'invalid_credentials':
|
||||
return texts.errors.auth.invalid_credentials;
|
||||
case 'account_exists':
|
||||
return texts.errors.auth.account_exists;
|
||||
case 'session_expired':
|
||||
return texts.errors.auth.session_expired;
|
||||
case 'user_not_found':
|
||||
return texts.errors.auth.user_not_found;
|
||||
case 'unauthorized_app':
|
||||
return texts.errors.auth.unauthorized_app;
|
||||
case 'weak_password':
|
||||
return texts.errors.auth.weak_password;
|
||||
case 'sign_up_failed':
|
||||
return texts.errors.auth.sign_up_failed;
|
||||
case 'sign_in_failed':
|
||||
return texts.errors.auth.sign_in_failed;
|
||||
case 'not_authenticated':
|
||||
return texts.errors.auth.not_authenticated;
|
||||
case 'password_mismatch':
|
||||
return texts.errors.auth.password_mismatch;
|
||||
case 'google_only_account':
|
||||
return texts.errors.auth.google_only_account;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getHubError(Texts texts, String key) {
|
||||
switch (key) {
|
||||
case 'has_orders':
|
||||
return texts.errors.hub.has_orders;
|
||||
case 'not_found':
|
||||
return texts.errors.hub.not_found;
|
||||
case 'creation_failed':
|
||||
return texts.errors.hub.creation_failed;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getOrderError(Texts texts, String key) {
|
||||
switch (key) {
|
||||
case 'missing_hub':
|
||||
return texts.errors.order.missing_hub;
|
||||
case 'missing_vendor':
|
||||
return texts.errors.order.missing_vendor;
|
||||
case 'creation_failed':
|
||||
return texts.errors.order.creation_failed;
|
||||
case 'shift_creation_failed':
|
||||
return texts.errors.order.shift_creation_failed;
|
||||
case 'missing_business':
|
||||
return texts.errors.order.missing_business;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getProfileError(Texts texts, String key) {
|
||||
switch (key) {
|
||||
case 'staff_not_found':
|
||||
return texts.errors.profile.staff_not_found;
|
||||
case 'business_not_found':
|
||||
return texts.errors.profile.business_not_found;
|
||||
case 'update_failed':
|
||||
return texts.errors.profile.update_failed;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getShiftError(Texts texts, String key) {
|
||||
switch (key) {
|
||||
case 'no_open_roles':
|
||||
return texts.errors.shift.no_open_roles;
|
||||
case 'application_not_found':
|
||||
return texts.errors.shift.application_not_found;
|
||||
case 'no_active_shift':
|
||||
return texts.errors.shift.no_active_shift;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
static String _getGenericError(Texts texts, String key) {
|
||||
switch (key) {
|
||||
case 'unknown':
|
||||
return texts.errors.generic.unknown;
|
||||
case 'no_connection':
|
||||
return texts.errors.generic.no_connection;
|
||||
default:
|
||||
return texts.errors.generic.unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../ui_colors.dart';
|
||||
import '../ui_typography.dart';
|
||||
|
||||
/// Centralized success snackbar for consistent success message presentation.
|
||||
///
|
||||
/// This widget provides a unified way to show success feedback across the app
|
||||
/// with consistent styling and behavior.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// UiSuccessSnackbar.show(
|
||||
/// context,
|
||||
/// message: 'Profile updated successfully!',
|
||||
/// );
|
||||
/// ```
|
||||
class UiSuccessSnackbar {
|
||||
/// Shows a success snackbar with a custom message.
|
||||
///
|
||||
/// [message] is the success message to display
|
||||
/// [duration] controls how long the snackbar is visible
|
||||
static void show(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline, color: UiColors.white),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message,
|
||||
style: UiTypography.body2m.copyWith(color: UiColors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: UiColors.success,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: duration,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
margin: const EdgeInsets.all(16),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user