Merge branch 'dev' into 312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation

This commit is contained in:
Achintha Isuru
2026-02-01 01:42:41 -05:00
74 changed files with 2639 additions and 321 deletions

View File

@@ -39,20 +39,25 @@ melos bootstrap
### 3. Running the Apps
You can run the applications using Melos scripts or through the `Makefile`:
First, find your device ID:
```bash
flutter devices
```
#### Client App
```bash
# Using Melos
melos run start:client -d android # or ios
# Using Makefile
make mobile-client-dev-android
melos run start:client -- -d <device_id>
# Using Makefile (DEVICE defaults to 'android' if not specified)
make mobile-client-dev-android DEVICE=<device_id>
```
#### Staff App
```bash
# Using Melos
melos run start:staff -d android # or ios
# Using Makefile
make mobile-staff-dev-android
melos run start:staff -- -d <device_id>
# Using Makefile (DEVICE defaults to 'android' if not specified)
make mobile-staff-dev-android DEVICE=<device_id>
```
## 🛠 Useful Commands

View File

@@ -1,32 +0,0 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View File

@@ -1,5 +0,0 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_linux-2.4.1/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_linux-3.2.2/

View File

@@ -1,18 +0,0 @@
// This is a generated file; do not edit or check into version control.
<<<<<<< Updated upstream
FLUTTER_ROOT=/Users/josesalazar/flutter
FLUTTER_APPLICATION_PATH=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client
=======
FLUTTER_ROOT=C:\flutter\src\flutter
FLUTTER_APPLICATION_PATH=E:\Krow-google\krow-workforce\apps\mobile\apps\client
>>>>>>> Stashed changes
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client/lib/main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/.dart_tool/package_config.json

View File

@@ -1,14 +0,0 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=C:\flutter\src\flutter"
export "FLUTTER_APPLICATION_PATH=E:\Krow-google\krow-workforce\apps\mobile\apps\client"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/apps/client/lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"
export "DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=/Users/josesalazar/Documents/DEV/krow-workforce/apps/mobile/.dart_tool/package_config.json"

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_auth-6.1.4/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core-4.4.0/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_windows-2.4.1/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_windows-3.1.5/

View File

@@ -1,32 +0,0 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View File

@@ -1,5 +0,0 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View File

@@ -1,11 +0,0 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=C:\flutter\src\flutter
FLUTTER_APPLICATION_PATH=E:\Krow-google\krow-workforce\apps\mobile\apps\design_system_viewer
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=1.0.0
FLUTTER_BUILD_NUMBER=1
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=.dart_tool/package_config.json

View File

@@ -1,12 +0,0 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=C:\flutter\src\flutter"
export "FLUTTER_APPLICATION_PATH=E:\Krow-google\krow-workforce\apps\mobile\apps\design_system_viewer"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=1.0.0"
export "FLUTTER_BUILD_NUMBER=1"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=.dart_tool/package_config.json"

View File

@@ -1,32 +0,0 @@
#
# Generated file, do not edit.
#
import lldb
def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict):
"""Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages."""
base = frame.register["x0"].GetValueAsAddress()
page_len = frame.register["x1"].GetValueAsUnsigned()
# Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the
# first page to see if handled it correctly. This makes diagnosing
# misconfiguration (e.g. missing breakpoint) easier.
data = bytearray(page_len)
data[0:8] = b'IHELPED!'
error = lldb.SBError()
frame.GetThread().GetProcess().WriteMemory(base, data, error)
if not error.Success():
print(f'Failed to write into {base}[+{page_len}]', error)
return
def __lldb_init_module(debugger: lldb.SBDebugger, _):
target = debugger.GetDummyTarget()
# Caveat: must use BreakpointCreateByRegEx here and not
# BreakpointCreateByName. For some reasons callback function does not
# get carried over from dummy target for the later.
bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$")
bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__))
bp.SetAutoContinue(True)
print("-- LLDB integration loaded --")

View File

@@ -1,5 +0,0 @@
#
# Generated file, do not edit.
#
command script import --relative-to-command-file flutter_lldb_helper.py

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_linux-2.4.1/

View File

@@ -1,13 +0,0 @@
// This is a generated file; do not edit or check into version control.
FLUTTER_ROOT=/Users/achinthaisuru/Documents/flutter
FLUTTER_APPLICATION_PATH=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/staff
COCOAPODS_PARALLEL_CODE_SIGN=true
FLUTTER_TARGET=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/staff/lib/main.dart
FLUTTER_BUILD_DIR=build
FLUTTER_BUILD_NAME=0.0.13
FLUTTER_BUILD_NUMBER=1
DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43
DART_OBFUSCATION=false
TRACK_WIDGET_CREATION=true
TREE_SHAKE_ICONS=false
PACKAGE_CONFIG=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/.dart_tool/package_config.json

View File

@@ -1,14 +0,0 @@
#!/bin/sh
# This is a generated file; do not edit or check into version control.
export "FLUTTER_ROOT=/Users/achinthaisuru/Documents/flutter"
export "FLUTTER_APPLICATION_PATH=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/staff"
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
export "FLUTTER_TARGET=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/apps/staff/lib/main.dart"
export "FLUTTER_BUILD_DIR=build"
export "FLUTTER_BUILD_NAME=0.0.13"
export "FLUTTER_BUILD_NUMBER=1"
export "DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43"
export "DART_OBFUSCATION=false"
export "TRACK_WIDGET_CREATION=true"
export "TREE_SHAKE_ICONS=false"
export "PACKAGE_CONFIG=/Users/achinthaisuru/Documents/Github/krow-workforce/apps/mobile/.dart_tool/package_config.json"

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_auth-6.1.4/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core-4.4.0/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0/

View File

@@ -1 +0,0 @@
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_windows-2.4.1/

View File

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

View File

@@ -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!"
}
}
}

View File

@@ -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!"
}
}
}

View File

@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 2
/// Strings: 1044 (522 per locale)
/// Strings: 1108 (554 per locale)
///
/// Built on 2026-01-31 at 17:37 UTC

View File

@@ -57,6 +57,8 @@ class Translations with BaseTranslations<AppLocale, Translations> {
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,
};
}

View File

@@ -54,6 +54,8 @@ class TranslationsEs with BaseTranslations<AppLocale, Translations> 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,
};
}

View File

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

View File

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

View File

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

View File

@@ -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<domain.User> _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<dc.CreateBusinessData, dc.CreateBusinessVariables> 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<dc.CreateUserData, dc.CreateUserVariables> 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<Never> _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<bool> _checkBusinessUserExists(String firebaseUserId) async {
try {
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> 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<domain.User> _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<dc.CreateBusinessData, dc.CreateBusinessVariables> 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<dc.CreateUserData, dc.CreateUserVariables> 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<void> _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<dc.GetUserByIdData, dc.GetUserByIdVariables> 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(

View File

@@ -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<ClientAuthEvent, ClientAuthState> {
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<ClientAuthEvent, ClientAuthState> {
),
);
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<ClientAuthEvent, ClientAuthState> {
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<ClientAuthEvent, ClientAuthState> {
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',
),
);
}

View File

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

View File

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

View File

@@ -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>(FinancialRepositoryMock.new);
// Repositories
i.addSingleton<BillingRepository>(
() => BillingRepositoryImpl(

View File

@@ -83,7 +83,7 @@ class _BillingViewState extends State<BillingView> {
leading: Center(
child: UiIconButton.secondary(
icon: UiIcons.arrowLeft,
onTap: () => Modular.to.pop(),
onTap: () => Modular.to.navigate('/client-main/home/'),
),
),
title: AnimatedSwitcher(

View File

@@ -68,7 +68,7 @@ class _CoveragePageState extends State<CoveragePage> {
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(

View File

@@ -67,7 +67,7 @@ class CoverageHeader extends StatelessWidget {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
onTap: () => Modular.to.navigate('/client-main/home/'),
child: Container(
width: UiConstants.space10,
height: UiConstants.space10,

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

@@ -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(

View File

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

View File

@@ -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<domain.Hub> 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<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> 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<dc.CreateTeamData, dc.CreateTeamVariables> 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;

View File

@@ -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<ClientHubsEvent, ClientHubsState>
try {
final List<Hub> 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<ClientHubsEvent, ClientHubsState>
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<ClientHubsEvent, ClientHubsState>
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<ClientHubsEvent, ClientHubsState>
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<ClientHubsEvent, ClientHubsState>
) {
emit(
state.copyWith(
errorMessage: null,
successMessage: null,
clearErrorMessage: true,
clearSuccessMessage: true,
status:
state.status == ClientHubsStatus.actionSuccess ||
state.status == ClientHubsStatus.actionFailure

View File

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

View File

@@ -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<ClientHubsBloc>(
context,
).add(const ClientHubsMessageCleared());
@@ -178,7 +179,7 @@ class ClientHubsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
onTap: () => Modular.to.navigate('/client-main/home/'),
child: Container(
width: 40,
height: 40,

View File

@@ -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(),
),
],
),

View File

@@ -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(

View File

@@ -202,21 +202,38 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
],
),
const SizedBox(height: UiConstants.space2),
// Address
// Location (Hub name + Address)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
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: <Widget>[
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,
),
],
),
),
],