diff --git a/apps/mobile/packages/core_localization/lib/core_localization.dart b/apps/mobile/packages/core_localization/lib/core_localization.dart index 8c8c71fc..0b0bc657 100644 --- a/apps/mobile/packages/core_localization/lib/core_localization.dart +++ b/apps/mobile/packages/core_localization/lib/core_localization.dart @@ -8,3 +8,4 @@ export 'src/domain/usecases/set_locale_use_case.dart'; export 'src/data/repositories_impl/locale_repository_impl.dart'; export 'src/data/datasources/locale_local_data_source.dart'; export 'src/localization_module.dart'; +export 'src/utils/error_translator.dart'; diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index f42dd659..2aa6c54f 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -728,6 +728,60 @@ "paid": "Paid", "pending": "Pending" } + }, + "errors": { + "auth": { + "invalid_credentials": "The email or password you entered is incorrect.", + "account_exists": "An account with this email already exists. Try signing in instead.", + "session_expired": "Your session has expired. Please sign in again.", + "user_not_found": "We couldn't find your account. Please check your email and try again.", + "unauthorized_app": "This account is not authorized for this app.", + "weak_password": "Please choose a stronger password with at least 8 characters.", + "sign_up_failed": "We couldn't create your account. Please try again.", + "sign_in_failed": "We couldn't sign you in. Please try again.", + "not_authenticated": "Please sign in to continue.", + "password_mismatch": "This email is already registered. Please use the correct password or tap 'Forgot Password' to reset it.", + "google_only_account": "This email is registered via Google. Please use 'Forgot Password' to set a password, then try signing up again with the same information." + }, + "hub": { + "has_orders": "This hub has active orders and cannot be deleted.", + "not_found": "The hub you're looking for doesn't exist.", + "creation_failed": "We couldn't create the hub. Please try again." + }, + "order": { + "missing_hub": "Please select a location for your order.", + "missing_vendor": "Please select a vendor for your order.", + "creation_failed": "We couldn't create your order. Please try again.", + "shift_creation_failed": "We couldn't schedule the shift. Please try again.", + "missing_business": "Your business profile couldn't be loaded. Please sign in again." + }, + "profile": { + "staff_not_found": "Your profile couldn't be loaded. Please sign in again.", + "business_not_found": "Your business profile couldn't be loaded. Please sign in again.", + "update_failed": "We couldn't update your profile. Please try again." + }, + "shift": { + "no_open_roles": "There are no open positions available for this shift.", + "application_not_found": "Your application couldn't be found.", + "no_active_shift": "You don't have an active shift to clock out from." + }, + "generic": { + "unknown": "Something went wrong. Please try again.", + "no_connection": "No internet connection. Please check your network and try again." + } + }, + "success": { + "hub": { + "created": "Hub created successfully!", + "deleted": "Hub deleted successfully!", + "nfc_assigned": "NFC tag assigned successfully!" + }, + "order": { + "created": "Order created successfully!" + }, + "profile": { + "updated": "Profile updated successfully!" + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index dd6c6b3c..6e7e8f1c 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -727,5 +727,59 @@ "paid": "Pagado", "pending": "Pendiente" } + }, + "errors": { + "auth": { + "invalid_credentials": "El correo electrónico o la contraseña que ingresaste es incorrecta.", + "account_exists": "Ya existe una cuenta con este correo electrónico. Intenta iniciar sesión.", + "session_expired": "Tu sesión ha expirado. Por favor, inicia sesión de nuevo.", + "user_not_found": "No pudimos encontrar tu cuenta. Por favor, verifica tu correo electrónico e intenta de nuevo.", + "unauthorized_app": "Esta cuenta no está autorizada para esta aplicación.", + "weak_password": "Por favor, elige una contraseña más segura con al menos 8 caracteres.", + "sign_up_failed": "No pudimos crear tu cuenta. Por favor, intenta de nuevo.", + "sign_in_failed": "No pudimos iniciar sesión. Por favor, intenta de nuevo.", + "not_authenticated": "Por favor, inicia sesión para continuar.", + "password_mismatch": "Este correo ya está registrado. Por favor, usa la contraseña correcta o toca 'Olvidé mi contraseña' para restablecerla.", + "google_only_account": "Este correo está registrado con Google. Por favor, usa 'Olvidé mi contraseña' para establecer una contraseña, luego intenta registrarte de nuevo con la misma información." + }, + "hub": { + "has_orders": "Este hub tiene órdenes activas y no puede ser eliminado.", + "not_found": "El hub que buscas no existe.", + "creation_failed": "No pudimos crear el hub. Por favor, intenta de nuevo." + }, + "order": { + "missing_hub": "Por favor, selecciona una ubicación para tu orden.", + "missing_vendor": "Por favor, selecciona un proveedor para tu orden.", + "creation_failed": "No pudimos crear tu orden. Por favor, intenta de nuevo.", + "shift_creation_failed": "No pudimos programar el turno. Por favor, intenta de nuevo.", + "missing_business": "No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo." + }, + "profile": { + "staff_not_found": "No se pudo cargar tu perfil. Por favor, inicia sesión de nuevo.", + "business_not_found": "No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo.", + "update_failed": "No pudimos actualizar tu perfil. Por favor, intenta de nuevo." + }, + "shift": { + "no_open_roles": "No hay posiciones abiertas disponibles para este turno.", + "application_not_found": "No se pudo encontrar tu solicitud.", + "no_active_shift": "No tienes un turno activo para registrar salida." + }, + "generic": { + "unknown": "Algo salió mal. Por favor, intenta de nuevo.", + "no_connection": "Sin conexión a internet. Por favor, verifica tu red e intenta de nuevo." + } + }, + "success": { + "hub": { + "created": "¡Hub creado exitosamente!", + "deleted": "¡Hub eliminado exitosamente!", + "nfc_assigned": "¡Etiqueta NFC asignada exitosamente!" + }, + "order": { + "created": "¡Orden creada exitosamente!" + }, + "profile": { + "updated": "¡Perfil actualizado exitosamente!" + } } } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart index 8e28b106..08583076 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart +++ b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 2 -/// Strings: 1044 (522 per locale) +/// Strings: 1108 (554 per locale) /// -/// Built on 2026-01-31 at 13:17 UTC +/// Built on 2026-01-31 at 17:08 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart index 9b06837d..d56b70a7 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart +++ b/apps/mobile/packages/core_localization/lib/src/l10n/strings_en.g.dart @@ -57,6 +57,8 @@ class Translations with BaseTranslations { late final TranslationsStaffProfileAttireEn staff_profile_attire = TranslationsStaffProfileAttireEn._(_root); late final TranslationsStaffShiftsEn staff_shifts = TranslationsStaffShiftsEn._(_root); late final TranslationsStaffTimeCardEn staff_time_card = TranslationsStaffTimeCardEn._(_root); + late final TranslationsErrorsEn errors = TranslationsErrorsEn._(_root); + late final TranslationsSuccessEn success = TranslationsSuccessEn._(_root); } // Path: common @@ -420,6 +422,33 @@ class TranslationsStaffTimeCardEn { late final TranslationsStaffTimeCardStatusEn status = TranslationsStaffTimeCardStatusEn._(_root); } +// Path: errors +class TranslationsErrorsEn { + TranslationsErrorsEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsErrorsAuthEn auth = TranslationsErrorsAuthEn._(_root); + late final TranslationsErrorsHubEn hub = TranslationsErrorsHubEn._(_root); + late final TranslationsErrorsOrderEn order = TranslationsErrorsOrderEn._(_root); + late final TranslationsErrorsProfileEn profile = TranslationsErrorsProfileEn._(_root); + late final TranslationsErrorsShiftEn shift = TranslationsErrorsShiftEn._(_root); + late final TranslationsErrorsGenericEn generic = TranslationsErrorsGenericEn._(_root); +} + +// Path: success +class TranslationsSuccessEn { + TranslationsSuccessEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + late final TranslationsSuccessHubEn hub = TranslationsSuccessHubEn._(_root); + late final TranslationsSuccessOrderEn order = TranslationsSuccessOrderEn._(_root); + late final TranslationsSuccessProfileEn profile = TranslationsSuccessProfileEn._(_root); +} + // Path: staff_authentication.get_started_page class TranslationsStaffAuthenticationGetStartedPageEn { TranslationsStaffAuthenticationGetStartedPageEn._(this._root); @@ -1745,6 +1774,183 @@ class TranslationsStaffTimeCardStatusEn { String get pending => 'Pending'; } +// Path: errors.auth +class TranslationsErrorsAuthEn { + TranslationsErrorsAuthEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'The email or password you entered is incorrect.' + String get invalid_credentials => 'The email or password you entered is incorrect.'; + + /// en: 'An account with this email already exists. Try signing in instead.' + String get account_exists => 'An account with this email already exists. Try signing in instead.'; + + /// en: 'Your session has expired. Please sign in again.' + String get session_expired => 'Your session has expired. Please sign in again.'; + + /// en: 'We couldn't find your account. Please check your email and try again.' + String get user_not_found => 'We couldn\'t find your account. Please check your email and try again.'; + + /// en: 'This account is not authorized for this app.' + String get unauthorized_app => 'This account is not authorized for this app.'; + + /// en: 'Please choose a stronger password with at least 8 characters.' + String get weak_password => 'Please choose a stronger password with at least 8 characters.'; + + /// en: 'We couldn't create your account. Please try again.' + String get sign_up_failed => 'We couldn\'t create your account. Please try again.'; + + /// en: 'We couldn't sign you in. Please try again.' + String get sign_in_failed => 'We couldn\'t sign you in. Please try again.'; + + /// en: 'Please sign in to continue.' + String get not_authenticated => 'Please sign in to continue.'; + + /// en: 'This email is already registered. Please use the correct password or tap 'Forgot Password' to reset it.' + String get password_mismatch => 'This email is already registered. Please use the correct password or tap \'Forgot Password\' to reset it.'; + + /// en: 'This email is registered via Google. Please use 'Forgot Password' to set a password, then try signing up again with the same information.' + String get google_only_account => 'This email is registered via Google. Please use \'Forgot Password\' to set a password, then try signing up again with the same information.'; +} + +// Path: errors.hub +class TranslationsErrorsHubEn { + TranslationsErrorsHubEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'This hub has active orders and cannot be deleted.' + String get has_orders => 'This hub has active orders and cannot be deleted.'; + + /// en: 'The hub you're looking for doesn't exist.' + String get not_found => 'The hub you\'re looking for doesn\'t exist.'; + + /// en: 'We couldn't create the hub. Please try again.' + String get creation_failed => 'We couldn\'t create the hub. Please try again.'; +} + +// Path: errors.order +class TranslationsErrorsOrderEn { + TranslationsErrorsOrderEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Please select a location for your order.' + String get missing_hub => 'Please select a location for your order.'; + + /// en: 'Please select a vendor for your order.' + String get missing_vendor => 'Please select a vendor for your order.'; + + /// en: 'We couldn't create your order. Please try again.' + String get creation_failed => 'We couldn\'t create your order. Please try again.'; + + /// en: 'We couldn't schedule the shift. Please try again.' + String get shift_creation_failed => 'We couldn\'t schedule the shift. Please try again.'; + + /// en: 'Your business profile couldn't be loaded. Please sign in again.' + String get missing_business => 'Your business profile couldn\'t be loaded. Please sign in again.'; +} + +// Path: errors.profile +class TranslationsErrorsProfileEn { + TranslationsErrorsProfileEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Your profile couldn't be loaded. Please sign in again.' + String get staff_not_found => 'Your profile couldn\'t be loaded. Please sign in again.'; + + /// en: 'Your business profile couldn't be loaded. Please sign in again.' + String get business_not_found => 'Your business profile couldn\'t be loaded. Please sign in again.'; + + /// en: 'We couldn't update your profile. Please try again.' + String get update_failed => 'We couldn\'t update your profile. Please try again.'; +} + +// Path: errors.shift +class TranslationsErrorsShiftEn { + TranslationsErrorsShiftEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'There are no open positions available for this shift.' + String get no_open_roles => 'There are no open positions available for this shift.'; + + /// en: 'Your application couldn't be found.' + String get application_not_found => 'Your application couldn\'t be found.'; + + /// en: 'You don't have an active shift to clock out from.' + String get no_active_shift => 'You don\'t have an active shift to clock out from.'; +} + +// Path: errors.generic +class TranslationsErrorsGenericEn { + TranslationsErrorsGenericEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Something went wrong. Please try again.' + String get unknown => 'Something went wrong. Please try again.'; + + /// en: 'No internet connection. Please check your network and try again.' + String get no_connection => 'No internet connection. Please check your network and try again.'; +} + +// Path: success.hub +class TranslationsSuccessHubEn { + TranslationsSuccessHubEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Hub created successfully!' + String get created => 'Hub created successfully!'; + + /// en: 'Hub deleted successfully!' + String get deleted => 'Hub deleted successfully!'; + + /// en: 'NFC tag assigned successfully!' + String get nfc_assigned => 'NFC tag assigned successfully!'; +} + +// Path: success.order +class TranslationsSuccessOrderEn { + TranslationsSuccessOrderEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Order created successfully!' + String get created => 'Order created successfully!'; +} + +// Path: success.profile +class TranslationsSuccessProfileEn { + TranslationsSuccessProfileEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Profile updated successfully!' + String get updated => 'Profile updated successfully!'; +} + // Path: staff_authentication.profile_setup_page.steps class TranslationsStaffAuthenticationProfileSetupPageStepsEn { TranslationsStaffAuthenticationProfileSetupPageStepsEn._(this._root); @@ -3216,6 +3422,38 @@ extension on Translations { 'staff_time_card.status.disputed' => 'Disputed', 'staff_time_card.status.paid' => 'Paid', 'staff_time_card.status.pending' => 'Pending', + 'errors.auth.invalid_credentials' => 'The email or password you entered is incorrect.', + 'errors.auth.account_exists' => 'An account with this email already exists. Try signing in instead.', + 'errors.auth.session_expired' => 'Your session has expired. Please sign in again.', + 'errors.auth.user_not_found' => 'We couldn\'t find your account. Please check your email and try again.', + 'errors.auth.unauthorized_app' => 'This account is not authorized for this app.', + 'errors.auth.weak_password' => 'Please choose a stronger password with at least 8 characters.', + 'errors.auth.sign_up_failed' => 'We couldn\'t create your account. Please try again.', + 'errors.auth.sign_in_failed' => 'We couldn\'t sign you in. Please try again.', + 'errors.auth.not_authenticated' => 'Please sign in to continue.', + 'errors.auth.password_mismatch' => 'This email is already registered. Please use the correct password or tap \'Forgot Password\' to reset it.', + 'errors.auth.google_only_account' => 'This email is registered via Google. Please use \'Forgot Password\' to set a password, then try signing up again with the same information.', + 'errors.hub.has_orders' => 'This hub has active orders and cannot be deleted.', + 'errors.hub.not_found' => 'The hub you\'re looking for doesn\'t exist.', + 'errors.hub.creation_failed' => 'We couldn\'t create the hub. Please try again.', + 'errors.order.missing_hub' => 'Please select a location for your order.', + 'errors.order.missing_vendor' => 'Please select a vendor for your order.', + 'errors.order.creation_failed' => 'We couldn\'t create your order. Please try again.', + 'errors.order.shift_creation_failed' => 'We couldn\'t schedule the shift. Please try again.', + 'errors.order.missing_business' => 'Your business profile couldn\'t be loaded. Please sign in again.', + 'errors.profile.staff_not_found' => 'Your profile couldn\'t be loaded. Please sign in again.', + 'errors.profile.business_not_found' => 'Your business profile couldn\'t be loaded. Please sign in again.', + 'errors.profile.update_failed' => 'We couldn\'t update your profile. Please try again.', + 'errors.shift.no_open_roles' => 'There are no open positions available for this shift.', + 'errors.shift.application_not_found' => 'Your application couldn\'t be found.', + 'errors.shift.no_active_shift' => 'You don\'t have an active shift to clock out from.', + 'errors.generic.unknown' => 'Something went wrong. Please try again.', + 'errors.generic.no_connection' => 'No internet connection. Please check your network and try again.', + 'success.hub.created' => 'Hub created successfully!', + 'success.hub.deleted' => 'Hub deleted successfully!', + 'success.hub.nfc_assigned' => 'NFC tag assigned successfully!', + 'success.order.created' => 'Order created successfully!', + 'success.profile.updated' => 'Profile updated successfully!', _ => null, }; } diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart index 7dfd4009..6c9c6b03 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart +++ b/apps/mobile/packages/core_localization/lib/src/l10n/strings_es.g.dart @@ -54,6 +54,8 @@ class TranslationsEs with BaseTranslations implements T @override late final _TranslationsStaffProfileAttireEs staff_profile_attire = _TranslationsStaffProfileAttireEs._(_root); @override late final _TranslationsStaffShiftsEs staff_shifts = _TranslationsStaffShiftsEs._(_root); @override late final _TranslationsStaffTimeCardEs staff_time_card = _TranslationsStaffTimeCardEs._(_root); + @override late final _TranslationsErrorsEs errors = _TranslationsErrorsEs._(_root); + @override late final _TranslationsSuccessEs success = _TranslationsSuccessEs._(_root); } // Path: common @@ -310,6 +312,33 @@ class _TranslationsStaffTimeCardEs implements TranslationsStaffTimeCardEn { @override late final _TranslationsStaffTimeCardStatusEs status = _TranslationsStaffTimeCardStatusEs._(_root); } +// Path: errors +class _TranslationsErrorsEs implements TranslationsErrorsEn { + _TranslationsErrorsEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final _TranslationsErrorsAuthEs auth = _TranslationsErrorsAuthEs._(_root); + @override late final _TranslationsErrorsHubEs hub = _TranslationsErrorsHubEs._(_root); + @override late final _TranslationsErrorsOrderEs order = _TranslationsErrorsOrderEs._(_root); + @override late final _TranslationsErrorsProfileEs profile = _TranslationsErrorsProfileEs._(_root); + @override late final _TranslationsErrorsShiftEs shift = _TranslationsErrorsShiftEs._(_root); + @override late final _TranslationsErrorsGenericEs generic = _TranslationsErrorsGenericEs._(_root); +} + +// Path: success +class _TranslationsSuccessEs implements TranslationsSuccessEn { + _TranslationsSuccessEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override late final _TranslationsSuccessHubEs hub = _TranslationsSuccessHubEs._(_root); + @override late final _TranslationsSuccessOrderEs order = _TranslationsSuccessOrderEs._(_root); + @override late final _TranslationsSuccessProfileEs profile = _TranslationsSuccessProfileEs._(_root); +} + // Path: staff_authentication.get_started_page class _TranslationsStaffAuthenticationGetStartedPageEs implements TranslationsStaffAuthenticationGetStartedPageEn { _TranslationsStaffAuthenticationGetStartedPageEs._(this._root); @@ -1080,6 +1109,119 @@ class _TranslationsStaffTimeCardStatusEs implements TranslationsStaffTimeCardSta @override String get pending => 'Pendiente'; } +// Path: errors.auth +class _TranslationsErrorsAuthEs implements TranslationsErrorsAuthEn { + _TranslationsErrorsAuthEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get invalid_credentials => 'El correo electrónico o la contraseña que ingresaste es incorrecta.'; + @override String get account_exists => 'Ya existe una cuenta con este correo electrónico. Intenta iniciar sesión.'; + @override String get session_expired => 'Tu sesión ha expirado. Por favor, inicia sesión de nuevo.'; + @override String get user_not_found => 'No pudimos encontrar tu cuenta. Por favor, verifica tu correo electrónico e intenta de nuevo.'; + @override String get unauthorized_app => 'Esta cuenta no está autorizada para esta aplicación.'; + @override String get weak_password => 'Por favor, elige una contraseña más segura con al menos 8 caracteres.'; + @override String get sign_up_failed => 'No pudimos crear tu cuenta. Por favor, intenta de nuevo.'; + @override String get sign_in_failed => 'No pudimos iniciar sesión. Por favor, intenta de nuevo.'; + @override String get not_authenticated => 'Por favor, inicia sesión para continuar.'; + @override String get password_mismatch => 'Este correo ya está registrado. Por favor, usa la contraseña correcta o toca \'Olvidé mi contraseña\' para restablecerla.'; + @override String get google_only_account => 'Este correo está registrado con Google. Por favor, usa \'Olvidé mi contraseña\' para establecer una contraseña, luego intenta registrarte de nuevo con la misma información.'; +} + +// Path: errors.hub +class _TranslationsErrorsHubEs implements TranslationsErrorsHubEn { + _TranslationsErrorsHubEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get has_orders => 'Este hub tiene órdenes activas y no puede ser eliminado.'; + @override String get not_found => 'El hub que buscas no existe.'; + @override String get creation_failed => 'No pudimos crear el hub. Por favor, intenta de nuevo.'; +} + +// Path: errors.order +class _TranslationsErrorsOrderEs implements TranslationsErrorsOrderEn { + _TranslationsErrorsOrderEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get missing_hub => 'Por favor, selecciona una ubicación para tu orden.'; + @override String get missing_vendor => 'Por favor, selecciona un proveedor para tu orden.'; + @override String get creation_failed => 'No pudimos crear tu orden. Por favor, intenta de nuevo.'; + @override String get shift_creation_failed => 'No pudimos programar el turno. Por favor, intenta de nuevo.'; + @override String get missing_business => 'No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo.'; +} + +// Path: errors.profile +class _TranslationsErrorsProfileEs implements TranslationsErrorsProfileEn { + _TranslationsErrorsProfileEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get staff_not_found => 'No se pudo cargar tu perfil. Por favor, inicia sesión de nuevo.'; + @override String get business_not_found => 'No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo.'; + @override String get update_failed => 'No pudimos actualizar tu perfil. Por favor, intenta de nuevo.'; +} + +// Path: errors.shift +class _TranslationsErrorsShiftEs implements TranslationsErrorsShiftEn { + _TranslationsErrorsShiftEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get no_open_roles => 'No hay posiciones abiertas disponibles para este turno.'; + @override String get application_not_found => 'No se pudo encontrar tu solicitud.'; + @override String get no_active_shift => 'No tienes un turno activo para registrar salida.'; +} + +// Path: errors.generic +class _TranslationsErrorsGenericEs implements TranslationsErrorsGenericEn { + _TranslationsErrorsGenericEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get unknown => 'Algo salió mal. Por favor, intenta de nuevo.'; + @override String get no_connection => 'Sin conexión a internet. Por favor, verifica tu red e intenta de nuevo.'; +} + +// Path: success.hub +class _TranslationsSuccessHubEs implements TranslationsSuccessHubEn { + _TranslationsSuccessHubEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get created => '¡Hub creado exitosamente!'; + @override String get deleted => '¡Hub eliminado exitosamente!'; + @override String get nfc_assigned => '¡Etiqueta NFC asignada exitosamente!'; +} + +// Path: success.order +class _TranslationsSuccessOrderEs implements TranslationsSuccessOrderEn { + _TranslationsSuccessOrderEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get created => '¡Orden creada exitosamente!'; +} + +// Path: success.profile +class _TranslationsSuccessProfileEs implements TranslationsSuccessProfileEn { + _TranslationsSuccessProfileEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get updated => '¡Perfil actualizado exitosamente!'; +} + // Path: staff_authentication.profile_setup_page.steps class _TranslationsStaffAuthenticationProfileSetupPageStepsEs implements TranslationsStaffAuthenticationProfileSetupPageStepsEn { _TranslationsStaffAuthenticationProfileSetupPageStepsEs._(this._root); @@ -2153,6 +2295,38 @@ extension on TranslationsEs { 'staff_time_card.status.disputed' => 'Disputado', 'staff_time_card.status.paid' => 'Pagado', 'staff_time_card.status.pending' => 'Pendiente', + 'errors.auth.invalid_credentials' => 'El correo electrónico o la contraseña que ingresaste es incorrecta.', + 'errors.auth.account_exists' => 'Ya existe una cuenta con este correo electrónico. Intenta iniciar sesión.', + 'errors.auth.session_expired' => 'Tu sesión ha expirado. Por favor, inicia sesión de nuevo.', + 'errors.auth.user_not_found' => 'No pudimos encontrar tu cuenta. Por favor, verifica tu correo electrónico e intenta de nuevo.', + 'errors.auth.unauthorized_app' => 'Esta cuenta no está autorizada para esta aplicación.', + 'errors.auth.weak_password' => 'Por favor, elige una contraseña más segura con al menos 8 caracteres.', + 'errors.auth.sign_up_failed' => 'No pudimos crear tu cuenta. Por favor, intenta de nuevo.', + 'errors.auth.sign_in_failed' => 'No pudimos iniciar sesión. Por favor, intenta de nuevo.', + 'errors.auth.not_authenticated' => 'Por favor, inicia sesión para continuar.', + 'errors.auth.password_mismatch' => 'Este correo ya está registrado. Por favor, usa la contraseña correcta o toca \'Olvidé mi contraseña\' para restablecerla.', + 'errors.auth.google_only_account' => 'Este correo está registrado con Google. Por favor, usa \'Olvidé mi contraseña\' para establecer una contraseña, luego intenta registrarte de nuevo con la misma información.', + 'errors.hub.has_orders' => 'Este hub tiene órdenes activas y no puede ser eliminado.', + 'errors.hub.not_found' => 'El hub que buscas no existe.', + 'errors.hub.creation_failed' => 'No pudimos crear el hub. Por favor, intenta de nuevo.', + 'errors.order.missing_hub' => 'Por favor, selecciona una ubicación para tu orden.', + 'errors.order.missing_vendor' => 'Por favor, selecciona un proveedor para tu orden.', + 'errors.order.creation_failed' => 'No pudimos crear tu orden. Por favor, intenta de nuevo.', + 'errors.order.shift_creation_failed' => 'No pudimos programar el turno. Por favor, intenta de nuevo.', + 'errors.order.missing_business' => 'No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo.', + 'errors.profile.staff_not_found' => 'No se pudo cargar tu perfil. Por favor, inicia sesión de nuevo.', + 'errors.profile.business_not_found' => 'No se pudo cargar tu perfil de empresa. Por favor, inicia sesión de nuevo.', + 'errors.profile.update_failed' => 'No pudimos actualizar tu perfil. Por favor, intenta de nuevo.', + 'errors.shift.no_open_roles' => 'No hay posiciones abiertas disponibles para este turno.', + 'errors.shift.application_not_found' => 'No se pudo encontrar tu solicitud.', + 'errors.shift.no_active_shift' => 'No tienes un turno activo para registrar salida.', + 'errors.generic.unknown' => 'Algo salió mal. Por favor, intenta de nuevo.', + 'errors.generic.no_connection' => 'Sin conexión a internet. Por favor, verifica tu red e intenta de nuevo.', + 'success.hub.created' => '¡Hub creado exitosamente!', + 'success.hub.deleted' => '¡Hub eliminado exitosamente!', + 'success.hub.nfc_assigned' => '¡Etiqueta NFC asignada exitosamente!', + 'success.order.created' => '¡Orden creada exitosamente!', + 'success.profile.updated' => '¡Perfil actualizado exitosamente!', _ => null, }; } diff --git a/apps/mobile/packages/core_localization/lib/src/utils/error_translator.dart b/apps/mobile/packages/core_localization/lib/src/utils/error_translator.dart new file mode 100644 index 00000000..5e7df68d --- /dev/null +++ b/apps/mobile/packages/core_localization/lib/src/utils/error_translator.dart @@ -0,0 +1,139 @@ +import '../l10n/strings.g.dart'; + +/// Translates error message keys to localized strings. +/// +/// This utility function takes a dot-notation key like 'errors.auth.account_exists' +/// and returns the corresponding localized string from the translation system. +/// +/// If the key is not found or doesn't match the expected format, the original +/// key is returned as a fallback. +/// +/// Example: +/// ```dart +/// final message = translateErrorKey('errors.auth.account_exists'); +/// // Returns: "An account with this email already exists. Try signing in instead." +/// ``` +String translateErrorKey(String key) { + final List parts = key.split('.'); + + // Expected format: errors.{category}.{error_type} + if (parts.length != 3 || parts[0] != 'errors') { + return key; + } + + final String category = parts[1]; + final String errorType = parts[2]; + + switch (category) { + case 'auth': + return _translateAuthError(errorType); + case 'hub': + return _translateHubError(errorType); + case 'order': + return _translateOrderError(errorType); + case 'profile': + return _translateProfileError(errorType); + case 'shift': + return _translateShiftError(errorType); + case 'generic': + return _translateGenericError(errorType); + default: + return key; + } +} + +String _translateAuthError(String errorType) { + switch (errorType) { + case 'invalid_credentials': + return t.errors.auth.invalid_credentials; + case 'account_exists': + return t.errors.auth.account_exists; + case 'session_expired': + return t.errors.auth.session_expired; + case 'user_not_found': + return t.errors.auth.user_not_found; + case 'unauthorized_app': + return t.errors.auth.unauthorized_app; + case 'weak_password': + return t.errors.auth.weak_password; + case 'sign_up_failed': + return t.errors.auth.sign_up_failed; + case 'sign_in_failed': + return t.errors.auth.sign_in_failed; + case 'not_authenticated': + return t.errors.auth.not_authenticated; + case 'password_mismatch': + return t.errors.auth.password_mismatch; + case 'google_only_account': + return t.errors.auth.google_only_account; + default: + return t.errors.generic.unknown; + } +} + +String _translateHubError(String errorType) { + switch (errorType) { + case 'has_orders': + return t.errors.hub.has_orders; + case 'not_found': + return t.errors.hub.not_found; + case 'creation_failed': + return t.errors.hub.creation_failed; + default: + return t.errors.generic.unknown; + } +} + +String _translateOrderError(String errorType) { + switch (errorType) { + case 'missing_hub': + return t.errors.order.missing_hub; + case 'missing_vendor': + return t.errors.order.missing_vendor; + case 'creation_failed': + return t.errors.order.creation_failed; + case 'shift_creation_failed': + return t.errors.order.shift_creation_failed; + case 'missing_business': + return t.errors.order.missing_business; + default: + return t.errors.generic.unknown; + } +} + +String _translateProfileError(String errorType) { + switch (errorType) { + case 'staff_not_found': + return t.errors.profile.staff_not_found; + case 'business_not_found': + return t.errors.profile.business_not_found; + case 'update_failed': + return t.errors.profile.update_failed; + default: + return t.errors.generic.unknown; + } +} + +String _translateShiftError(String errorType) { + switch (errorType) { + case 'no_open_roles': + return t.errors.shift.no_open_roles; + case 'application_not_found': + return t.errors.shift.application_not_found; + case 'no_active_shift': + return t.errors.shift.no_active_shift; + default: + return t.errors.generic.unknown; + } +} + +String _translateGenericError(String errorType) { + switch (errorType) { + case 'unknown': + return t.errors.generic.unknown; + case 'no_connection': + return t.errors.generic.no_connection; + default: + return t.errors.generic.unknown; + } +} diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index df3a825c..fc763e64 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -94,3 +94,6 @@ export 'src/entities/profile/experience_skill.dart'; export 'src/adapters/profile/bank_account_adapter.dart'; export 'src/adapters/profile/tax_form_adapter.dart'; export 'src/adapters/financial/payment_adapter.dart'; + +// Exceptions +export 'src/exceptions/app_exception.dart'; diff --git a/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart b/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart new file mode 100644 index 00000000..bf9ae481 --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/exceptions/app_exception.dart @@ -0,0 +1,314 @@ +/// Base sealed class for all application exceptions. +/// +/// Provides type-safe error handling with user-friendly message keys. +/// Technical details are captured for logging but never shown to users. +sealed class AppException implements Exception { + const AppException({ + required this.code, + this.technicalMessage, + }); + + /// Unique error code for logging/tracking (e.g., "AUTH_001") + final String code; + + /// Technical details for developers (never shown to users) + final String? technicalMessage; + + /// Returns the localization key for user-friendly message + String get messageKey; + + @override + String toString() => 'AppException($code): $technicalMessage'; +} + +// ============================================================ +// AUTH EXCEPTIONS +// ============================================================ + +/// Base class for authentication-related exceptions. +sealed class AuthException extends AppException { + const AuthException({required super.code, super.technicalMessage}); +} + +/// Thrown when email/password combination is incorrect. +class InvalidCredentialsException extends AuthException { + const InvalidCredentialsException({String? technicalMessage}) + : super(code: 'AUTH_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.invalid_credentials'; +} + +/// Thrown when attempting to register with an email that already exists. +class AccountExistsException extends AuthException { + const AccountExistsException({String? technicalMessage}) + : super(code: 'AUTH_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.account_exists'; +} + +/// Thrown when the user session has expired. +class SessionExpiredException extends AuthException { + const SessionExpiredException({String? technicalMessage}) + : super(code: 'AUTH_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.session_expired'; +} + +/// Thrown when user profile is not found in database after Firebase auth. +class UserNotFoundException extends AuthException { + const UserNotFoundException({String? technicalMessage}) + : super(code: 'AUTH_004', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.user_not_found'; +} + +/// Thrown when user is not authorized for the current app (wrong role). +class UnauthorizedAppException extends AuthException { + const UnauthorizedAppException({String? technicalMessage}) + : super(code: 'AUTH_005', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.unauthorized_app'; +} + +/// Thrown when password doesn't meet security requirements. +class WeakPasswordException extends AuthException { + const WeakPasswordException({String? technicalMessage}) + : super(code: 'AUTH_006', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.weak_password'; +} + +/// Thrown when sign-up process fails. +class SignUpFailedException extends AuthException { + const SignUpFailedException({String? technicalMessage}) + : super(code: 'AUTH_007', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.sign_up_failed'; +} + +/// Thrown when sign-in process fails. +class SignInFailedException extends AuthException { + const SignInFailedException({String? technicalMessage}) + : super(code: 'AUTH_008', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.sign_in_failed'; +} + +/// Thrown when email exists but password doesn't match. +class PasswordMismatchException extends AuthException { + const PasswordMismatchException({String? technicalMessage}) + : super(code: 'AUTH_009', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.password_mismatch'; +} + +/// Thrown when account exists only with Google provider (no password). +class GoogleOnlyAccountException extends AuthException { + const GoogleOnlyAccountException({String? technicalMessage}) + : super(code: 'AUTH_010', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.google_only_account'; +} + +// ============================================================ +// HUB EXCEPTIONS +// ============================================================ + +/// Base class for hub-related exceptions. +sealed class HubException extends AppException { + const HubException({required super.code, super.technicalMessage}); +} + +/// Thrown when attempting to delete a hub that has active orders. +class HubHasOrdersException extends HubException { + const HubHasOrdersException({String? technicalMessage}) + : super(code: 'HUB_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.has_orders'; +} + +/// Thrown when hub is not found. +class HubNotFoundException extends HubException { + const HubNotFoundException({String? technicalMessage}) + : super(code: 'HUB_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.not_found'; +} + +/// Thrown when hub creation fails. +class HubCreationFailedException extends HubException { + const HubCreationFailedException({String? technicalMessage}) + : super(code: 'HUB_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.hub.creation_failed'; +} + +// ============================================================ +// ORDER EXCEPTIONS +// ============================================================ + +/// Base class for order-related exceptions. +sealed class OrderException extends AppException { + const OrderException({required super.code, super.technicalMessage}); +} + +/// Thrown when order creation is attempted without a hub. +class OrderMissingHubException extends OrderException { + const OrderMissingHubException({String? technicalMessage}) + : super(code: 'ORDER_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_hub'; +} + +/// Thrown when order creation is attempted without a vendor. +class OrderMissingVendorException extends OrderException { + const OrderMissingVendorException({String? technicalMessage}) + : super(code: 'ORDER_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_vendor'; +} + +/// Thrown when order creation fails. +class OrderCreationFailedException extends OrderException { + const OrderCreationFailedException({String? technicalMessage}) + : super(code: 'ORDER_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.creation_failed'; +} + +/// Thrown when shift creation fails. +class ShiftCreationFailedException extends OrderException { + const ShiftCreationFailedException({String? technicalMessage}) + : super(code: 'ORDER_004', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.shift_creation_failed'; +} + +/// Thrown when order is missing required business context. +class OrderMissingBusinessException extends OrderException { + const OrderMissingBusinessException({String? technicalMessage}) + : super(code: 'ORDER_005', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.order.missing_business'; +} + +// ============================================================ +// PROFILE EXCEPTIONS +// ============================================================ + +/// Base class for profile-related exceptions. +sealed class ProfileException extends AppException { + const ProfileException({required super.code, super.technicalMessage}); +} + +/// Thrown when staff profile is not found. +class StaffProfileNotFoundException extends ProfileException { + const StaffProfileNotFoundException({String? technicalMessage}) + : super(code: 'PROFILE_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.staff_not_found'; +} + +/// Thrown when business profile is not found. +class BusinessNotFoundException extends ProfileException { + const BusinessNotFoundException({String? technicalMessage}) + : super(code: 'PROFILE_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.business_not_found'; +} + +/// Thrown when profile update fails. +class ProfileUpdateFailedException extends ProfileException { + const ProfileUpdateFailedException({String? technicalMessage}) + : super(code: 'PROFILE_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.profile.update_failed'; +} + +// ============================================================ +// SHIFT EXCEPTIONS +// ============================================================ + +/// Base class for shift-related exceptions. +sealed class ShiftException extends AppException { + const ShiftException({required super.code, super.technicalMessage}); +} + +/// Thrown when no open roles are available for a shift. +class NoOpenRolesException extends ShiftException { + const NoOpenRolesException({String? technicalMessage}) + : super(code: 'SHIFT_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.shift.no_open_roles'; +} + +/// Thrown when application for shift is not found. +class ApplicationNotFoundException extends ShiftException { + const ApplicationNotFoundException({String? technicalMessage}) + : super(code: 'SHIFT_002', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.shift.application_not_found'; +} + +/// Thrown when no active shift is found for clock out. +class NoActiveShiftException extends ShiftException { + const NoActiveShiftException({String? technicalMessage}) + : super(code: 'SHIFT_003', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.shift.no_active_shift'; +} + +// ============================================================ +// NETWORK/GENERIC EXCEPTIONS +// ============================================================ + +/// Thrown when there is no network connection. +class NetworkException extends AppException { + const NetworkException({String? technicalMessage}) + : super(code: 'NET_001', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.no_connection'; +} + +/// Thrown when an unexpected error occurs. +class UnknownException extends AppException { + const UnknownException({String? technicalMessage}) + : super(code: 'UNKNOWN', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.generic.unknown'; +} + +/// Thrown when user is not authenticated. +class NotAuthenticatedException extends AppException { + const NotAuthenticatedException({String? technicalMessage}) + : super(code: 'AUTH_NOT_LOGGED', technicalMessage: technicalMessage); + + @override + String get messageKey => 'errors.auth.not_authenticated'; +} diff --git a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart index c6831eee..43674a96 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart @@ -1,7 +1,20 @@ +import 'dart:developer' as developer; + import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; +import 'package:krow_domain/krow_domain.dart' + show + InvalidCredentialsException, + SignInFailedException, + SignUpFailedException, + WeakPasswordException, + AccountExistsException, + UserNotFoundException, + UnauthorizedAppException, + PasswordMismatchException, + GoogleOnlyAccountException; import '../../domain/repositories/auth_repository_interface.dart'; @@ -33,7 +46,9 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { final firebase.User? firebaseUser = credential.user; if (firebaseUser == null) { - throw Exception('Sign-in failed, no Firebase user received.'); + throw const SignInFailedException( + technicalMessage: 'No Firebase user received after sign-in', + ); } return _getUserProfile( @@ -44,12 +59,20 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { } on firebase.FirebaseAuthException catch (e) { if (e.code == 'invalid-credential' || e.code == 'wrong-password') { - throw Exception('Incorrect email or password.'); + throw InvalidCredentialsException( + technicalMessage: 'Firebase error code: ${e.code}', + ); } else { - throw Exception('Authentication error: ${e.message}'); + throw SignInFailedException( + technicalMessage: 'Firebase auth error: ${e.message}', + ); } + } on domain.AppException { + rethrow; } catch (e) { - throw Exception('Failed to sign in and fetch user data: ${e.toString()}'); + throw SignInFailedException( + technicalMessage: 'Unexpected error: $e', + ); } } @@ -59,63 +82,225 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { required String email, required String password, }) async { + firebase.User? firebaseUser; + String? createdBusinessId; + try { + // Step 1: Try to create Firebase Auth user final firebase.UserCredential credential = await _firebaseAuth.createUserWithEmailAndPassword( email: email, password: password, ); + firebaseUser = credential.user; + if (firebaseUser == null) { + throw const SignUpFailedException( + technicalMessage: 'Firebase user could not be created', + ); + } + + // New user created successfully, proceed to create PostgreSQL entities + return await _createBusinessAndUser( + firebaseUser: firebaseUser, + companyName: companyName, + email: email, + onBusinessCreated: (String businessId) => createdBusinessId = businessId, + ); + + } on firebase.FirebaseAuthException catch (e) { + if (e.code == 'weak-password') { + throw WeakPasswordException( + technicalMessage: 'Firebase: ${e.message}', + ); + } else if (e.code == 'email-already-in-use') { + // Email exists in Firebase Auth - try to sign in and complete registration + return await _handleExistingFirebaseAccount( + email: email, + password: password, + companyName: companyName, + ); + } else { + throw SignUpFailedException( + technicalMessage: 'Firebase auth error: ${e.message}', + ); + } + } on domain.AppException { + // Rollback for our known exceptions + await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); + rethrow; + } catch (e) { + // Rollback: Clean up any partially created resources + await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId); + throw SignUpFailedException( + technicalMessage: 'Unexpected error: $e', + ); + } + } + + /// Handles the case where email already exists in Firebase Auth. + /// + /// This can happen when: + /// 1. User signed up with Google in another app sharing the same Firebase project + /// 2. User already has a KROW account + /// + /// The flow: + /// 1. Try to sign in with provided password + /// 2. If sign-in succeeds, check if BUSINESS user exists in PostgreSQL + /// 3. If not, create Business + User (user is new to KROW) + /// 4. If yes, they already have a KROW account + Future _handleExistingFirebaseAccount({ + required String email, + required String password, + required String companyName, + }) async { + developer.log('Email exists in Firebase, attempting sign-in: $email', name: 'AuthRepository'); + + try { + // Try to sign in with the provided password + final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword( + email: email, + password: password, + ); + final firebase.User? firebaseUser = credential.user; if (firebaseUser == null) { - throw Exception('Sign-up failed, Firebase user could not be created.'); + throw const SignUpFailedException( + technicalMessage: 'Sign-in succeeded but no user returned', + ); } - // Client-specific business logic: - // 1. Create a `Business` entity. - // 2. Create a `User` entity associated with the business. - final OperationResult createBusinessResponse = await _dataConnect.createBusiness( - businessName: companyName, - userId: firebaseUser.uid, - rateGroup: dc.BusinessRateGroup.STANDARD, - status: dc.BusinessStatus.PENDING, - ).execute(); + // Sign-in succeeded! Check if user already has a BUSINESS account in PostgreSQL + final bool hasBusinessAccount = await _checkBusinessUserExists(firebaseUser.uid); - final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert; - if (businessData == null) { - await firebaseUser.delete(); // Rollback if business creation fails - throw Exception('Business creation failed after Firebase user registration.'); + if (hasBusinessAccount) { + // User already has a KROW Client account + developer.log('User already has BUSINESS account: ${firebaseUser.uid}', name: 'AuthRepository'); + throw AccountExistsException( + technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role', + ); } - final OperationResult createUserResponse = await _dataConnect.createUser( - id: firebaseUser.uid, - role: dc.UserBaseRole.USER, - ) - .email(email) - .userRole('BUSINESS') - .execute(); - - final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert; - if (newUserData == null) { - await firebaseUser.delete(); // Rollback if user profile creation fails - // TO-DO: Also delete the created Business if this fails - throw Exception('User profile creation failed after Firebase user registration.'); - } - - return _getUserProfile( - firebaseUserId: firebaseUser.uid, - fallbackEmail: firebaseUser.email ?? email, + // User exists in Firebase but not in KROW PostgreSQL - create the entities + developer.log('Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}', name: 'AuthRepository'); + return await _createBusinessAndUser( + firebaseUser: firebaseUser, + companyName: companyName, + email: email, + onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user ); } on firebase.FirebaseAuthException catch (e) { - if (e.code == 'weak-password') { - throw Exception('The password provided is too weak.'); - } else if (e.code == 'email-already-in-use') { - throw Exception('An account already exists for that email address.'); + // Sign-in failed - check why + developer.log('Sign-in failed with code: ${e.code}', name: 'AuthRepository'); + + if (e.code == 'wrong-password' || e.code == 'invalid-credential') { + // Password doesn't match - check what providers are available + return await _handlePasswordMismatch(email); } else { - throw Exception('Sign-up error: ${e.message}'); + throw SignUpFailedException( + technicalMessage: 'Firebase sign-in error: ${e.message}', + ); } + } on domain.AppException { + rethrow; + } + } + + /// Handles the case where the password doesn't match the existing account. + /// + /// Note: fetchSignInMethodsForEmail was deprecated by Firebase for security + /// reasons (email enumeration). We show a combined message that covers both + /// cases: wrong password OR account uses different sign-in method (Google). + Future _handlePasswordMismatch(String email) async { + // We can't distinguish between "wrong password" and "no password provider" + // due to Firebase deprecating fetchSignInMethodsForEmail. + // The PasswordMismatchException message covers both scenarios. + developer.log('Password mismatch or different provider for: $email', name: 'AuthRepository'); + throw PasswordMismatchException( + technicalMessage: 'Email $email: password mismatch or different auth provider', + ); + } + + /// Checks if a user with BUSINESS role exists in PostgreSQL. + Future _checkBusinessUserExists(String firebaseUserId) async { + try { + final QueryResult response = + await _dataConnect.getUserById(id: firebaseUserId).execute(); + final dc.GetUserByIdUser? user = response.data?.user; + return user != null && user.userRole == 'BUSINESS'; } catch (e) { - throw Exception('Failed to sign up and create user data: ${e.toString()}'); + developer.log('Error checking business user: $e', name: 'AuthRepository'); + return false; + } + } + + /// Creates Business and User entities in PostgreSQL for a Firebase user. + Future _createBusinessAndUser({ + required firebase.User firebaseUser, + required String companyName, + required String email, + required void Function(String businessId) onBusinessCreated, + }) async { + // Create Business entity in PostgreSQL + final OperationResult createBusinessResponse = + await _dataConnect.createBusiness( + businessName: companyName, + userId: firebaseUser.uid, + rateGroup: dc.BusinessRateGroup.STANDARD, + status: dc.BusinessStatus.PENDING, + ).execute(); + + final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert; + if (businessData == null) { + throw const SignUpFailedException( + technicalMessage: 'Business creation failed in PostgreSQL', + ); + } + onBusinessCreated(businessData.id); + + // Create User entity in PostgreSQL + final OperationResult createUserResponse = + await _dataConnect.createUser( + id: firebaseUser.uid, + role: dc.UserBaseRole.USER, + ) + .email(email) + .userRole('BUSINESS') + .execute(); + + final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert; + if (newUserData == null) { + throw const SignUpFailedException( + technicalMessage: 'User profile creation failed in PostgreSQL', + ); + } + + return _getUserProfile( + firebaseUserId: firebaseUser.uid, + fallbackEmail: firebaseUser.email ?? email, + ); + } + + /// Rollback helper to clean up partially created resources during sign-up. + Future _rollbackSignUp({ + firebase.User? firebaseUser, + String? businessId, + }) async { + // Delete business first (if created) + if (businessId != null) { + try { + await _dataConnect.deleteBusiness(id: businessId).execute(); + } catch (_) { + // Log but don't throw - we're already in error recovery + } + } + // Delete Firebase user (if created) + if (firebaseUser != null) { + try { + await firebaseUser.delete(); + } catch (_) { + // Log but don't throw - we're already in error recovery + } } } @@ -142,17 +327,23 @@ class AuthRepositoryImpl implements AuthRepositoryInterface { final QueryResult response = await _dataConnect.getUserById(id: firebaseUserId).execute(); final dc.GetUserByIdUser? user = response.data?.user; if (user == null) { - throw Exception('Authenticated user profile not found in database.'); + throw UserNotFoundException( + technicalMessage: 'Firebase UID $firebaseUserId not found in users table', + ); } if (requireBusinessRole && user.userRole != 'BUSINESS') { await _firebaseAuth.signOut(); dc.ClientSessionStore.instance.clear(); - throw Exception('User is not authorized for this app.'); + throw UnauthorizedAppException( + technicalMessage: 'User role is ${user.userRole}, expected BUSINESS', + ); } final String? email = user.email ?? fallbackEmail; if (email == null || email.isEmpty) { - throw Exception('User email is missing in profile data.'); + throw UserNotFoundException( + technicalMessage: 'User email missing for UID $firebaseUserId', + ); } final domain.User domainUser = domain.User( diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart index e1c39429..b264922c 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/blocs/client_auth_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_domain/krow_domain.dart'; @@ -56,11 +58,20 @@ class ClientAuthBloc extends Bloc { SignInWithEmailArguments(email: event.email, password: event.password), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } @@ -81,11 +92,20 @@ class ClientAuthBloc extends Bloc { ), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } @@ -102,11 +122,20 @@ class ClientAuthBloc extends Bloc { SignInWithSocialArguments(provider: event.provider), ); emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } @@ -121,11 +150,20 @@ class ClientAuthBloc extends Bloc { try { await _signOut(); emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc'); emit( state.copyWith( status: ClientAuthStatus.error, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientAuthBloc'); + emit( + state.copyWith( + status: ClientAuthStatus.error, + errorMessage: 'errors.generic.unknown', ), ); } diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart index 6bde0059..33df7cbe 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_in_page.dart @@ -45,10 +45,11 @@ class ClientSignInPage extends StatelessWidget { if (state.status == ClientAuthStatus.authenticated) { Modular.to.navigateClientHome(); } else if (state.status == ClientAuthStatus.error) { + final String errorMessage = state.errorMessage != null + ? translateErrorKey(state.errorMessage!) + : t.errors.generic.unknown; ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'Authentication Error'), - ), + SnackBar(content: Text(errorMessage)), ); } }, diff --git a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart index 0df74969..2453b486 100644 --- a/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart +++ b/apps/mobile/packages/features/client/authentication/lib/src/presentation/pages/client_sign_up_page.dart @@ -49,10 +49,11 @@ class ClientSignUpPage extends StatelessWidget { if (state.status == ClientAuthStatus.authenticated) { Modular.to.navigateClientHome(); } else if (state.status == ClientAuthStatus.error) { + final String errorMessage = state.errorMessage != null + ? translateErrorKey(state.errorMessage!) + : t.errors.generic.unknown; ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage ?? 'Authentication Error'), - ), + SnackBar(content: Text(errorMessage)), ); } }, diff --git a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart index c45991ae..2a7f9677 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/billing_module.dart @@ -15,6 +15,9 @@ import 'presentation/pages/billing_page.dart'; class BillingModule extends Module { @override void binds(Injector i) { + // Mock repositories (TODO: Replace with real implementations) + i.addSingleton(FinancialRepositoryMock.new); + // Repositories i.addSingleton( () => BillingRepositoryImpl( diff --git a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart index 2a5774a7..825917ff 100644 --- a/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart +++ b/apps/mobile/packages/features/client/billing/lib/src/presentation/pages/billing_page.dart @@ -83,7 +83,7 @@ class _BillingViewState extends State { leading: Center( child: UiIconButton.secondary( icon: UiIcons.arrowLeft, - onTap: () => Modular.to.pop(), + onTap: () => Modular.to.navigate('/client-main/home/'), ), ), title: AnimatedSwitcher( diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart index e10f7432..52f5388f 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart @@ -68,7 +68,7 @@ class _CoveragePageState extends State { expandedHeight: 300.0, backgroundColor: UiColors.primary, leading: IconButton( - onPressed: () => Modular.to.pop(), + onPressed: () => Modular.to.navigate('/client-main/home/'), icon: Container( padding: const EdgeInsets.all(UiConstants.space2), decoration: BoxDecoration( diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart index 6c9513bb..f346e8fd 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/widgets/coverage_header.dart @@ -67,7 +67,7 @@ class CoverageHeader extends StatelessWidget { Row( children: [ GestureDetector( - onTap: () => Modular.to.pop(), + onTap: () => Modular.to.navigate('/client-main/home/'), child: Container( width: UiConstants.space10, height: UiConstants.space10, diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart index fd38a142..fae4d2d1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -18,7 +18,7 @@ class PermanentOrderPage extends StatelessWidget { backgroundColor: UiColors.bgPrimary, appBar: UiAppBar( title: labels.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.navigate('/client/create-order/'), ), body: Center( child: Padding( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart index 64324b46..2f15cf70 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -18,7 +18,7 @@ class RecurringOrderPage extends StatelessWidget { backgroundColor: UiColors.bgPrimary, appBar: UiAppBar( title: labels.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.navigate('/client/create-order/'), ), body: Center( child: Padding( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart index eb1775fb..290165fc 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/create_order/create_order_view.dart @@ -43,7 +43,7 @@ class CreateOrderView extends StatelessWidget { backgroundColor: UiColors.bgPrimary, appBar: UiAppBar( title: t.client_create_order.title, - onLeadingPressed: () => Modular.to.pop(), + onLeadingPressed: () => Modular.to.navigate('/client-main/home/'), ), body: SafeArea( child: Padding( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart index ed81e3f0..895c4ce1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart @@ -50,7 +50,7 @@ class OneTimeOrderView extends StatelessWidget { OneTimeOrderHeader( title: labels.title, subtitle: labels.subtitle, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.navigate('/client/create-order/'), ), Expanded( child: Center( @@ -89,7 +89,7 @@ class OneTimeOrderView extends StatelessWidget { OneTimeOrderHeader( title: labels.title, subtitle: labels.subtitle, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.navigate('/client/create-order/'), ), Expanded( child: Stack( diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart index 093ec39d..95713729 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart @@ -28,7 +28,7 @@ class RapidOrderView extends StatelessWidget { title: labels.success_title, message: labels.success_message, buttonLabel: labels.back_to_orders, - onDone: () => Modular.to.pop(), + onDone: () => Modular.to.navigate('/client-main/orders/'), ); } @@ -82,7 +82,7 @@ class _RapidOrderFormState extends State<_RapidOrderForm> { subtitle: labels.subtitle, date: dateStr, time: timeStr, - onBack: () => Modular.to.pop(), + onBack: () => Modular.to.navigate('/client/create-order/'), ), // Content diff --git a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart index fdfec83d..45b8b8f8 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart @@ -5,6 +5,12 @@ import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:http/http.dart' as http; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; +import 'package:krow_domain/krow_domain.dart' + show + HubHasOrdersException, + HubCreationFailedException, + BusinessNotFoundException, + NotAuthenticatedException; import '../../domain/repositories/hub_repository_interface.dart'; import '../../util/hubs_constants.dart'; @@ -67,7 +73,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { .execute(); final String? createdId = result.data?.teamHub_insert.id; if (createdId == null) { - throw Exception('Hub creation failed.'); + throw HubCreationFailedException( + technicalMessage: 'teamHub_insert returned null for hub: $name', + ); } final List hubs = await _fetchHubsForTeam( @@ -97,7 +105,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; if (businessId == null || businessId.isEmpty) { await _firebaseAuth.signOut(); - throw Exception('Business is missing. Please sign in again.'); + throw const BusinessNotFoundException( + technicalMessage: 'Business ID missing from session', + ); } final QueryResult< @@ -110,7 +120,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { .execute(); if (result.data.orders.isNotEmpty) { - throw Exception("Sorry this hub has orders, it can't be deleted."); + throw HubHasOrdersException( + technicalMessage: 'Hub $id has ${result.data.orders.length} orders', + ); } await _dataConnect.deleteTeamHub(id: id).execute(); @@ -151,7 +163,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { final firebase.User? user = _firebaseAuth.currentUser; if (user == null) { - throw Exception('User is not authenticated.'); + throw const NotAuthenticatedException( + technicalMessage: 'No Firebase user in currentUser', + ); } final QueryResult result = await _dataConnect.getBusinessesByUserId( @@ -159,7 +173,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { ).execute(); if (result.data.businesses.isEmpty) { await _firebaseAuth.signOut(); - throw Exception('No business found for this user. Please sign in again.'); + throw BusinessNotFoundException( + technicalMessage: 'No business found for user ${user.uid}', + ); } final dc.GetBusinessesByUserIdBusinesses business = result.data.businesses.first; @@ -206,7 +222,9 @@ class HubRepositoryImpl implements HubRepositoryInterface { final OperationResult createTeamResult = await createTeamBuilder.execute(); final String? teamId = createTeamResult.data?.team_insert.id; if (teamId == null) { - throw Exception('Team creation failed.'); + throw HubCreationFailedException( + technicalMessage: 'Team creation failed for business ${business.id}', + ); } return teamId; diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart index 2359f296..becc3e8c 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:developer' as developer; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:krow_domain/krow_domain.dart'; @@ -67,11 +69,20 @@ class ClientHubsBloc extends Bloc try { final List hubs = await _getHubsUseCase(); emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs)); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.failure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.failure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -106,11 +117,20 @@ class ClientHubsBloc extends Bloc showAddHubDialog: false, ), ); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.actionFailure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -131,11 +151,20 @@ class ClientHubsBloc extends Bloc successMessage: 'Hub deleted successfully', ), ); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.actionFailure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -159,11 +188,20 @@ class ClientHubsBloc extends Bloc clearHubToIdentify: true, ), ); - } catch (e) { + } on AppException catch (e) { + developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc'); emit( state.copyWith( status: ClientHubsStatus.actionFailure, - errorMessage: e.toString(), + errorMessage: e.messageKey, + ), + ); + } catch (e) { + developer.log('Unexpected error: $e', name: 'ClientHubsBloc'); + emit( + state.copyWith( + status: ClientHubsStatus.actionFailure, + errorMessage: 'errors.generic.unknown', ), ); } @@ -175,8 +213,8 @@ class ClientHubsBloc extends Bloc ) { emit( state.copyWith( - errorMessage: null, - successMessage: null, + clearErrorMessage: true, + clearSuccessMessage: true, status: state.status == ClientHubsStatus.actionSuccess || state.status == ClientHubsStatus.actionFailure diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart index efccca99..4d592df8 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart @@ -43,12 +43,18 @@ class ClientHubsState extends Equatable { bool? showAddHubDialog, Hub? hubToIdentify, bool clearHubToIdentify = false, + bool clearErrorMessage = false, + bool clearSuccessMessage = false, }) { return ClientHubsState( status: status ?? this.status, hubs: hubs ?? this.hubs, - errorMessage: errorMessage ?? this.errorMessage, - successMessage: successMessage ?? this.successMessage, + errorMessage: clearErrorMessage + ? null + : (errorMessage ?? this.errorMessage), + successMessage: clearSuccessMessage + ? null + : (successMessage ?? this.successMessage), showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog, hubToIdentify: clearHubToIdentify ? null diff --git a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart index 76d7c8cd..85f60930 100644 --- a/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart +++ b/apps/mobile/packages/features/client/hubs/lib/src/presentation/pages/client_hubs_page.dart @@ -33,9 +33,10 @@ class ClientHubsPage extends StatelessWidget { }, listener: (BuildContext context, ClientHubsState state) { if (state.errorMessage != null && state.errorMessage!.isNotEmpty) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(state.errorMessage!))); + final String errorMessage = translateErrorKey(state.errorMessage!); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(errorMessage)), + ); BlocProvider.of( context, ).add(const ClientHubsMessageCleared()); @@ -178,7 +179,7 @@ class ClientHubsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( - onTap: () => Modular.to.pop(), + onTap: () => Modular.to.navigate('/client-main/home/'), child: Container( width: 40, height: 40, diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart index e044d1ec..e3e99090 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_actions.dart @@ -83,7 +83,7 @@ class SettingsActions extends StatelessWidget { // Cancel button UiButton.secondary( text: t.common.cancel, - onPressed: () => Modular.to.pop(), + onPressed: () => Navigator.of(dialogContext).pop(), ), ], ), diff --git a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart index fc05ccf6..f644caf3 100644 --- a/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart +++ b/apps/mobile/packages/features/client/settings/lib/src/presentation/widgets/client_settings_page/settings_profile_header.dart @@ -30,7 +30,7 @@ class SettingsProfileHeader extends StatelessWidget { shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)), leading: IconButton( icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary), - onPressed: () => Modular.to.pop(), + onPressed: () => Modular.to.navigate('/client-main/home/'), ), flexibleSpace: FlexibleSpaceBar( background: Container( diff --git a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart index 343acc25..6886cfe0 100644 --- a/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart +++ b/apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -202,21 +202,38 @@ class _ViewOrderCardState extends State { ], ), const SizedBox(height: UiConstants.space2), - // Address + // Location (Hub name + Address) Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Icon( - UiIcons.mapPin, - size: 14, - color: UiColors.iconSecondary, + const Padding( + padding: EdgeInsets.only(top: 2), + child: Icon( + UiIcons.mapPin, + size: 14, + color: UiColors.iconSecondary, + ), ), const SizedBox(width: 4), Expanded( - child: Text( - order.locationAddress, - style: UiTypography.footnote2r.textSecondary, - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (order.location.isNotEmpty) + Text( + order.location, + style: UiTypography.footnote1b.textPrimary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (order.locationAddress.isNotEmpty) + Text( + order.locationAddress, + style: UiTypography.footnote2r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], ), ), ], diff --git a/bugs/10.png b/bugs/10.png new file mode 100644 index 00000000..c2127bb0 Binary files /dev/null and b/bugs/10.png differ