Merge pull request #343 from Oloodi/fix/check-boris-applied
Fix/check boris applied
6
.gitignore
vendored
@@ -126,6 +126,12 @@ build/
|
||||
**/ios/Pods/
|
||||
**/ios/.symlinks/
|
||||
|
||||
# Ephemeral files (generated by Flutter for desktop platforms)
|
||||
**/linux/flutter/ephemeral/
|
||||
**/windows/flutter/ephemeral/
|
||||
**/macos/Flutter/ephemeral/
|
||||
**/ios/Flutter/ephemeral/
|
||||
|
||||
# ==============================================================================
|
||||
# FIREBASE & BACKEND
|
||||
# ==============================================================================
|
||||
|
||||
129
CLAUDE.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# CLAUDE.md - Project Context for AI Assistants
|
||||
|
||||
This file provides context for Claude Code and other AI assistants working on this codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**KROW Workforce** is a workforce management platform connecting businesses with temporary/on-demand workers. It consists of:
|
||||
- **Client App**: For businesses to create orders, manage hubs, handle billing
|
||||
- **Staff App**: For workers to manage availability, clock in/out, view earnings
|
||||
- **Web Dashboard**: Admin portal (React/Vite - WIP)
|
||||
- **Backend**: Firebase Data Connect + PostgreSQL on Cloud SQL
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
/apps
|
||||
/mobile # Flutter apps (managed by Melos)
|
||||
/apps
|
||||
/client # krowwithus_client - Business app
|
||||
/staff # krowwithus_staff - Worker app
|
||||
/design_system_viewer
|
||||
/packages
|
||||
/core # Base utilities
|
||||
/domain # Business entities, repository interfaces
|
||||
/data_connect # Data layer, Firebase Data Connect SDK
|
||||
/design_system # Shared UI components
|
||||
/core_localization # i18n (Slang)
|
||||
/features
|
||||
/client/* # Client-specific features
|
||||
/staff/* # Staff-specific features
|
||||
/web-dashboard # React web app (WIP)
|
||||
/backend
|
||||
/dataconnect # GraphQL schemas, Firebase Data Connect config
|
||||
/cloud-functions # Serverless functions (placeholder)
|
||||
/internal
|
||||
/launchpad # Internal DevOps portal
|
||||
/api-harness # API testing tool
|
||||
/makefiles # Modular Make targets
|
||||
/docs # Project documentation
|
||||
```
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Mobile Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
make mobile-install
|
||||
|
||||
# Run client app (specify your device ID)
|
||||
make mobile-client-dev-android DEVICE=<device_id>
|
||||
|
||||
# Run staff app
|
||||
make mobile-staff-dev-android DEVICE=<device_id>
|
||||
|
||||
# Find your device ID
|
||||
flutter devices
|
||||
|
||||
# Build APK
|
||||
make mobile-client-build PLATFORM=apk
|
||||
make mobile-staff-build PLATFORM=apk
|
||||
|
||||
# Code generation (localization + build_runner)
|
||||
cd apps/mobile && melos run gen:all
|
||||
```
|
||||
|
||||
### Web Development
|
||||
```bash
|
||||
make install # Install web dependencies
|
||||
make dev # Run web dev server
|
||||
```
|
||||
|
||||
### Data Connect
|
||||
```bash
|
||||
make dataconnect-sync # Deploy schemas, migrate, regenerate SDK
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
- **State Management**: BLoC pattern (flutter_bloc)
|
||||
- **Navigation**: Flutter Modular
|
||||
- **Architecture**: Clean Architecture (domain/data/presentation layers)
|
||||
- **Feature Organization**: Each feature is a separate package
|
||||
- **Value Objects**: Equatable for entity equality
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Features go in `/apps/mobile/packages/features/{client|staff}/`
|
||||
- Shared code goes in `/apps/mobile/packages/{core|domain|data_connect}/`
|
||||
- UI components go in `/apps/mobile/packages/design_system/`
|
||||
- GraphQL schemas go in `/backend/dataconnect/schema/`
|
||||
- Documentation language: **English**
|
||||
|
||||
## Important Files
|
||||
|
||||
- `apps/mobile/melos.yaml` - Melos workspace config
|
||||
- `makefiles/mobile.mk` - Mobile Make targets
|
||||
- `backend/dataconnect/dataconnect.yaml` - Data Connect config
|
||||
- `firebase.json` - Firebase hosting/emulator config
|
||||
- `BLOCKERS.md` - Known blockers and deviations
|
||||
|
||||
## Branch Protection
|
||||
|
||||
- `main` and `dev` branches are protected
|
||||
- Always create feature branches: `feature/`, `fix/`, `chore/`
|
||||
- PRs required for merging
|
||||
|
||||
## Testing Mobile Apps
|
||||
|
||||
1. Connect your Android device or start emulator
|
||||
2. Run `flutter devices` to get device ID
|
||||
3. Run `make mobile-client-dev-android DEVICE=<your_device_id>`
|
||||
|
||||
## Common Issues
|
||||
|
||||
### "No devices found with name 'android'"
|
||||
The Makefile defaults to device ID `android`. Override with your actual device:
|
||||
```bash
|
||||
make mobile-client-dev-android DEVICE=3fb285a7
|
||||
```
|
||||
|
||||
### Dependency resolution issues
|
||||
```bash
|
||||
cd apps/mobile && melos clean && melos bootstrap
|
||||
```
|
||||
|
||||
### Code generation out of sync
|
||||
```bash
|
||||
cd apps/mobile && melos run gen:all
|
||||
```
|
||||
138
GEMINI.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# GEMINI.md - Project Context for AI Assistants
|
||||
|
||||
This file provides context for Gemini and other AI assistants working on this codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**KROW Workforce** is a workforce management platform connecting businesses with temporary/on-demand workers. It consists of:
|
||||
- **Client App**: For businesses to create orders, manage hubs, handle billing
|
||||
- **Staff App**: For workers to manage availability, clock in/out, view earnings
|
||||
- **Web Dashboard**: Admin portal (React/Vite - WIP)
|
||||
- **Backend**: Firebase Data Connect + PostgreSQL on Cloud SQL
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
```
|
||||
/apps
|
||||
/mobile # Flutter apps (managed by Melos)
|
||||
/apps
|
||||
/client # krowwithus_client - Business app
|
||||
/staff # krowwithus_staff - Worker app
|
||||
/design_system_viewer
|
||||
/packages
|
||||
/core # Base utilities
|
||||
/domain # Business entities, repository interfaces
|
||||
/data_connect # Data layer, Firebase Data Connect SDK
|
||||
/design_system # Shared UI components
|
||||
/core_localization # i18n (Slang)
|
||||
/features
|
||||
/client/* # Client-specific features
|
||||
/staff/* # Staff-specific features
|
||||
/web-dashboard # React web app (WIP)
|
||||
/backend
|
||||
/dataconnect # GraphQL schemas, Firebase Data Connect config
|
||||
/cloud-functions # Serverless functions (placeholder)
|
||||
/internal
|
||||
/launchpad # Internal DevOps portal
|
||||
/api-harness # API testing tool
|
||||
/makefiles # Modular Make targets
|
||||
/docs # Project documentation
|
||||
/bugs # Bug reports and screenshots
|
||||
```
|
||||
|
||||
## Key Commands
|
||||
|
||||
### Mobile Development
|
||||
```bash
|
||||
# Install dependencies
|
||||
make mobile-install
|
||||
|
||||
# Run client app (specify your device ID)
|
||||
make mobile-client-dev-android DEVICE=<device_id>
|
||||
|
||||
# Run staff app
|
||||
make mobile-staff-dev-android DEVICE=<device_id>
|
||||
|
||||
# Find your device ID
|
||||
flutter devices
|
||||
|
||||
# Build APK
|
||||
make mobile-client-build PLATFORM=apk
|
||||
make mobile-staff-build PLATFORM=apk
|
||||
|
||||
# Code generation (localization + build_runner)
|
||||
cd apps/mobile && melos run gen:all
|
||||
```
|
||||
|
||||
### Web Development
|
||||
```bash
|
||||
make install # Install web dependencies
|
||||
make dev # Run web dev server
|
||||
```
|
||||
|
||||
### Data Connect
|
||||
```bash
|
||||
make dataconnect-sync # Deploy schemas, migrate, regenerate SDK
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
- **State Management**: BLoC pattern (flutter_bloc)
|
||||
- **Navigation**: Flutter Modular
|
||||
- **Architecture**: Clean Architecture (domain/data/presentation layers)
|
||||
- **Feature Organization**: Each feature is a separate package
|
||||
- **Value Objects**: Equatable for entity equality
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Features go in `/apps/mobile/packages/features/{client|staff}/`
|
||||
- Shared code goes in `/apps/mobile/packages/{core|domain|data_connect}/`
|
||||
- UI components go in `/apps/mobile/packages/design_system/`
|
||||
- GraphQL schemas go in `/backend/dataconnect/schema/`
|
||||
- Documentation language: **English**
|
||||
|
||||
## Important Files
|
||||
|
||||
- `apps/mobile/melos.yaml` - Melos workspace config
|
||||
- `makefiles/mobile.mk` - Mobile Make targets
|
||||
- `backend/dataconnect/dataconnect.yaml` - Data Connect config
|
||||
- `firebase.json` - Firebase hosting/emulator config
|
||||
- `BLOCKERS.md` - Known blockers and deviations
|
||||
- `bugs/BUG-REPORT-*.md` - Bug reports with analysis
|
||||
|
||||
## Branch Protection
|
||||
|
||||
- `main` and `dev` branches are protected
|
||||
- Always create feature branches: `feature/`, `fix/`, `chore/`
|
||||
- PRs required for merging
|
||||
|
||||
## Testing Mobile Apps
|
||||
|
||||
1. Connect your Android device or start emulator
|
||||
2. Run `flutter devices` to get device ID
|
||||
3. Run `make mobile-client-dev-android DEVICE=<your_device_id>`
|
||||
|
||||
## Common Issues
|
||||
|
||||
### "No devices found with name 'android'"
|
||||
The Makefile defaults to device ID `android`. Override with your actual device:
|
||||
```bash
|
||||
make mobile-client-dev-android DEVICE=3fb285a7
|
||||
```
|
||||
|
||||
### Dependency resolution issues
|
||||
```bash
|
||||
cd apps/mobile && melos clean && melos bootstrap
|
||||
```
|
||||
|
||||
### Code generation out of sync
|
||||
```bash
|
||||
cd apps/mobile && melos run gen:all
|
||||
```
|
||||
|
||||
## Known Technical Debt
|
||||
|
||||
See `bugs/BUG-REPORT-*.md` for detailed analysis of:
|
||||
- Authentication/User sync issues
|
||||
- Error handling architecture (needs AppException pattern)
|
||||
- BLoC state management patterns (copyWith null handling)
|
||||
@@ -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
|
||||
|
||||
@@ -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 --")
|
||||
@@ -1,5 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
command script import --relative-to-command-file flutter_lldb_helper.py
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_linux-2.4.1/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_linux-3.2.2/
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_auth-6.1.4/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core-4.4.0/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_windows-2.4.1/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/url_launcher_windows-3.1.5/
|
||||
@@ -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 --")
|
||||
@@ -1,5 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
command script import --relative-to-command-file flutter_lldb_helper.py
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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 --")
|
||||
@@ -1,5 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
command script import --relative-to-command-file flutter_lldb_helper.py
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_linux-2.2.1/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_linux-2.4.1/
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_auth-6.1.4/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/firebase_core-4.4.0/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/path_provider_windows-2.3.0/
|
||||
@@ -1 +0,0 @@
|
||||
C:/Users/Admin/AppData/Local/Pub/Cache/hosted/pub.dev/shared_preferences_windows-2.4.1/
|
||||
@@ -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';
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
BIN
bugs/1.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
bugs/10.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
bugs/2.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
bugs/3.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
bugs/4.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
bugs/5.png
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
bugs/6.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
bugs/7.png
Normal file
|
After Width: | Height: | Size: 180 KiB |
BIN
bugs/8.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
bugs/9.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
932
bugs/BUG-REPORT-2026-01-31.md
Normal file
@@ -0,0 +1,932 @@
|
||||
# Bug Report & Technical Debt Analysis
|
||||
|
||||
**Branch:** `fix/check-boris`
|
||||
**Apps Tested:** Client Mobile (Android)
|
||||
|
||||
---
|
||||
|
||||
## Housekeeping: Git Hygiene Issue
|
||||
|
||||
### Problem
|
||||
|
||||
Several Flutter-generated ephemeral files were incorrectly committed to the repository. These files are platform-specific symlinks and generated configs that should be ignored.
|
||||
|
||||
### Affected Files
|
||||
|
||||
```
|
||||
apps/mobile/apps/*/linux/flutter/ephemeral/.plugin_symlinks/*
|
||||
apps/mobile/apps/*/windows/flutter/ephemeral/.plugin_symlinks/*
|
||||
apps/mobile/apps/*/macos/Flutter/ephemeral/*
|
||||
apps/mobile/apps/*/ios/Flutter/ephemeral/*
|
||||
```
|
||||
|
||||
### Fix Applied
|
||||
|
||||
1. Updated `.gitignore` to include:
|
||||
```gitignore
|
||||
# Ephemeral files (generated by Flutter for desktop platforms)
|
||||
**/linux/flutter/ephemeral/
|
||||
**/windows/flutter/ephemeral/
|
||||
**/macos/Flutter/ephemeral/
|
||||
**/ios/Flutter/ephemeral/
|
||||
```
|
||||
|
||||
2. Run these commands to remove from tracking (files stay on disk):
|
||||
```bash
|
||||
git rm -r --cached apps/mobile/apps/client/linux/flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/client/windows/flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/client/macos/Flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/client/ios/Flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/staff/linux/flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/staff/windows/flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/staff/ios/Flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/design_system_viewer/ios/Flutter/ephemeral/
|
||||
git rm -r --cached apps/mobile/apps/design_system_viewer/macos/Flutter/ephemeral/
|
||||
```
|
||||
|
||||
### Note on `strings.g.dart`
|
||||
|
||||
The file `apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart` is auto-generated by Slang. It is listed in `apps/mobile/.gitignore` as `*.g.dart` but was committed before the gitignore rule existed.
|
||||
|
||||
**Recommendation:** Remove from tracking with `git rm --cached` and regenerate via `melos run gen:all` after each pull.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report documents critical bugs discovered during manual testing of the Client mobile application, along with architectural issues requiring immediate attention. The analysis covers authentication flow, data model design, UI state management, and error handling practices.
|
||||
|
||||
| Priority | Issue | Severity | Effort |
|
||||
|----------|-------|----------|--------|
|
||||
| P0 | Auth/User Sync Issue | Critical | Medium |
|
||||
| P1 | Error Handling Architecture | High | High |
|
||||
| P1 | Order Display Logic | Medium | Low |
|
||||
| P2 | Hub Delete UI Freeze | Medium | Low |
|
||||
| P2 | Hub Name vs Address Display | Low | Low |
|
||||
|
||||
---
|
||||
|
||||
## Bug 1: Authentication & User Sync Issue
|
||||
|
||||
### Status: CRITICAL
|
||||
|
||||
### Description
|
||||
|
||||
Users who attempt to create an account may end up in an inconsistent state where:
|
||||
- Firebase Authentication has their account (email/password stored)
|
||||
- PostgreSQL database does NOT have their user profile
|
||||
|
||||
This results in:
|
||||
- "Account already exists" when trying to register again
|
||||
- "Incorrect email or password" when trying to log in (even with correct credentials)
|
||||
|
||||
### Screenshots
|
||||
|
||||
- `1.png` - Registration attempt with boris@bwnyasse.net showing "Account already exists"
|
||||
- `2.png` - Login attempt showing "Incorrect email or password"
|
||||
- `3.png`, `4.png` - Additional registration failures
|
||||
|
||||
### Root Cause
|
||||
|
||||
**File:** `apps/mobile/packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart`
|
||||
|
||||
The registration flow (lines 57-120) performs three sequential operations:
|
||||
|
||||
```dart
|
||||
// Step 1: Create Firebase Auth user
|
||||
final credential = await _firebaseAuth.createUserWithEmailAndPassword(...);
|
||||
|
||||
// Step 2: Create Business in PostgreSQL
|
||||
final createBusinessResponse = await _dataConnect.createBusiness(...);
|
||||
|
||||
// Step 3: Create User in PostgreSQL
|
||||
final createUserResponse = await _dataConnect.createUser(...);
|
||||
```
|
||||
|
||||
**Problem:** If Step 2 or Step 3 fails AFTER Step 1 succeeds:
|
||||
- The rollback `firebaseUser.delete()` may fail silently
|
||||
- User exists in Firebase Auth but NOT in PostgreSQL
|
||||
- Login fails because `_getUserProfile()` cannot find the user (line 142-145)
|
||||
|
||||
**Additionally, line 100 has an incomplete TODO:**
|
||||
```dart
|
||||
// TO-DO: Also delete the created Business if this fails
|
||||
```
|
||||
|
||||
If `createUser` fails, the orphaned Business record remains in PostgreSQL.
|
||||
|
||||
### Technical Flow
|
||||
|
||||
```
|
||||
Registration Attempt:
|
||||
├─ Firebase Auth: createUserWithEmailAndPassword() ✓ (User created)
|
||||
├─ Data Connect: createBusiness() ✓ (Business created)
|
||||
└─ Data Connect: createUser() ✗ (FAILS - network error, constraint violation, etc.)
|
||||
└─ Rollback: firebaseUser.delete() ✗ (May fail silently)
|
||||
|
||||
Result:
|
||||
- Firebase Auth: User EXISTS
|
||||
- PostgreSQL users table: User MISSING
|
||||
- PostgreSQL businesses table: Orphaned Business record
|
||||
|
||||
Subsequent Login:
|
||||
├─ Firebase Auth: signInWithEmailAndPassword() ✓ (Credentials valid)
|
||||
└─ _getUserProfile(): getUserById() returns NULL
|
||||
└─ Throws: "Authenticated user profile not found in database."
|
||||
└─ But error shown to user: "Incorrect email or password." (misleading!)
|
||||
```
|
||||
|
||||
### Recommended Fix
|
||||
|
||||
1. **Implement transactional rollback:**
|
||||
```dart
|
||||
Future<domain.User> signUpWithEmail({...}) async {
|
||||
firebase.User? firebaseUser;
|
||||
String? businessId;
|
||||
|
||||
try {
|
||||
// Step 1
|
||||
final credential = await _firebaseAuth.createUserWithEmailAndPassword(...);
|
||||
firebaseUser = credential.user;
|
||||
|
||||
// Step 2
|
||||
final businessResult = await _dataConnect.createBusiness(...);
|
||||
businessId = businessResult.data?.business_insert.id;
|
||||
|
||||
// Step 3
|
||||
final userResult = await _dataConnect.createUser(...);
|
||||
if (userResult.data?.user_insert == null) {
|
||||
throw Exception('User creation failed');
|
||||
}
|
||||
|
||||
return _getUserProfile(...);
|
||||
|
||||
} catch (e) {
|
||||
// Full rollback
|
||||
if (businessId != null) {
|
||||
await _dataConnect.deleteBusiness(id: businessId).execute();
|
||||
}
|
||||
if (firebaseUser != null) {
|
||||
await firebaseUser.delete();
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Add admin tool to reconcile orphaned accounts**
|
||||
3. **Add retry mechanism with idempotency checks**
|
||||
|
||||
---
|
||||
|
||||
## Bug 2: Hub Name vs Address Display
|
||||
|
||||
### Status: LOW PRIORITY
|
||||
|
||||
### Description
|
||||
|
||||
Order cards display the hub's street address instead of the hub name, creating inconsistency with the hub management screen.
|
||||
|
||||
### Screenshots
|
||||
|
||||
- `5.png` - Order showing "6800 San Jose Street, Granada Hills, CA, USA"
|
||||
- `6.png` - Order showing "San Jose Street"
|
||||
- Compare with `7.png` showing hub names: "Downtown Operations Hub", "Central Operations Hub"
|
||||
|
||||
### Root Cause
|
||||
|
||||
**File:** `apps/mobile/packages/features/client/view_orders/lib/src/presentation/widgets/view_order_card.dart`
|
||||
|
||||
Lines 213-219 display `order.locationAddress` instead of `order.location` (hub name):
|
||||
|
||||
```dart
|
||||
Expanded(
|
||||
child: Text(
|
||||
order.locationAddress, // Shows address, not hub name
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
```
|
||||
|
||||
The data is correctly stored during order creation in `client_create_order_repository_impl.dart`:
|
||||
- Line 104: `.location(hub.name)` - Hub name
|
||||
- Line 105: `.locationAddress(hub.address)` - Address
|
||||
|
||||
### Recommended Fix
|
||||
|
||||
Update `view_order_card.dart` to show hub name as primary, address as secondary:
|
||||
|
||||
```dart
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
order.location, // Hub name
|
||||
style: UiTypography.body2m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
order.locationAddress, // Address as subtitle
|
||||
style: UiTypography.footnote2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bug 3: Hub Delete UI Freeze
|
||||
|
||||
### Status: MEDIUM PRIORITY
|
||||
|
||||
### Description
|
||||
|
||||
After attempting to delete a hub that has orders (which correctly shows an error), subsequent delete attempts on ANY hub cause the UI to freeze with a loading overlay that never disappears.
|
||||
|
||||
### Screenshot
|
||||
|
||||
- `7.png` - Error message "Sorry this hub has orders, it can't be deleted."
|
||||
|
||||
### Root Cause
|
||||
|
||||
**File:** `apps/mobile/packages/features/client/hubs/lib/src/presentation/blocs/client_hubs_state.dart`
|
||||
|
||||
The `copyWith` method cannot reset `errorMessage` to `null` due to Dart's null-coalescing behavior:
|
||||
|
||||
```dart
|
||||
ClientHubsState copyWith({
|
||||
String? errorMessage,
|
||||
String? successMessage,
|
||||
// ...
|
||||
}) {
|
||||
return ClientHubsState(
|
||||
// BUG: null ?? this.errorMessage keeps the OLD value!
|
||||
errorMessage: errorMessage ?? this.errorMessage,
|
||||
successMessage: successMessage ?? this.successMessage,
|
||||
// ...
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Flow of the bug:**
|
||||
|
||||
1. Delete Hub A → Error "Sorry this hub has orders..."
|
||||
2. `_onMessageCleared` calls `copyWith(errorMessage: null)`
|
||||
3. `null ?? this.errorMessage` = OLD ERROR MESSAGE (not cleared!)
|
||||
4. Delete Hub B → Same error message
|
||||
5. `listenWhen` checks: `previous.errorMessage != current.errorMessage`
|
||||
6. Both are "Sorry this hub has orders..." → FALSE, listener not called
|
||||
7. `MessageCleared` never sent → Status never resets → Overlay stays forever
|
||||
|
||||
**Proof the team knows this pattern:** They correctly implemented `clearHubToIdentify` flag (lines 45, 53-54) but forgot to do the same for messages.
|
||||
|
||||
### Recommended Fix
|
||||
|
||||
**File:** `client_hubs_state.dart`
|
||||
|
||||
```dart
|
||||
ClientHubsState copyWith({
|
||||
ClientHubsStatus? status,
|
||||
List<Hub>? hubs,
|
||||
String? errorMessage,
|
||||
String? successMessage,
|
||||
bool? showAddHubDialog,
|
||||
Hub? hubToIdentify,
|
||||
bool clearHubToIdentify = false,
|
||||
bool clearErrorMessage = false, // ADD THIS
|
||||
bool clearSuccessMessage = false, // ADD THIS
|
||||
}) {
|
||||
return ClientHubsState(
|
||||
status: status ?? this.status,
|
||||
hubs: hubs ?? this.hubs,
|
||||
errorMessage: clearErrorMessage
|
||||
? null
|
||||
: (errorMessage ?? this.errorMessage),
|
||||
successMessage: clearSuccessMessage
|
||||
? null
|
||||
: (successMessage ?? this.successMessage),
|
||||
showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog,
|
||||
hubToIdentify: clearHubToIdentify
|
||||
? null
|
||||
: (hubToIdentify ?? this.hubToIdentify),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**File:** `client_hubs_bloc.dart` - Update `_onMessageCleared`:
|
||||
|
||||
```dart
|
||||
void _onMessageCleared(
|
||||
ClientHubsMessageCleared event,
|
||||
Emitter<ClientHubsState> emit,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
clearErrorMessage: true, // USE FLAG
|
||||
clearSuccessMessage: true, // USE FLAG
|
||||
status: state.status == ClientHubsStatus.actionSuccess ||
|
||||
state.status == ClientHubsStatus.actionFailure
|
||||
? ClientHubsStatus.success
|
||||
: state.status,
|
||||
),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bug 4: Order Display Shows Positions as Separate Orders
|
||||
|
||||
### Status: MEDIUM PRIORITY - DESIGN DECISION NEEDED
|
||||
|
||||
### Description
|
||||
|
||||
When creating an order with multiple positions (e.g., 1 Cook + 1 Bartender), the Orders list shows them as separate cards instead of a single order with multiple positions.
|
||||
|
||||
### Screenshot
|
||||
|
||||
- `8.png` - Shows "Cook - Boris Test Order 1" and "Bartender - Boris Test Order 1" as separate cards
|
||||
|
||||
### Analysis
|
||||
|
||||
**This is NOT a data bug.** The data model is correct:
|
||||
- 1 Order → 1 Shift → N ShiftRoles (positions)
|
||||
|
||||
The issue is in the **display logic**.
|
||||
|
||||
**File:** `apps/mobile/packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart`
|
||||
|
||||
Lines 32-89 query `ShiftRoles` and create one `OrderItem` per role:
|
||||
|
||||
```dart
|
||||
// Query returns ShiftRoles, not Orders
|
||||
final result = await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(...)
|
||||
.execute();
|
||||
|
||||
// Each ShiftRole becomes a card
|
||||
return result.data.shiftRoles.map((shiftRole) {
|
||||
return domain.OrderItem(
|
||||
title: '${shiftRole.role.name} - $eventName', // "Cook - Boris Test Order 1"
|
||||
// ...
|
||||
);
|
||||
}).toList();
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
**Option A: Keep current behavior (per-position cards)**
|
||||
- Pros: Granular view of each position's status
|
||||
- Cons: Confusing for users who think they created one order
|
||||
|
||||
**Option B: Group by Order (recommended)**
|
||||
- Pros: Matches user mental model
|
||||
- Cons: Requires refactoring view layer
|
||||
|
||||
### Recommended Fix (Option B)
|
||||
|
||||
```dart
|
||||
Future<List<domain.OrderItem>> getOrdersForRange({...}) async {
|
||||
final result = await _dataConnect
|
||||
.listShiftRolesByBusinessAndDateRange(...)
|
||||
.execute();
|
||||
|
||||
// Group ShiftRoles by Order ID
|
||||
final Map<String, List<ShiftRole>> orderGroups = {};
|
||||
for (final shiftRole in result.data.shiftRoles) {
|
||||
final orderId = shiftRole.shift.order.id;
|
||||
orderGroups.putIfAbsent(orderId, () => []);
|
||||
orderGroups[orderId]!.add(shiftRole);
|
||||
}
|
||||
|
||||
// Create one OrderItem per Order with positions summary
|
||||
return orderGroups.entries.map((entry) {
|
||||
final roles = entry.value;
|
||||
final firstRole = roles.first;
|
||||
final positionsSummary = roles.map((r) => r.role.name).join(', ');
|
||||
final totalWorkers = roles.fold(0, (sum, r) => sum + r.count);
|
||||
|
||||
return domain.OrderItem(
|
||||
id: entry.key,
|
||||
orderId: entry.key,
|
||||
title: firstRole.shift.order.eventName ?? 'Order',
|
||||
subtitle: positionsSummary, // "Cook, Bartender"
|
||||
workersNeeded: totalWorkers,
|
||||
// ... aggregate other fields
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bug 5: Mock Data in Production Views
|
||||
|
||||
### Status: INFORMATIONAL
|
||||
|
||||
### Description
|
||||
|
||||
The Coverage screen shows "Jose Salazar - Checked in at 9:00 AM" which appears to be test/mock data.
|
||||
|
||||
### Screenshot
|
||||
|
||||
- `9.png` - Daily Coverage showing mock worker data
|
||||
|
||||
### Recommendation
|
||||
|
||||
Ensure mock data is clearly separated and not visible in builds distributed for testing. Consider adding a visual indicator (e.g., "TEST DATA" banner) when using mock repositories.
|
||||
|
||||
---
|
||||
|
||||
## Architectural Issue: Error Handling
|
||||
|
||||
### Status: HIGH PRIORITY - TECHNICAL DEBT
|
||||
|
||||
### Description
|
||||
|
||||
The application exposes raw technical error messages to end users. This is unprofessional and potentially a security concern.
|
||||
|
||||
### Evidence
|
||||
|
||||
Found **60+ instances** of `throw Exception('technical message')` across the codebase:
|
||||
|
||||
```dart
|
||||
// Examples of problematic error messages shown to users:
|
||||
throw Exception('Authenticated user profile not found in database.');
|
||||
throw Exception('Business creation failed after Firebase user registration.');
|
||||
throw Exception('Staff profile not found for User ID: ${user.uid}');
|
||||
throw Exception('Failed to fetch certificates: $e'); // Exposes stack trace!
|
||||
throw Exception('Error signing out: ${e.toString()}');
|
||||
```
|
||||
|
||||
### Current State
|
||||
|
||||
- No centralized error handling system
|
||||
- No custom exception classes
|
||||
- Technical messages shown directly to users
|
||||
- No i18n support for error messages
|
||||
- No error codes for logging/tracking
|
||||
|
||||
### Recommended Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ DOMAIN LAYER │
|
||||
│ │
|
||||
│ packages/domain/lib/src/exceptions/app_exception.dart │
|
||||
│ │
|
||||
│ sealed class AppException implements Exception { │
|
||||
│ const AppException({required this.code, this.technical}); │
|
||||
│ final String code; // For logging: "AUTH_001" │
|
||||
│ final String? technical; // For devs only │
|
||||
│ String get messageKey; // For i18n: "errors.auth.x" │
|
||||
│ } │
|
||||
│ │
|
||||
│ class InvalidCredentialsException extends AuthException { │
|
||||
│ String get messageKey => 'errors.auth.invalid_credentials'; │
|
||||
│ } │
|
||||
│ │
|
||||
│ class HubHasOrdersException extends HubException { │
|
||||
│ String get messageKey => 'errors.hub.has_orders'; │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PRESENTATION LAYER │
|
||||
│ │
|
||||
│ } on AppException catch (e) { │
|
||||
│ log('Error ${e.code}: ${e.technical}'); // Dev logging │
|
||||
│ emit(state.copyWith(errorKey: e.messageKey)); │
|
||||
│ } catch (e) { │
|
||||
│ log('Unexpected: $e'); │
|
||||
│ emit(state.copyWith(errorKey: 'errors.generic.unknown')); │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ LOCALIZATION (i18n) │
|
||||
│ │
|
||||
│ errors: │
|
||||
│ auth: │
|
||||
│ invalid_credentials: "Email or password is incorrect" │
|
||||
│ account_exists: "An account with this email exists" │
|
||||
│ session_expired: "Please sign in again" │
|
||||
│ hub: │
|
||||
│ has_orders: "This hub has active orders" │
|
||||
│ order: │
|
||||
│ missing_hub: "Please select a location" │
|
||||
│ generic: │
|
||||
│ unknown: "Something went wrong. Please try again." │
|
||||
│ no_connection: "No internet connection" │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### Phase 1: Create Exception Classes (1 hour)
|
||||
|
||||
**Create:** `packages/domain/lib/src/exceptions/app_exception.dart`
|
||||
|
||||
```dart
|
||||
/// Base sealed class for all application exceptions.
|
||||
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
|
||||
// ============================================================
|
||||
|
||||
sealed class AuthException extends AppException {
|
||||
const AuthException({required super.code, super.technicalMessage});
|
||||
}
|
||||
|
||||
class InvalidCredentialsException extends AuthException {
|
||||
const InvalidCredentialsException({String? technicalMessage})
|
||||
: super(code: 'AUTH_001', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.auth.invalid_credentials';
|
||||
}
|
||||
|
||||
class AccountExistsException extends AuthException {
|
||||
const AccountExistsException({String? technicalMessage})
|
||||
: super(code: 'AUTH_002', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.auth.account_exists';
|
||||
}
|
||||
|
||||
class SessionExpiredException extends AuthException {
|
||||
const SessionExpiredException({String? technicalMessage})
|
||||
: super(code: 'AUTH_003', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.auth.session_expired';
|
||||
}
|
||||
|
||||
class UserNotFoundException extends AuthException {
|
||||
const UserNotFoundException({String? technicalMessage})
|
||||
: super(code: 'AUTH_004', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.auth.user_not_found';
|
||||
}
|
||||
|
||||
class UnauthorizedAppException extends AuthException {
|
||||
const UnauthorizedAppException({String? technicalMessage})
|
||||
: super(code: 'AUTH_005', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.auth.unauthorized_app';
|
||||
}
|
||||
|
||||
class WeakPasswordException extends AuthException {
|
||||
const WeakPasswordException({String? technicalMessage})
|
||||
: super(code: 'AUTH_006', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.auth.weak_password';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HUB EXCEPTIONS
|
||||
// ============================================================
|
||||
|
||||
sealed class HubException extends AppException {
|
||||
const HubException({required super.code, super.technicalMessage});
|
||||
}
|
||||
|
||||
class HubHasOrdersException extends HubException {
|
||||
const HubHasOrdersException({String? technicalMessage})
|
||||
: super(code: 'HUB_001', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.hub.has_orders';
|
||||
}
|
||||
|
||||
class HubNotFoundException extends HubException {
|
||||
const HubNotFoundException({String? technicalMessage})
|
||||
: super(code: 'HUB_002', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.hub.not_found';
|
||||
}
|
||||
|
||||
class HubCreationFailedException extends HubException {
|
||||
const HubCreationFailedException({String? technicalMessage})
|
||||
: super(code: 'HUB_003', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.hub.creation_failed';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// ORDER EXCEPTIONS
|
||||
// ============================================================
|
||||
|
||||
sealed class OrderException extends AppException {
|
||||
const OrderException({required super.code, super.technicalMessage});
|
||||
}
|
||||
|
||||
class OrderMissingHubException extends OrderException {
|
||||
const OrderMissingHubException({String? technicalMessage})
|
||||
: super(code: 'ORDER_001', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.order.missing_hub';
|
||||
}
|
||||
|
||||
class OrderMissingVendorException extends OrderException {
|
||||
const OrderMissingVendorException({String? technicalMessage})
|
||||
: super(code: 'ORDER_002', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.order.missing_vendor';
|
||||
}
|
||||
|
||||
class OrderCreationFailedException extends OrderException {
|
||||
const OrderCreationFailedException({String? technicalMessage})
|
||||
: super(code: 'ORDER_003', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.order.creation_failed';
|
||||
}
|
||||
|
||||
class ShiftCreationFailedException extends OrderException {
|
||||
const ShiftCreationFailedException({String? technicalMessage})
|
||||
: super(code: 'ORDER_004', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.order.shift_creation_failed';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROFILE EXCEPTIONS
|
||||
// ============================================================
|
||||
|
||||
sealed class ProfileException extends AppException {
|
||||
const ProfileException({required super.code, super.technicalMessage});
|
||||
}
|
||||
|
||||
class StaffProfileNotFoundException extends ProfileException {
|
||||
const StaffProfileNotFoundException({String? technicalMessage})
|
||||
: super(code: 'PROFILE_001', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.profile.staff_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';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NETWORK/GENERIC EXCEPTIONS
|
||||
// ============================================================
|
||||
|
||||
class NetworkException extends AppException {
|
||||
const NetworkException({String? technicalMessage})
|
||||
: super(code: 'NET_001', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.generic.no_connection';
|
||||
}
|
||||
|
||||
class UnknownException extends AppException {
|
||||
const UnknownException({String? technicalMessage})
|
||||
: super(code: 'UNKNOWN', technicalMessage: technicalMessage);
|
||||
|
||||
@override
|
||||
String get messageKey => 'errors.generic.unknown';
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 2: Add Localization Keys (30 minutes)
|
||||
|
||||
**Update:** `packages/core_localization/lib/src/l10n/strings.i18n.yaml`
|
||||
|
||||
```yaml
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
generic:
|
||||
unknown: "Something went wrong. Please try again."
|
||||
no_connection: "No internet connection. Please check your network and try again."
|
||||
```
|
||||
|
||||
#### Phase 3: Migrate Repositories (4-6 hours)
|
||||
|
||||
**Example migration for auth_repository_impl.dart:**
|
||||
|
||||
```dart
|
||||
// BEFORE
|
||||
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
|
||||
throw Exception('Incorrect email or password.');
|
||||
}
|
||||
|
||||
// AFTER
|
||||
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
|
||||
throw InvalidCredentialsException(
|
||||
technicalMessage: 'Firebase error: ${e.code}',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// BEFORE
|
||||
if (e.code == 'email-already-in-use') {
|
||||
throw Exception('An account already exists for that email address.');
|
||||
}
|
||||
|
||||
// AFTER
|
||||
if (e.code == 'email-already-in-use') {
|
||||
throw AccountExistsException(
|
||||
technicalMessage: 'Email: $email',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// BEFORE
|
||||
if (user == null) {
|
||||
throw Exception('Authenticated user profile not found in database.');
|
||||
}
|
||||
|
||||
// AFTER
|
||||
if (user == null) {
|
||||
throw UserNotFoundException(
|
||||
technicalMessage: 'Firebase UID: $firebaseUserId not found in users table',
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 4: Update BLoCs (2-3 hours)
|
||||
|
||||
**Example for client_hubs_bloc.dart:**
|
||||
|
||||
```dart
|
||||
// BEFORE
|
||||
Future<void> _onDeleteRequested(...) async {
|
||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||
try {
|
||||
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
|
||||
final List<Hub> hubs = await _getHubsUseCase();
|
||||
emit(state.copyWith(
|
||||
status: ClientHubsStatus.actionSuccess,
|
||||
hubs: hubs,
|
||||
successMessage: 'Hub deleted successfully',
|
||||
));
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
status: ClientHubsStatus.actionFailure,
|
||||
errorMessage: e.toString(), // EXPOSES TECHNICAL ERROR!
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// AFTER
|
||||
Future<void> _onDeleteRequested(...) async {
|
||||
emit(state.copyWith(status: ClientHubsStatus.actionInProgress));
|
||||
try {
|
||||
await _deleteHubUseCase(DeleteHubArguments(hubId: event.hubId));
|
||||
final List<Hub> hubs = await _getHubsUseCase();
|
||||
emit(state.copyWith(
|
||||
status: ClientHubsStatus.actionSuccess,
|
||||
hubs: hubs,
|
||||
successMessageKey: 'success.hub.deleted',
|
||||
));
|
||||
} on AppException catch (e) {
|
||||
// Log technical details for debugging
|
||||
debugPrint('Error ${e.code}: ${e.technicalMessage}');
|
||||
emit(state.copyWith(
|
||||
status: ClientHubsStatus.actionFailure,
|
||||
errorMessageKey: e.messageKey,
|
||||
));
|
||||
} catch (e, stackTrace) {
|
||||
// Unexpected error - log full details
|
||||
debugPrint('Unexpected error: $e\n$stackTrace');
|
||||
emit(state.copyWith(
|
||||
status: ClientHubsStatus.actionFailure,
|
||||
errorMessageKey: 'errors.generic.unknown',
|
||||
));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Phase 5: Update UI Layer (1-2 hours)
|
||||
|
||||
**Example for client_hubs_page.dart:**
|
||||
|
||||
```dart
|
||||
// BEFORE
|
||||
listener: (context, state) {
|
||||
if (state.errorMessage != null && state.errorMessage!.isNotEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(state.errorMessage!)),
|
||||
);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// AFTER
|
||||
listener: (context, state) {
|
||||
if (state.errorMessageKey != null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(t[state.errorMessageKey!])),
|
||||
);
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of Required Changes
|
||||
|
||||
### Immediate Fixes (This Sprint)
|
||||
|
||||
| File | Change | Priority |
|
||||
|------|--------|----------|
|
||||
| `client_hubs_state.dart` | Add `clearErrorMessage`/`clearSuccessMessage` flags | P2 |
|
||||
| `client_hubs_bloc.dart` | Use clear flags in `_onMessageCleared` | P2 |
|
||||
| `view_order_card.dart` | Display `location` instead of `locationAddress` | P2 |
|
||||
|
||||
### Short-term (Next Sprint)
|
||||
|
||||
| Task | Effort |
|
||||
|------|--------|
|
||||
| Create `AppException` sealed class in domain | 1h |
|
||||
| Add error localization keys | 30min |
|
||||
| Migrate auth repositories | 2h |
|
||||
| Migrate hub repositories | 1h |
|
||||
|
||||
### Medium-term (Next 2-3 Sprints)
|
||||
|
||||
| Task | Effort |
|
||||
|------|--------|
|
||||
| Migrate all repositories to AppException | 4-6h |
|
||||
| Update all BLoCs for proper error handling | 2-3h |
|
||||
| Update all UI components for localized errors | 1-2h |
|
||||
| Add admin tool to reconcile orphaned Firebase accounts | 4h |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Files Requiring Changes
|
||||
|
||||
### Repositories with `throw Exception()` to migrate:
|
||||
|
||||
1. `packages/features/client/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` (13 instances)
|
||||
2. `packages/features/client/hubs/lib/src/data/repositories_impl/hub_repository_impl.dart` (6 instances)
|
||||
3. `packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart` (6 instances)
|
||||
4. `packages/features/client/view_orders/lib/src/data/repositories/view_orders_repository_impl.dart` (2 instances)
|
||||
5. `packages/features/client/settings/lib/src/data/repositories_impl/settings_repository_impl.dart` (1 instance)
|
||||
6. `packages/features/staff/authentication/lib/src/data/repositories_impl/auth_repository_impl.dart` (7 instances)
|
||||
7. `packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart` (2 instances)
|
||||
8. `packages/features/staff/profile/lib/src/data/repositories/profile_repository_impl.dart` (3 instances)
|
||||
9. `packages/features/staff/availability/lib/src/data/repositories_impl/availability_repository_impl.dart` (2 instances)
|
||||
10. `packages/features/staff/clock_in/lib/src/data/repositories_impl/clock_in_repository_impl.dart` (3 instances)
|
||||
11. And 10+ more files in staff profile sections...
|
||||
|
||||
---
|
||||
|
||||
**Report prepared for development team review.**
|
||||
@@ -4,6 +4,10 @@
|
||||
|
||||
MOBILE_DIR := apps/mobile
|
||||
|
||||
# Device ID for running mobile apps (override with DEVICE=<id>)
|
||||
# Find your device ID with: flutter devices
|
||||
DEVICE ?= android
|
||||
|
||||
# --- General ---
|
||||
mobile-install: install-melos
|
||||
@echo "--> Bootstrapping mobile workspace (Melos)..."
|
||||
@@ -17,8 +21,8 @@ mobile-info:
|
||||
|
||||
# --- Client App ---
|
||||
mobile-client-dev-android:
|
||||
@echo "--> Running client app on Android..."
|
||||
@cd $(MOBILE_DIR) && melos run start:client -- -d android
|
||||
@echo "--> Running client app on Android (device: $(DEVICE))..."
|
||||
@cd $(MOBILE_DIR) && melos run start:client -- -d $(DEVICE)
|
||||
|
||||
mobile-client-build:
|
||||
@if [ -z "$(PLATFORM)" ]; then \
|
||||
@@ -33,8 +37,8 @@ mobile-client-build:
|
||||
|
||||
# --- Staff App ---
|
||||
mobile-staff-dev-android:
|
||||
@echo "--> Running staff app on Android..."
|
||||
@cd $(MOBILE_DIR) && melos run start:staff -- -d android
|
||||
@echo "--> Running staff app on Android (device: $(DEVICE))..."
|
||||
@cd $(MOBILE_DIR) && melos run start:staff -- -d $(DEVICE)
|
||||
|
||||
mobile-staff-build:
|
||||
@if [ -z "$(PLATFORM)" ]; then \
|
||||
|
||||