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:
2026-02-05 15:35:35 +05:30
parent 6dc700f226
commit 3924801f70
11 changed files with 783 additions and 213 deletions

View File

@@ -4,5 +4,7 @@ export 'src/domain/arguments/usecase_argument.dart';
export 'src/domain/usecases/usecase.dart';
export 'src/utils/date_time_utils.dart';
export 'src/presentation/widgets/web_mobile_frame.dart';
export 'src/presentation/mixins/bloc_error_handler.dart';
export 'src/presentation/observers/core_bloc_observer.dart';
export 'src/config/app_config.dart';
export 'src/routing/routing.dart';

View File

@@ -0,0 +1,120 @@
import 'dart:developer' as developer;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_domain/krow_domain.dart';
/// Mixin to standardize error handling across all BLoCs.
///
/// This mixin provides a centralized way to handle errors in BLoC event handlers,
/// reducing boilerplate and ensuring consistent error handling patterns.
///
/// **Benefits:**
/// - Eliminates repetitive try-catch blocks
/// - Automatically logs errors with technical details
/// - Converts AppException to localized error keys
/// - Handles unexpected errors gracefully
///
/// **Usage:**
/// ```dart
/// class MyBloc extends Bloc<MyEvent, MyState> with BlocErrorHandler<MyState> {
/// Future<void> _onEvent(MyEvent event, Emitter<MyState> emit) async {
/// await handleError(
/// emit: emit,
/// action: () async {
/// final result = await _useCase();
/// emit(MyState.success(result));
/// },
/// onError: (errorKey) => MyState.error(errorKey),
/// );
/// }
/// }
/// ```
mixin BlocErrorHandler<S> {
/// Executes an async action with centralized error handling.
///
/// [emit] - The state emitter from the event handler
/// [action] - The async operation to execute (e.g., calling a use case)
/// [onError] - Function that creates an error state from the error message key
/// [loggerName] - Optional custom logger name (defaults to BLoC class name)
///
/// **Error Flow:**
/// 1. Executes the action
/// 2. If AppException is thrown:
/// - Logs error code and technical message
/// - Emits error state with localization key
/// 3. If unexpected error is thrown:
/// - Logs full error and stack trace
/// - Emits generic error state
Future<void> handleError({
required Emitter<S> emit,
required Future<void> Function() action,
required S Function(String errorKey) onError,
String? loggerName,
}) async {
try {
await action();
} on AppException catch (e) {
// Known application error - log technical details
developer.log(
'Error ${e.code}: ${e.technicalMessage}',
name: loggerName ?? runtimeType.toString(),
);
// Emit error state with localization key
emit(onError(e.messageKey));
} catch (e, stackTrace) {
// Unexpected error - log everything for debugging
developer.log(
'Unexpected error: $e',
name: loggerName ?? runtimeType.toString(),
error: e,
stackTrace: stackTrace,
);
// Emit generic error state
emit(onError('errors.generic.unknown'));
}
}
/// Executes an async action with error handling and returns a result.
///
/// This variant is useful when you need to get a value from the action
/// and handle errors without emitting states.
///
/// Returns the result of the action, or null if an error occurred.
///
/// **Usage:**
/// ```dart
/// final user = await handleErrorWithResult(
/// action: () => _getUserUseCase(),
/// onError: (errorKey) {
/// emit(MyState.error(errorKey));
/// },
/// );
/// if (user != null) {
/// emit(MyState.success(user));
/// }
/// ```
Future<T?> handleErrorWithResult<T>({
required Future<T> Function() action,
required void Function(String errorKey) onError,
String? loggerName,
}) async {
try {
return await action();
} on AppException catch (e) {
developer.log(
'Error ${e.code}: ${e.technicalMessage}',
name: loggerName ?? runtimeType.toString(),
);
onError(e.messageKey);
return null;
} catch (e, stackTrace) {
developer.log(
'Unexpected error: $e',
name: loggerName ?? runtimeType.toString(),
error: e,
stackTrace: stackTrace,
);
onError('errors.generic.unknown');
return null;
}
}
}

View File

@@ -0,0 +1,116 @@
import 'dart:developer' as developer;
import 'package:flutter_bloc/flutter_bloc.dart';
/// Global BLoC observer for centralized logging and monitoring.
///
/// This observer provides visibility into all BLoC lifecycle events across
/// the entire application, enabling centralized logging, debugging, and
/// error monitoring.
///
/// **Features:**
/// - Logs BLoC creation and disposal
/// - Logs all events and state changes
/// - Captures and logs errors with stack traces
/// - Ready for integration with monitoring services (Sentry, Firebase Crashlytics)
///
/// **Setup:**
/// Register this observer in your app's main.dart before runApp():
/// ```dart
/// void main() {
/// Bloc.observer = CoreBlocObserver();
/// runApp(MyApp());
/// }
/// ```
class CoreBlocObserver extends BlocObserver {
/// Whether to log state changes (can be verbose in production)
final bool logStateChanges;
/// Whether to log events
final bool logEvents;
CoreBlocObserver({
this.logStateChanges = false,
this.logEvents = true,
});
@override
void onCreate(BlocBase bloc) {
super.onCreate(bloc);
developer.log(
'Created: ${bloc.runtimeType}',
name: 'BlocObserver',
);
}
@override
void onEvent(Bloc bloc, Object? event) {
super.onEvent(bloc, event);
if (logEvents) {
developer.log(
'Event: ${event.runtimeType}',
name: bloc.runtimeType.toString(),
);
}
}
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
if (logStateChanges) {
developer.log(
'State: ${change.currentState.runtimeType}${change.nextState.runtimeType}',
name: bloc.runtimeType.toString(),
);
}
}
@override
void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
// Log error to console
developer.log(
'ERROR in ${bloc.runtimeType}',
name: 'BlocObserver',
error: error,
stackTrace: stackTrace,
);
// TODO: Send to monitoring service
// Example integrations:
//
// Sentry:
// Sentry.captureException(
// error,
// stackTrace: stackTrace,
// hint: Hint.withMap({'bloc': bloc.runtimeType.toString()}),
// );
//
// Firebase Crashlytics:
// FirebaseCrashlytics.instance.recordError(
// error,
// stackTrace,
// reason: 'BLoC Error in ${bloc.runtimeType}',
// );
}
@override
void onClose(BlocBase bloc) {
super.onClose(bloc);
developer.log(
'Closed: ${bloc.runtimeType}',
name: 'BlocObserver',
);
}
@override
void onTransition(Bloc bloc, Transition transition) {
super.onTransition(bloc, transition);
if (logStateChanges) {
developer.log(
'Transition: ${transition.event.runtimeType}${transition.nextState.runtimeType}',
name: bloc.runtimeType.toString(),
);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
import 'dart:developer' as developer;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/sign_in_with_email_arguments.dart';
@@ -24,7 +23,8 @@ import 'client_auth_state.dart';
/// * Business Account Registration
/// * Social Authentication
/// * Session Termination
class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
with BlocErrorHandler<ClientAuthState> {
final SignInWithEmailUseCase _signInWithEmail;
final SignUpWithEmailUseCase _signUpWithEmail;
final SignInWithSocialUseCase _signInWithSocial;
@@ -53,28 +53,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
final User user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
final user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
);
}
/// Handles the [ClientSignUpRequested] event.
@@ -83,32 +75,24 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
final User user = await _signUpWithEmail(
SignUpWithEmailArguments(
companyName: event.companyName,
email: event.email,
password: event.password,
),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
final user = await _signUpWithEmail(
SignUpWithEmailArguments(
companyName: event.companyName,
email: event.email,
password: event.password,
),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
);
}
/// Handles the [ClientSocialSignInRequested] event.
@@ -117,28 +101,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
final User user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
final user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
);
}
/// Handles the [ClientSignOutRequested] event.
@@ -147,25 +123,17 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
},
onError: (errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,7 +1,6 @@
import 'dart:developer' as developer;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/assign_nfc_tag_arguments.dart';
import '../../domain/arguments/create_hub_arguments.dart';
@@ -18,6 +17,7 @@ import 'client_hubs_state.dart';
/// It orchestrates the flow between the UI and the domain layer by invoking
/// specific use cases for fetching, creating, deleting, and assigning tags to hubs.
class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
with BlocErrorHandler<ClientHubsState>
implements Disposable {
final GetHubsUseCase _getHubsUseCase;
final CreateHubUseCase _createHubUseCase;
@@ -66,26 +66,18 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.loading));
try {
final List<Hub> hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
final hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
},
onError: (errorKey) => state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: errorKey,
),
);
}
Future<void> _onAddRequested(
@@ -93,47 +85,39 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _createHubUseCase(
CreateHubArguments(
name: event.name,
address: event.address,
placeId: event.placeId,
latitude: event.latitude,
longitude: event.longitude,
city: event.city,
state: event.state,
street: event.street,
country: event.country,
zipCode: event.zipCode,
),
);
final List<Hub> hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub created successfully',
showAddHubDialog: false,
),
);
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
await _createHubUseCase(
CreateHubArguments(
name: event.name,
address: event.address,
placeId: event.placeId,
latitude: event.latitude,
longitude: event.longitude,
city: event.city,
state: event.state,
street: event.street,
country: event.country,
zipCode: event.zipCode,
),
);
final hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub created successfully',
showAddHubDialog: false,
),
);
},
onError: (errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
);
}
Future<void> _onDeleteRequested(
@@ -141,33 +125,25 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final List<Hub> hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub deleted successfully',
),
);
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
final hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'Hub deleted successfully',
),
);
},
onError: (errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
);
}
Future<void> _onNfcTagAssignRequested(
@@ -175,36 +151,28 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
Emitter<ClientHubsState> emit,
) async {
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
try {
await _assignNfcTagUseCase(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
);
final List<Hub> hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'NFC tag assigned successfully',
clearHubToIdentify: true,
),
);
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: 'errors.generic.unknown',
),
);
}
await handleError(
emit: emit,
action: () async {
await _assignNfcTagUseCase(
AssignNfcTagArguments(hubId: event.hubId, nfcTagId: event.nfcTagId),
);
final hubs = await _getHubsUseCase();
emit(
state.copyWith(
status: ClientHubsStatus.actionSuccess,
hubs: hubs,
successMessage: 'NFC tag assigned successfully',
clearHubToIdentify: true,
),
);
},
onError: (errorKey) => state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: errorKey,
),
);
}
void _onMessageCleared(