feat: Migrate staff profile features from Data Connect to V2 REST API

- Removed data_connect package from mobile pubspec.yaml.
- Added documentation for V2 profile migration status and QA findings.
- Implemented new session management with ClientSessionStore and StaffSessionStore.
- Created V2SessionService for handling user sessions via the V2 API.
- Developed use cases for cancelling late worker assignments and submitting worker reviews.
- Added arguments and use cases for payment chart retrieval and profile completion checks.
- Implemented repository interfaces and their implementations for staff main and profile features.
- Ensured proper error handling and validation in use cases.
This commit is contained in:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

@@ -52,4 +52,175 @@
- BenefitsOverviewPage also has CircularProgressIndicator (not shimmer-ified yet)
- ShiftDetailsPage has a dialog-level spinner in the "applying" dialog -- this is intentional, not a page loading state
- Hub details/edit pages use CircularProgressIndicator as action overlays (save/delete) -- keep as-is, not initial load
- Client home page has no loading spinner; it renders with default empty dashboard data
- Client home page uses shimmer skeleton during loading (ClientHomePageSkeleton + ClientHomeHeaderSkeleton)
## V2 API Migration Patterns
- `BaseApiService` is registered in `CoreModule` as a lazy singleton (injected as `i.get<BaseApiService>()`)
- `BaseApiService` type lives in `krow_domain`; `ApiService` impl lives in `krow_core`
- V2 endpoints: `V2ApiEndpoints.staffDashboard` etc. from `krow_core/core.dart`
- V2 domain shift entities: `TodayShift`, `AssignedShift`, `OpenShift` (separate from core `Shift`)
- V2 `Benefit`: uses `targetHours`/`trackedHours`/`remainingHours` (int) -- old used `entitlementHours`/`usedHours` (double)
- Staff dashboard endpoint returns all home data in one call (todaysShifts, tomorrowsShifts, recommendedShifts, benefits, staffName)
- Navigator has `toShiftDetailsById(String shiftId)` for cases where only the ID is available
- `StaffDashboard` entity updated to use typed lists: `List<TodayShift>`, `List<AssignedShift>`, `List<OpenShift>`
- Staff home feature migrated (Phase 2): removed krow_data_connect, firebase_data_connect, staff_shifts deps
- [V2 Profile Migration](project_v2_profile_migration.md) -- entity mappings and DI patterns for all profile sub-packages
- Staff clock-in migrated (Phase 3): repo impl → V2 API, removed Data Connect deps
- V2 `Shift` entity: `startsAt`/`endsAt` (DateTime), `locationName` (String?), no `startTime`/`endTime`/`clientName`/`hourlyRate`/`location`
- V2 `AttendanceStatus`: `isClockedIn` getter (not `isCheckedIn`), `clockInAt` (not `checkInTime`), no `checkOutTime`/`activeApplicationId`
- `AttendanceStatus` constructor requires `attendanceStatus: AttendanceStatusType.notClockedIn` for default
- Clock-out uses `shiftId` (not `applicationId`) -- V2 API resolves assignment from shiftId
- `listTodayShifts` endpoint returns `{ items: [...] }` with TodayShift-like shape (no lat/lng, hourlyRate, clientName)
- `getCurrentAttendanceStatus` returns flat object `{ activeShiftId, attendanceStatus, clockInAt }`
- Clock-in/out POST endpoints return `{ attendanceEventId, assignmentId, sessionId, status, validationStatus }` -- repo re-fetches status after
- Geofence: lat/lng not available from listTodayShifts or shiftDetail endpoints (lives on clock_points table, not returned by BE)
- `BaseApiService` not exported from `krow_core/core.dart` -- must import from `krow_domain/krow_domain.dart`
## Staff Shifts Feature Migration (Phase 3 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_auth`, `firebase_data_connect`, `geolocator`, `google_maps_flutter`, `meta`
- State uses 5 typed lists: `List<AssignedShift>`, `List<OpenShift>`, `List<PendingAssignment>`, `List<CancelledShift>`, `List<CompletedShift>`
- ShiftDetail (not Shift) used for detail page -- loaded by BLoC via API, not passed as route argument
- Money: `hourlyRateCents` (int) / 100 for display -- all V2 shift entities use cents
- Dates: All V2 entities have `DateTime` fields (not `String`) -- no more `DateTime.parse()` in widgets
- AssignmentStatus enum drives bottom bar logic (accepted=clock-in, assigned=accept/decline, null=apply)
- Old `Shift` entity still exists in domain but only used by clock-in feature -- shift list/detail pages use V2 entities
- ShiftDetailsModule route no longer receives `Shift` data argument -- uses `shiftId` param only
- `toShiftDetailsById(String)` is the standard navigation for V2 (no entity passing)
- Profile completion: moved into feature repo impl via `V2ApiEndpoints.staffProfileCompletion` + `ProfileCompletion.fromJson`
- Find Shifts tab: removed geolocator distance filter and multi-day grouping (V2 API handles server-side)
- Renamed use cases: `GetMyShiftsUseCase``GetAssignedShiftsUseCase`, `GetAvailableShiftsUseCase``GetOpenShiftsUseCase`, `GetHistoryShiftsUseCase``GetCompletedShiftsUseCase`, `GetShiftDetailsUseCase``GetShiftDetailUseCase`
## Client Home Feature Migration (Phase 4 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_data_connect`, `intl`
- V2 entities: `ClientDashboard` (replaces `HomeDashboardData`), `RecentOrder` (replaces `ReorderItem`)
- `ClientDashboard` contains nested `SpendingSummary`, `CoverageMetrics`, `LiveActivityMetrics`
- Money: `weeklySpendCents`, `projectedNext7DaysCents`, `averageShiftCostCents` (int) / 100 for display
- Two API calls: `GET /client/dashboard` (all metrics + user/biz info) and `GET /client/reorders` (returns `{ items: [...] }`)
- Removed `GetUserSessionDataUseCase` -- user/business info now part of `ClientDashboard`
- `LiveActivityWidget` rewritten from StatefulWidget with direct DC calls to StatelessWidget consuming BLoC state
- Dead code removed: `ShiftOrderFormSheet`, `ClientHomeSheets`, `CoverageDashboard` (all unused)
- `RecentOrder` entity: `id`, `title`, `date` (DateTime?), `hubName` (String?), `positionCount` (int), `orderType` (OrderType)
- Module imports `CoreModule()` (not `DataConnectModule()`), injects `BaseApiService` into repo
- State has `dashboard` (ClientDashboard?) with computed getters `businessName`, `userName`
- No photoUrl in V2 dashboard response -- header shows letter avatar only
## Client Billing Feature Migration (Phase 4 -- completed)
- Migrated from `krow_data_connect` + `BillingConnectorRepository` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_data_connect`
- Deleted old presentation models: `BillingInvoice`, `BillingWorkerRecord`, `SpendingBreakdownItem`
- V2 domain entities used directly: `Invoice`, `BillingAccount`, `SpendItem`, `CurrentBill`, `Savings`
- Old domain types removed: `BusinessBankAccount`, `InvoiceItem`, `InvoiceWorker`, `BillingPeriod` enum
- Money: all amounts in cents (int). State has computed `currentBillDollars`, `savingsDollars`, `spendTotalCents` getters
- `Invoice` V2 entity: `invoiceId`, `invoiceNumber`, `amountCents` (int), `status` (InvoiceStatus enum), `dueDate`, `paymentDate`, `vendorId`, `vendorName`
- `BillingAccount` V2 entity: `accountId`, `bankName`, `providerReference`, `last4`, `isPrimary`, `accountType` (AccountType enum)
- `SpendItem` V2 entity: `category`, `amountCents` (int), `percentage` (double) -- server-side aggregation by role
- Spend breakdown: replaced `BillingPeriod` enum with `BillingPeriodTab` (local) + `SpendBreakdownParams` (startDate/endDate ISO strings)
- API response shapes: list endpoints return `{ items: [...] }`, scalar endpoints spread data (`{ currentBillCents, requestId }`)
- Approve/dispute: POST to `V2ApiEndpoints.clientInvoiceApprove(id)` / `clientInvoiceDispute(id)`
- Completion review page: `BillingInvoice` replaced with `Invoice` -- worker-level data not available in V2 (widget placeholder)
- `InvoiceStatus` enum has `.value` property for display and `fromJson` factory with safe fallback to `unknown`
## Client Reports Feature Migration (Phase 4 -- completed)
- Migrated from `krow_data_connect` + `ReportsConnectorRepository` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`
- 7 report endpoints: summary, daily-ops, spend, coverage, forecast, performance, no-show
- Old `ReportsSummary` entity replaced with V2 `ReportSummary` (different fields: totalShifts, totalSpendCents, averageCoveragePercentage, averagePerformanceScore, noShowCount, forecastAccuracyPercentage)
- `businessId` removed from all events/repo -- V2 API resolves from auth token
- DailyOps: old `DailyOpsShift` replaced with `ShiftWithWorkers` (from coverage_domain). `TimeRange` has `startsAt`/`endsAt` (not `start`/`end`)
- Spend: `SpendReport` uses `totalSpendCents` (int), `chart` (List<SpendDataPoint> with `bucket`/`amountCents`), `breakdown` (List<SpendItem> with `category`/`amountCents`/`percentage`)
- Coverage: `CoverageReport` uses `averageCoveragePercentage`, `filledWorkers`, `neededWorkers`, `chart` (List<CoverageDayPoint> with `day`/`needed`/`filled`/`coveragePercentage`)
- Forecast: `ForecastReport` uses `forecastSpendCents`, `averageWeeklySpendCents`, `totalWorkerHours`, `weeks` (List<ForecastWeek> with `week`/`shiftCount`/`workerHours`/`forecastSpendCents`/`averageShiftCostCents`)
- Performance: V2 uses int percentages (`fillRatePercentage`, `completionRatePercentage`, `onTimeRatePercentage`) and `averageFillTimeMinutes` (double) -- convert to hours: `/60`
- NoShow: `NoShowReport` uses `totalNoShowCount`, `noShowRatePercentage`, `workersWhoNoShowed`, `items` (List<NoShowWorkerItem> with `staffId`/`staffName`/`incidentCount`/`riskStatus`/`incidents`)
- Module injects `BaseApiService` via `i.get<BaseApiService>()` -- no more `DataConnectModule` import
## Client Hubs Feature Migration (Phase 5 -- completed)
- Migrated from `krow_data_connect` + `HubsConnectorRepository` + `DataConnectService` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_auth`, `firebase_data_connect`, `http`
- V2 `Hub` entity: `hubId` (not `id`), `fullAddress` (not `address`), `costCenterId`/`costCenterName` (flat, not nested `CostCenter` object)
- V2 `CostCenter` entity: `costCenterId` (not `id`), `name` only (no `code` field)
- V2 `HubManager` entity: `managerAssignmentId`, `businessMembershipId`, `managerId`, `name`
- API response shapes: `GET /client/hubs` returns `{ items: [...] }`, `GET /client/cost-centers` returns `{ items: [...] }`
- Create/update return `{ hubId, created: true }` / `{ hubId, updated: true }` -- repo returns hubId String
- Delete: soft-delete (sets status=INACTIVE). Backend rejects if hub has active orders (409 HUB_DELETE_BLOCKED)
- Assign NFC: `POST /client/hubs/:hubId/assign-nfc` with `{ nfcTagId }`
- Module no longer imports `DataConnectModule()` -- `BaseApiService` available from parent `CoreModule()`
- `UpdateHubArguments.id` renamed to `UpdateHubArguments.hubId`; `CreateHubArguments.address` renamed to `.fullAddress`
- `HubDetailsDeleteRequested.id` renamed to `.hubId`; `EditHubAddRequested.address` renamed to `.fullAddress`
- Navigator still passes full `Hub` entity via route args (not just hubId)
## Client Orders Feature Migration (Phase 5 -- completed)
- 3 sub-packages migrated: `orders_common`, `view_orders`, `create_order`
- Removed deps: `krow_data_connect`, `firebase_data_connect`, `firebase_auth` from all; kept `intl` in create_order and orders_common
- V2 `OrderItem` entity: `itemId`, `orderId`, `orderType` (OrderType enum), `roleName`, `date` (DateTime), `startsAt`/`endsAt` (DateTime), `requiredWorkerCount`, `filledCount`, `hourlyRateCents`, `totalCostCents` (int cents), `locationName` (String?), `status` (ShiftStatus enum), `workers` (List<AssignedWorkerSummary>)
- Old entities deleted: `OneTimeOrder`, `RecurringOrder`, `PermanentOrder`, `ReorderData`, `OneTimeOrderHubDetails`, `RecurringOrderHubDetails`
- `AssignedWorkerSummary`: `applicationId` (String?), `workerName` (String? -- nullable!), `role` (String?), `confirmationStatus` (ApplicationStatus?)
- V2 `Vendor` entity: field is `companyName` (not `name`) -- old code used `vendor.name`
- V2 `ShiftStatus` enum: only has `draft`, `open`, `pendingConfirmation`, `assigned`, `active`, `completed`, `cancelled`, `unknown` -- no `filled`/`confirmed`/`pending`
- `OrderType` enum has `unknown` variant -- must handle in switch statements
- View orders: removed `GetAcceptedApplicationsForDayUseCase` -- V2 returns workers inline with order items
- View orders cubit: date filtering now uses `_isSameDay(DateTime, DateTime)` instead of string comparison
- Create order BLoCs: build `Map<String, dynamic>` V2 payloads instead of old entity objects
- V2 create endpoints: `POST /client/orders/one-time` (requires `orderDate`), `/recurring` (requires `startDate`/`endDate`/`recurrenceDays`), `/permanent` (requires `startDate`/`daysOfWeek`)
- V2 edit endpoint: `POST /client/orders/:orderId/edit` -- creates edited copy, cancels original
- V2 cancel endpoint: `POST /client/orders/:orderId/cancel` with optional `reason`
- Reorder uses `OrderPreview` (from `V2ApiEndpoints.clientOrderReorderPreview`) instead of old `ReorderData`
- `OrderPreview` has nested `OrderPreviewShift` > `OrderPreviewRole` structure
- Query repo: `getHubs()` replaces `getHubsByOwner(businessId)` -- V2 resolves business from auth token
- `OneTimeOrderPosition` is now a typedef for `OrderPositionUiModel` from `orders_common`
- `OrderEditSheet` (1700 lines) fully rewritten: delegates to `IViewOrdersRepository` instead of direct DC calls
## Staff Authentication Feature Migration (Phase 6 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` + `firebase_data_connect` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `krow_data_connect`, `firebase_data_connect`, `firebase_core`
- KEPT `firebase_auth` -- V2 backend `startStaffPhoneAuth` returns `CLIENT_FIREBASE_SDK` mode for mobile, meaning phone verification stays client-side via Firebase SDK
- Auth flow: Firebase SDK phone verify (client-side) -> get idToken -> `POST /auth/staff/phone/verify` with `{ idToken, mode }` -> V2 hydrates session (upserts user, loads actor context)
- V2 verify response: `{ sessionToken, refreshToken, expiresInSeconds, user: { id, email, displayName, phone }, staff: { staffId, tenantId, fullName, ... }, tenant, requiresProfileSetup }`
- `requiresProfileSetup` boolean replaces old signup logic (create user/staff via DC mutations)
- Profile setup: `POST /staff/profile/setup` with `{ fullName, bio, preferredLocations, maxDistanceMiles, industries, skills }`
- Sign out: `POST /auth/sign-out` (server-side token revocation) + local `FirebaseAuth.signOut()`
- `AuthInterceptor` in `DioClient` stays as-is -- attaches Firebase Bearer tokens to all V2 API requests
- `AuthInterceptor` in `DioClient` stays as-is -- attaches Firebase Bearer tokens to all V2 API requests
- Pre-existing issue: `ExperienceSkill` and `Industry` enums deleted from domain but still referenced in `profile_setup_experience.dart`
## Client Authentication Feature Migration (Phase 6 -- completed)
- Migrated from `krow_data_connect` + `DataConnectService` + `firebase_data_connect` to `BaseApiService` + `V2ApiEndpoints`
- Removed deps: `firebase_data_connect`, `firebase_core` from pubspec
- KEPT `firebase_auth` -- client-side sign-in needed so `AuthInterceptor` can attach Bearer tokens
- KEPT `krow_data_connect` -- only for `ClientSessionStore`/`ClientSession`/`ClientBusinessSession` (not yet extracted)
- Auth flow (Option A -- hybrid):
1. Firebase Auth client-side `signInWithEmailAndPassword` (sets `FirebaseAuth.instance.currentUser`)
2. `GET /auth/session` via V2 API (returns user + business + tenant context)
3. Populate `ClientSessionStore` from V2 session response
- Sign-up flow:
1. `POST /auth/client/sign-up` via V2 API (server-side: creates Firebase account + user/tenant/business/memberships in one transaction)
2. Local `signInWithEmailAndPassword` (sets local auth state)
3. `GET /auth/session` to load context + populate session store
- V2 session response shape: `{ user: { userId, email, displayName, phone, status }, business: { businessId, businessName, businessSlug, role, tenantId, membershipId }, tenant: {...}, vendor: null, staff: null }`
- Sign-out: `POST /auth/client/sign-out` (server-side revocation) + `FirebaseAuth.instance.signOut()` + `ClientSessionStore.instance.clear()`
- V2 sign-up error codes: `AUTH_PROVIDER_ERROR` with message containing `EMAIL_EXISTS` or `WEAK_PASSWORD`, `FORBIDDEN` for role mismatch
- Old Data Connect calls removed: `getUserById`, `getBusinessesByUserId`, `createBusiness`, `createUser`, `updateUser`, `deleteBusiness`
- Old rollback logic removed -- V2 API handles rollback server-side in one transaction
- Domain `User` entity: V2 uses `status: UserStatus` (not `role: String`) -- constructor: `User(id:, email:, displayName:, phone:, status:)`
- Module: `CoreModule()` (not `DataConnectModule()`), injects `BaseApiService` into `AuthRepositoryImpl`
## Client Settings Feature Migration (Phase 6 -- completed)
- Migrated sign-out from `DataConnectService.signOut()` to V2 API + local Firebase Auth
- Removed `DataConnectModule` import from module, replaced with `CoreModule()`
- `SettingsRepositoryImpl` now takes `BaseApiService` (not `DataConnectService`)
- Sign-out: `POST /auth/client/sign-out` + `FirebaseAuth.instance.signOut()` + `ClientSessionStore.instance.clear()`
- `settings_profile_header.dart` still reads from `ClientSessionStore` (now from `krow_core`)
## V2SessionService (Final Phase -- completed)
- `V2SessionService` singleton at `packages/core/lib/src/services/session/v2_session_service.dart`
- Replaces `DataConnectService` for session state management in both apps
- Uses `SessionHandlerMixin` from core (same interface as old DC version)
- `fetchUserRole()` calls `GET /auth/session` via `BaseApiService` (not DC connector)
- `signOut()` calls `POST /auth/sign-out` + `FirebaseAuth.signOut()` + `handleSignOut()`
- Registered in `CoreModule` via `i.addLazySingleton<V2SessionService>()` -- calls `setApiService()`
- Both `main.dart` files use `V2SessionService.instance.initializeAuthListener()` instead of `DataConnectService`
- Both `SessionListener` widgets subscribe to `V2SessionService.instance.onSessionStateChanged`
- `staff_main` package migrated: local repo/usecase via `V2ApiEndpoints.staffProfileCompletion` + `ProfileCompletion.fromJson`
- `krow_data_connect` removed from: staff app, client app, staff_main package pubspecs
- Session stores (`StaffSessionStore`, `ClientSessionStore`) now live in core, not data_connect

View File

@@ -0,0 +1,33 @@
---
name: V2 Profile Migration Status
description: Staff profile sub-packages migrated from Data Connect to V2 REST API - entity mappings and patterns
type: project
---
## Phase 2 Profile Migration (completed 2026-03-16)
All staff profile read features migrated from Firebase Data Connect to V2 REST API.
**Why:** Data Connect is being deprecated in favor of V2 REST API for all mobile backend access.
**How to apply:** When working on any profile feature, use `ApiService.get(V2ApiEndpoints.staffXxx)` not Data Connect connectors.
### Entity Mappings (old -> V2)
- `Staff` (old with name/avatar/totalShifts) -> `Staff` (V2 with fullName/metadata) + `StaffPersonalInfo` for profile form
- `EmergencyContact` (old with name/phone/relationship enum) -> `EmergencyContact` (V2 with fullName/phone/relationshipType string)
- `AttireItem` (removed) -> `AttireChecklist` (V2)
- `StaffDocument` (removed) -> `ProfileDocument` (V2)
- `StaffCertificate` (old with ComplianceType enum) -> `StaffCertificate` (V2 with certificateType string)
- `TaxForm` (old with I9TaxForm/W4TaxForm subclasses) -> `TaxForm` (V2 with formType string + fields map)
- `StaffBankAccount` (removed) -> `BankAccount` (V2)
- `TimeCard` (removed) -> `TimeCardEntry` (V2 with minutesWorked/totalPayCents)
- `PrivacySettings` (new V2 entity)
### Profile Main Page
- Old: 7+ individual completion use cases from data_connect connectors
- New: Single `ProfileRepositoryImpl.getProfileSections()` call returning `ProfileSectionStatus`
- Stats fields (totalShifts, onTimeRate, etc.) no longer on V2 Staff entity -- hardcoded to 0 pending dashboard API
### DI Pattern
- All repos inject `BaseApiService` from `CoreModule` (registered as `i.get<BaseApiService>()`)
- Modules import `CoreModule()` instead of `DataConnectModule()`

View File

@@ -2,3 +2,5 @@
## Project Context
- [project_clock_in_feature_issues.md](project_clock_in_feature_issues.md) — Critical bugs in staff clock_in feature: BLoC lifecycle leak, stale geofence override, dead lunch break data, non-functional date selector
- [project_client_v2_migration_issues.md](project_client_v2_migration_issues.md) — Critical bugs in client app V2 migration: reports BLoCs missing BlocErrorHandler, firebase_auth in features, no executeProtected, hardcoded strings, double sign-in
- [project_v2_migration_qa_findings.md](project_v2_migration_qa_findings.md) — Critical bugs in staff app V2 migration: cold-start session logout, geofence bypass, auth navigation race, token expiry inversion, shifts response shape mismatch

View File

@@ -0,0 +1,22 @@
---
name: Client V2 Migration QA Findings
description: Critical bugs and patterns found in the client app V2 API migration — covers auth, billing, coverage, home, hubs, orders, reports, settings
type: project
---
Client V2 migration QA analysis completed 2026-03-16. Key systemic issues found:
1. **Reports BLoCs missing BlocErrorHandler** — All 7 report BLoCs (spend, coverage, daily_ops, forecast, no_show, performance, summary) use raw try/catch instead of BlocErrorHandler mixin, risking StateError crashes if user navigates away during loading.
2. **firebase_auth in feature packages** — Both `client_authentication` and `client_settings` have `firebase_auth` in pubspec.yaml and import it in their repository implementations. Architecture rule says Firebase packages belong ONLY in `core`.
3. **No repository-level `executeProtected()` usage** — Zero client feature repos wrap API calls with `ApiErrorHandler.executeProtected()`. All rely solely on BLoC-level `handleError`. Timeout and network errors may surface as raw exceptions.
4. **Hardcoded strings scattered across home widgets**`live_activity_widget.dart`, `reorder_widget.dart`, `client_home_error_state.dart` contain English strings ("Today's Status", "Running Late", "positions", "An error occurred", "Retry") instead of localized keys.
5. **Double sign-in in auth flow** — signInWithEmail does V2 POST then Firebase signInWithEmailAndPassword. If V2 succeeds but Firebase fails (e.g. user disabled locally), the server thinks user is signed in but client throws.
6. **`context.t` vs `t` inconsistency** — Coverage feature uses `context.t.client_coverage.*` throughout, while home/billing use global `t.*`. Both work in Slang but inconsistency confuses maintainers.
**Why:** Migration from Data Connect to V2 REST API was a large-scale change touching all features simultaneously.
**How to apply:** When reviewing client features post-migration, check these specific patterns. Reports BLoCs are highest-risk for user-facing crashes.

View File

@@ -0,0 +1,27 @@
---
name: V2 API migration QA findings (staff app)
description: Critical bugs found during V2 API migration review of the staff mobile app — session cold-start logout, geofence bypass, auth race condition, token expiry inversion
type: project
---
V2 API migration introduced several critical bugs across the staff app (reviewed 2026-03-16).
**Why:** The migration from Firebase Data Connect to V2 REST API required rewiring every repository, session service, and entity. Some integration gaps were missed.
**Key findings (severity order):**
1. **Cold-start session logout**`V2SessionService.initializeAuthListener()` is called in `main.dart` before `CoreModule` injects `ApiService`. On cold start, `fetchUserRole` finds `_apiService == null`, returns null, and emits `unauthenticated`, logging the user out.
2. **Geofence coordinates always null**`ClockInRepositoryImpl._mapTodayShiftJsonToShift` defaults latitude/longitude to null because the V2 endpoint doesn't return them. Geofence validation is completely bypassed for all shifts.
3. **Auth navigation race** — After OTP verify, both `PhoneVerificationPage` BlocListener and `SessionListener` try to navigate (one to profile setup, the other to home). Creates unpredictable navigation.
4. **Token expiry check inverted**`session_handler_mixin.dart` line 215: `now.difference(expiryTime)` should be `expiryTime.difference(now)`. Tokens are only "refreshed" after they've already expired.
5. **Shifts response shape mismatch**`shifts_repository_impl.dart` casts `response.data as List<dynamic>` but other repos use `response.data['items']`. Needs validation against actual V2 contract.
6. **Attire blocking poll**`attire_repository_impl.dart` polls verification status for up to 10 seconds on main isolate with no UI feedback.
7. **`firebase_auth` in feature package** — `auth_repository_impl.dart` directly imports firebase_auth. Architecture rules require firebase_auth only in core.
**How to apply:** When reviewing future V2 migration PRs, check: (a) session init ordering, (b) response shape matches between repos and API, (c) nullable field defaults in entity mapping, (d) navigation race conditions between SessionListener and feature BlocListeners.

View File

@@ -26,6 +26,7 @@ and load any additional skills as needed for specific review challenges.
- Ensuring business logic lives in use cases (not BLoCs/widgets)
- Flagging design system violations (hardcoded colors, TextStyle, spacing, icons)
- Validating BLoC pattern usage (SessionHandlerMixin, BlocErrorHandler, singleton registration)
- **Verifying every feature module that uses `BaseApiService` (or any CoreModule binding) declares `List<Module> get imports => <Module>[CoreModule()];`** — missing this causes `UnregisteredInstance` runtime crashes
- Ensuring safe navigation extensions are used (no direct Navigator usage)
- Verifying test coverage for business logic
- Checking documentation on public APIs
@@ -205,6 +206,7 @@ Produce a structured report in this exact format:
|------|--------|---------|
| Design System | ✅/❌ | [details] |
| Architecture Boundaries | ✅/❌ | [details] |
| DI / CoreModule Imports | ✅/❌ | [Every module using BaseApiService must import CoreModule] |
| State Management | ✅/❌ | [details] |
| Navigation | ✅/❌ | [details] |
| Testing Coverage | ✅/❌ | [estimated %] |

View File

@@ -43,10 +43,11 @@ If any of these files are missing or unreadable, notify the user before proceedi
- Import icon libraries directly — use `UiIcons`
- Use `Navigator.push` directly — use Modular safe extensions
- Navigate without home fallback
- Call DataConnect directly from BLoCs — go through repository
- Call API directly from BLoCs — go through repository
- Skip tests for business logic
### ALWAYS:
- **Add `CoreModule` import to every feature module that uses `BaseApiService` or any other `CoreModule` binding** (e.g., `FileUploadService`, `DeviceFileUploadService`, `CameraService`). Without this, the DI container throws `UnregisteredInstance` at runtime. Add: `@override List<Module> get imports => <Module>[CoreModule()];`
- **Use `package:` imports everywhere inside `lib/`** for consistency and robustness. Use relative imports only in `test/` and `bin/` directories. Example: `import 'package:staff_clock_in/src/presentation/bloc/clock_in/clock_in_bloc.dart';` not `import '../bloc/clock_in/clock_in_bloc.dart';`
- Place reusable utility functions (math, geo, formatting, etc.) in `apps/mobile/packages/core/lib/src/utils/` and export from `core.dart` — never keep them as private methods in feature packages
- Use feature-first packaging: `domain/`, `data/`, `presentation/`
@@ -72,7 +73,7 @@ If any of these files are missing or unreadable, notify the user before proceedi
The mobile apps are migrating from Firebase Data Connect (direct DB) to V2 REST API. Follow these rules for ALL new and migrated features:
### Backend Access
- **Use `ApiService.get/post/put/delete`** for ALL backend calls — NEVER use Data Connect connectors
- **Use `ApiService.get/post/put/delete`** for ALL backend calls
- Import `ApiService` from `package:krow_core/core.dart`
- Use `V2ApiEndpoints` from `package:krow_core/core.dart` for endpoint URLs
- V2 API docs are at `docs/BACKEND/API_GUIDES/V2/` — check response shapes before writing code
@@ -87,7 +88,7 @@ The mobile apps are migrating from Firebase Data Connect (direct DB) to V2 REST
- **RepoImpl lives in the feature package** at `data/repositories/`
- **Feature-level domain layer is optional** — only add `domain/` when the feature has use cases, validators, or feature-specific interfaces
- **Simple features** (read-only, no business logic) = just `data/` + `presentation/`
- Do NOT import from `packages/data_connect/` — it is deprecated
- Do NOT import from `packages/data_connect/` — deleted
### Status & Type Enums
All status/type fields from the V2 API must use Dart enums, NOT raw strings. Parse at the `fromJson` boundary with a safe fallback:
@@ -169,7 +170,7 @@ Follow these steps in order for every feature implementation:
- Create barrel file exporting the domain public API
### 4. Data Layer
- Implement repository classes using `ApiService` with `V2ApiEndpoints` — NOT DataConnectService
- Implement repository classes using `ApiService` with `V2ApiEndpoints`
- Parse V2 API JSON responses into domain entities via `Entity.fromJson()`
- Map errors to domain `Failure` types
- Create barrel file for data layer
@@ -266,7 +267,7 @@ After completing implementation, prepare a handoff summary including:
As you work on features, update your agent memory with discoveries about:
- Existing feature patterns and conventions in the codebase
- Session store usage patterns and available stores
- DataConnect query/mutation names and their locations
- V2 API endpoint patterns and response shapes
- Design token values and component patterns actually in use
- Module registration patterns and route conventions
- Recurring issues found during `melos analyze`

View File

@@ -22,8 +22,8 @@ You are working within a Flutter monorepo (where features are organized into pac
- **State Management**: Flutter BLoC/Cubit. BLoCs registered with `i.add()` (transient), never `i.addSingleton()`. `BlocProvider.value()` for shared BLoCs.
- **DI & Routing**: Flutter Modular. Safe navigation via `safeNavigate()`, `safePush()`, `popSafe()`. Never `Navigator.push()` directly (except when popping a dialog).
- **Error Handling**: `BlocErrorHandler` mixin with `_safeEmit()` to prevent StateError on disposed BLoCs.
- **Backend**: Firebase Data Connect through `data_connect` package Connectors. `_service.run(() => connector.<query>().execute())` for auth/token management.
- **Session Management**: `SessionHandlerMixin` + `SessionListener` widget.
- **Backend**: V2 REST API via `ApiService` with `V2ApiEndpoints`. Domain entities have `fromJson`/`toJson`. Status fields use typed enums from `krow_domain`. Money values are `int` in cents.
- **Session Management**: `V2SessionService` + `SessionHandlerMixin` + `SessionListener` widget. Session stores (`StaffSessionStore`, `ClientSessionStore`) in `core`.
- **Localization**: Slang (`t.section.key`), not `context.strings`.
- **Design System**: Tokens from `UiColors`, `UiTypography`, `UiConstants`. No hardcoded values.
@@ -55,7 +55,7 @@ Detect potential bugs including:
- **API integration issues**: Missing error handling, incorrect data mapping, async issues
- **Performance concerns**: Inefficient algorithms, unnecessary rebuilds, memory problems
- **Security vulnerabilities**: Hardcoded credentials, insecure data storage, authentication gaps
- **Architecture violations**: Features importing other features, business logic in BLoCs/widgets, Firebase packages outside `data_connect`
- **Architecture violations**: Features importing other features, business logic in BLoCs/widgets, Firebase packages outside `core`, direct Dio usage instead of `ApiService`
- **Data persistence issues**: Cache invalidation, concurrent access
## Analysis Methodology
@@ -64,7 +64,7 @@ Detect potential bugs including:
1. Map the feature's architecture and key screens
2. Identify critical user flows and navigation paths
3. Review state management implementation (BLoC states, events, transitions)
4. Understand data models and API contracts via Data Connect connectors
4. Understand data models and API contracts via V2 API endpoints
5. Document assumptions and expected behaviors
### Phase 2: Use Case Extraction
@@ -108,7 +108,7 @@ Analyze code for:
- Missing error handling in `.then()` chains
- Mounted checks missing in async callbacks
- Race conditions in concurrent requests
- Missing `_service.run()` wrapper for Data Connect calls
- Missing `ApiErrorHandler.executeProtected()` wrapper for API calls
### Background Tasks & WorkManager
When reviewing code that uses WorkManager or background task scheduling, check these edge cases:
@@ -140,7 +140,9 @@ When reviewing code that uses WorkManager or background task scheduling, check t
### Architecture Rules
- Features importing other features directly
- Business logic in BLoCs or widgets instead of Use Cases
- Firebase packages used outside `data_connect` package
- Firebase packages (`firebase_auth`) used outside `core` package
- Direct Dio/HTTP usage instead of `ApiService` with `V2ApiEndpoints`
- Importing deleted `krow_data_connect` package
- `context.read<T>()` instead of `ReadContext(context).read<T>()`
## Output Format

View File

@@ -1,6 +1,6 @@
---
name: krow-mobile-architecture
description: KROW mobile app Clean Architecture implementation including package structure, dependency rules, feature isolation, BLoC lifecycle management, session handling, and Data Connect connectors pattern. Use this when architecting new mobile features, debugging state management issues, preventing prop drilling, managing BLoC disposal, implementing session stores, or setting up connector repositories. Essential for maintaining architectural integrity across staff and client apps.
description: KROW mobile app Clean Architecture implementation including package structure, dependency rules, feature isolation, BLoC lifecycle management, session handling, and V2 REST API integration. Use this when architecting new mobile features, debugging state management issues, preventing prop drilling, managing BLoC disposal, implementing session stores, or setting up API repository patterns. Essential for maintaining architectural integrity across staff and client apps.
---
# KROW Mobile Architecture
@@ -13,7 +13,7 @@ This skill defines the authoritative mobile architecture for the KROW platform.
- Debugging state management or BLoC lifecycle issues
- Preventing prop drilling in UI code
- Managing session state and authentication
- Implementing Data Connect connector repositories
- Implementing V2 API repository patterns
- Setting up feature modules and dependency injection
- Understanding package boundaries and dependencies
- Refactoring legacy code to Clean Architecture
@@ -46,13 +46,14 @@ KROW follows **Clean Architecture** in a **Melos Monorepo**. Dependencies flow *
│ both depend on
┌─────────────────▼───────────────────────────────────────┐
│ Services (Interface Adapters) │
│ • data_connect: Backend integration, session mgmt
• core: Extensions, base classes, utilities
│ • core: API service, session management, device
services, utilities, extensions, base classes │
└─────────────────┬───────────────────────────────────────┘
both depend on
│ depends on
┌─────────────────▼───────────────────────────────────────┐
│ Domain (Stable Core) │
│ • Entities (immutable data models)
│ • Entities (data models with fromJson/toJson)
│ • Enums (shared enumerations) │
│ • Failures (domain-specific errors) │
│ • Pure Dart only, zero Flutter dependencies │
└─────────────────────────────────────────────────────────┘
@@ -69,9 +70,9 @@ KROW follows **Clean Architecture** in a **Melos Monorepo**. Dependencies flow *
**Responsibilities:**
- Initialize Flutter Modular
- Assemble features into navigation tree
- Inject concrete implementations (from `data_connect`) into features
- Inject concrete implementations into features
- Configure environment-specific settings (dev/stage/prod)
- Initialize session management
- Initialize session management via `V2SessionService`
**Structure:**
```
@@ -119,21 +120,22 @@ features/staff/profile/
**Key Principles:**
- **Presentation:** UI Pages and Widgets, BLoCs/Cubits for state
- **Application:** Use Cases (business logic orchestration)
- **Data:** Repository implementations (backend integration)
- **Data:** Repository implementations using `ApiService` with `V2ApiEndpoints`
- **Pages as StatelessWidget:** Move state to BLoCs for better performance and testability
- **Feature-level domain is optional:** Only needed when the feature has business logic (use cases, validators). Simple features can have just `data/` + `presentation/`.
**RESTRICTION:** Features MUST NOT import other features. Communication happens via:
- Shared domain entities
- Session stores (`StaffSessionStore`, `ClientSessionStore`)
- Navigation via Modular
- Data Connect connector repositories
### 2.3 Domain (`apps/mobile/packages/domain`)
**Role:** The stable, pure heart of the system
**Responsibilities:**
- Define **Entities** (immutable data models using Data Classes or Freezed)
- Define **Entities** (data models with `fromJson`/`toJson` for V2 API serialization)
- Define **Enums** (shared enumerations in `entities/enums/`)
- Define **Failures** (domain-specific error types)
**Structure:**
@@ -144,11 +146,17 @@ domain/
│ ├── entities/
│ │ ├── user.dart
│ │ ├── staff.dart
│ │ ── shift.dart
└── failures/
│ ├── failure.dart # Base class
├── auth_failure.dart
└── network_failure.dart
│ │ ── shift.dart
│ └── enums/
├── staff_status.dart
└── order_type.dart
├── failures/
│ │ ├── failure.dart # Base class
│ │ ├── auth_failure.dart
│ │ └── network_failure.dart
│ └── core/
│ └── services/api_services/
│ └── base_api_service.dart
└── pubspec.yaml
```
@@ -161,68 +169,120 @@ class Staff extends Equatable {
final String name;
final String email;
final StaffStatus status;
const Staff({
required this.id,
required this.name,
required this.email,
required this.status,
});
factory Staff.fromJson(Map<String, dynamic> json) {
return Staff(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
status: StaffStatus.values.byName(json['status'] as String),
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'status': status.name,
};
@override
List<Object?> get props => [id, name, email, status];
}
```
**RESTRICTION:**
**RESTRICTION:**
- NO Flutter dependencies (no `import 'package:flutter/material.dart'`)
- NO `json_annotation` or serialization code
- Only `equatable` for value equality
- Pure Dart only
- `fromJson`/`toJson` live directly on entities (no separate DTOs or adapters)
### 2.4 Data Connect (`apps/mobile/packages/data_connect`)
### 2.4 Core (`apps/mobile/packages/core`)
**Role:** Interface Adapter for Backend Access
**Role:** Cross-cutting concerns, API infrastructure, session management, device services, and utilities
**Responsibilities:**
- Centralized connector repositories (see Data Connect Connectors Pattern section)
- Implement Firebase Data Connect service layer
- Map Domain Entities ↔ Data Connect generated code
- Handle Firebase exceptions → domain failures
- Provide `DataConnectService` with session management
- `ApiService` — HTTP client wrapper around Dio with consistent response/error handling
- `V2ApiEndpoints` — All V2 REST API endpoint constants
- `DioClient` — Pre-configured Dio with `AuthInterceptor` and `IdempotencyInterceptor`
- `AuthInterceptor` — Automatically attaches Firebase Auth ID token to requests
- `IdempotencyInterceptor` — Adds `Idempotency-Key` header to POST/PUT/DELETE requests
- `ApiErrorHandler` mixin — Maps API errors to domain failures
- `SessionHandlerMixin` — Handles auth state, token refresh, role validation
- `V2SessionService` — Manages session lifecycle, replaces legacy DataConnectService
- Session stores (`StaffSessionStore`, `ClientSessionStore`)
- Device services (camera, gallery, location, notifications, storage, etc.)
- Extension methods (`NavigationExtensions`, `ListExtensions`, etc.)
- Base classes (`UseCase`, `Failure`, `BlocErrorHandler`)
- Logger configuration
- `AppConfig` — Environment-specific configuration (API base URLs, keys)
**Structure:**
```
data_connect/
core/
├── lib/
│ ├── src/
│ ├── services/
│ │ ├── data_connect_service.dart # Core service
── mixins/
│ │ └── session_handler_mixin.dart
├── connectors/ # Connector pattern (see below)
│ ├── staff/
│ │ ├── domain/
│ │ │ │ ├── repositories/
│ │ │ │ │ ── staff_connector_repository.dart
│ │ │ ── usecases/
│ │ │ └── get_profile_completion_usecase.dart
│ │ ── data/
│ │ │ │ └── repositories/
│ │ │ │ └── staff_connector_repository_impl.dart
│ │ │ ── order/
│ │ │ └── shifts/
│ └── session/
│ │ ├── staff_session_store.dart
│ │ ── client_session_store.dart
└── krow_data_connect.dart # Exports
│ ├── core.dart # Barrel exports
└── src/
├── config/
── app_config.dart # Env-specific config (V2_API_BASE_URL, etc.)
└── app_environment.dart
├── services/
│ ├── api_service/
│ │ ├── api_service.dart # ApiService (get/post/put/patch/delete)
│ │ ├── dio_client.dart # Pre-configured Dio
│ │ ── inspectors/
│ │ │ ── auth_interceptor.dart
│ │ │ └── idempotency_interceptor.dart
│ │ ── mixins/
│ │ │ ├── api_error_handler.dart
│ │ │ └── session_handler_mixin.dart
│ │ ── core_api_services/
│ │ ├── v2_api_endpoints.dart
│ │ ├── core_api_endpoints.dart
│ │ ├── file_upload/
│ │ ── signed_url/
│ │ ├── llm/
│ │ │ ├── verification/
│ │ │ └── rapid_order/
│ │ ├── session/
│ │ │ ├── v2_session_service.dart
│ │ │ ├── staff_session_store.dart
│ │ │ └── client_session_store.dart
│ │ └── device/
│ │ ├── camera/
│ │ ├── gallery/
│ │ ├── location/
│ │ ├── notification/
│ │ ├── storage/
│ │ └── background_task/
│ ├── presentation/
│ │ ├── mixins/
│ │ │ └── bloc_error_handler.dart
│ │ └── observers/
│ │ └── core_bloc_observer.dart
│ ├── routing/
│ │ └── routing.dart
│ ├── domain/
│ │ ├── arguments/
│ │ └── usecases/
│ └── utils/
│ ├── date_time_utils.dart
│ ├── geo_utils.dart
│ └── time_utils.dart
└── pubspec.yaml
```
**RESTRICTION:**
- NO feature-specific logic
- Connectors are domain-neutral and reusable
- All queries follow Clean Architecture (domain interfaces → data implementations)
- Core services are domain-neutral and reusable
- All V2 API access goes through `ApiService` — never use raw Dio directly in features
### 2.5 Design System (`apps/mobile/packages/design_system`)
@@ -274,13 +334,13 @@ design_system/
**Feature Integration:**
```dart
// CORRECT: Access via Slang's global `t` accessor
// CORRECT: Access via Slang's global `t` accessor
import 'package:core_localization/core_localization.dart';
Text(t.client_create_order.review.invalid_arguments)
Text(t.errors.order.creation_failed)
// FORBIDDEN: Hardcoded user-facing strings
// FORBIDDEN: Hardcoded user-facing strings
Text('Invalid review arguments') // Must use localized key
Text('Order created!') // Must use localized key
```
@@ -313,62 +373,86 @@ BlocProvider<LocaleBloc>(
)
```
### 2.7 Core (`apps/mobile/packages/core`)
**Role:** Cross-cutting concerns
**Responsibilities:**
- Extension methods (NavigationExtensions, ListExtensions, etc.)
- Base classes (UseCase, Failure, BlocErrorHandler)
- Logger configuration
- Result types for functional error handling
## 3. Dependency Direction Rules
1. **Domain Independence:** `domain` knows NOTHING about outer layers
- Defines *what* needs to be done, not *how*
- Pure Dart, zero Flutter dependencies
- Stable contracts that rarely change
- Entities include `fromJson`/`toJson` for practical V2 API serialization
2. **UI Agnosticism:** Features depend on `design_system` for UI and `domain` for logic
- Features do NOT know about Firebase or backend details
- Features do NOT know about HTTP/Dio details
- Backend changes don't affect feature implementation
3. **Data Isolation:** `data_connect` depends on `domain` to know interfaces
- Implements domain repository interfaces
- Maps backend models to domain entities
3. **Data Isolation:** Feature `data/` layer depends on `core` for API access and `domain` for entities
- RepoImpl uses `ApiService` with `V2ApiEndpoints`
- Maps JSON responses to domain entities via `Entity.fromJson()`
- Does NOT know about UI
**Dependency Flow:**
```
Apps → Features → Design System
→ Core Localization
Data Connect → Domain
→ Core
Core → Domain
```
## 4. Data Connect Service & Session Management
## 4. V2 API Service & Session Management
### 4.1 Session Handler Mixin
### 4.1 ApiService
**Location:** `apps/mobile/packages/data_connect/lib/src/services/mixins/session_handler_mixin.dart`
**Location:** `apps/mobile/packages/core/lib/src/services/api_service/api_service.dart`
**Responsibilities:**
- Automatic token refresh (triggered when <5 minutes to expiry)
- Firebase auth state listening
- Role-based access validation
- Session state stream emissions
- 3-attempt retry with exponential backoff (1s → 2s → 4s)
- Wraps Dio HTTP methods (GET, POST, PUT, PATCH, DELETE)
- Consistent response parsing via `ApiResponse`
- Consistent error handling (maps `DioException` to `ApiResponse` with V2 error envelope)
**Key Usage:**
```dart
final ApiService apiService;
// GET request
final response = await apiService.get(
V2ApiEndpoints.staffDashboard,
params: {'date': '2026-01-15'},
);
// POST request
final response = await apiService.post(
V2ApiEndpoints.staffClockIn,
data: {'shiftId': shiftId, 'latitude': lat, 'longitude': lng},
);
```
### 4.2 DioClient & Interceptors
**Location:** `apps/mobile/packages/core/lib/src/services/api_service/dio_client.dart`
**Pre-configured with:**
- `AuthInterceptor` — Automatically attaches Firebase Auth ID token as `Bearer` token
- `IdempotencyInterceptor` — Adds `Idempotency-Key` (UUID v4) to POST/PUT/DELETE requests
- `LogInterceptor` — Logs request/response bodies for debugging
### 4.3 V2SessionService
**Location:** `apps/mobile/packages/core/lib/src/services/session/v2_session_service.dart`
**Responsibilities:**
- Manages session lifecycle (initialize, refresh, invalidate)
- Fetches session data from V2 API on auth state change
- Populates session stores with user/role data
- Provides session state stream for `SessionListener`
**Key Method:**
```dart
// Call once on app startup
DataConnectService.instance.initializeAuthListener(
V2SessionService.instance.initializeAuthListener(
allowedRoles: ['STAFF', 'BOTH'], // or ['CLIENT', 'BUSINESS', 'BOTH']
);
```
### 4.2 Session Listener Widget
### 4.4 Session Listener Widget
**Location:** `apps/mobile/apps/<app>/lib/src/widgets/session_listener.dart`
@@ -381,13 +465,13 @@ DataConnectService.instance.initializeAuthListener(
```dart
// main.dart
runApp(
SessionListener( // Critical wrapper
SessionListener( // Critical wrapper
child: ModularApp(module: AppModule(), child: AppWidget()),
),
);
```
### 4.3 Repository Pattern with Data Connect
### 4.5 Repository Pattern with V2 API
**Step 1:** Define interface in feature domain:
```dart
@@ -397,32 +481,33 @@ abstract interface class ProfileRepositoryInterface {
}
```
**Step 2:** Implement using `DataConnectService.run()`:
**Step 2:** Implement using `ApiService` with `V2ApiEndpoints`:
```dart
// features/staff/profile/lib/src/data/repositories_impl/
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
final DataConnectService _service = DataConnectService.instance;
final ApiService _apiService;
ProfileRepositoryImpl({required ApiService apiService})
: _apiService = apiService;
@override
Future<Staff> getProfile(String id) async {
return await _service.run(() async {
final response = await _service.connector
.getStaffById(id: id)
.execute();
return _mapToStaff(response.data.staff);
});
final response = await _apiService.get(
V2ApiEndpoints.staffSession,
params: {'staffId': id},
);
return Staff.fromJson(response.data as Map<String, dynamic>);
}
}
```
**Benefits of `_service.run()`:**
- Auto validates user is authenticated
- ✅ Refreshes token if <5 min to expiry
- ✅ Executes the query
- ✅ 3-attempt retry with exponential backoff
- ✅ Maps exceptions to domain failures
**Benefits of `ApiService` + interceptors:**
- AuthInterceptor auto-attaches Firebase Auth token
- IdempotencyInterceptor prevents duplicate writes
- Consistent error handling via `ApiResponse`
- No manual token management in features
### 4.4 Session Store Pattern
### 4.6 Session Store Pattern
After successful auth, populate session stores:
@@ -451,9 +536,10 @@ ClientSessionStore.instance.setSession(
```dart
final session = StaffSessionStore.instance.session;
if (session?.staff == null) {
final staff = await getStaffById(session!.user.uid);
final response = await apiService.get(V2ApiEndpoints.staffSession);
final staff = Staff.fromJson(response.data as Map<String, dynamic>);
StaffSessionStore.instance.setSession(
session.copyWith(staff: staff),
session!.copyWith(staff: staff),
);
}
```
@@ -463,12 +549,12 @@ if (session?.staff == null) {
### Zero Direct Imports
```dart
// FORBIDDEN
// FORBIDDEN
import 'package:staff_profile/staff_profile.dart'; // in another feature
// ALLOWED
// ALLOWED
import 'package:krow_domain/krow_domain.dart'; // shared domain
import 'package:krow_core/krow_core.dart'; // shared utilities
import 'package:krow_core/krow_core.dart'; // shared utilities + API
import 'package:design_system/design_system.dart'; // shared UI
```
@@ -485,7 +571,7 @@ extension NavigationExtensions on IModularNavigator {
await navigate('/home'); // Fallback
}
}
/// Safely push with fallback to home
Future<T?> safePush<T>(String route) async {
try {
@@ -495,7 +581,7 @@ extension NavigationExtensions on IModularNavigator {
return null;
}
}
/// Safely pop with guard against empty stack
void popSafe() {
if (canPop()) {
@@ -512,23 +598,23 @@ extension NavigationExtensions on IModularNavigator {
// apps/mobile/apps/staff/lib/src/navigation/staff_navigator.dart
extension StaffNavigator on IModularNavigator {
Future<void> toStaffHome() => safeNavigate(StaffPaths.home);
Future<void> toShiftDetails(String shiftId) =>
Future<void> toShiftDetails(String shiftId) =>
safePush('${StaffPaths.shifts}/$shiftId');
Future<void> toProfileEdit() => safePush(StaffPaths.profileEdit);
}
```
**Usage in Features:**
```dart
// CORRECT
// CORRECT
Modular.to.toStaffHome();
Modular.to.toShiftDetails(shiftId: '123');
Modular.to.popSafe();
// AVOID
Modular.to.navigate('/home'); // No safety
// AVOID
Modular.to.navigate('/profile'); // No safety
Navigator.push(...); // No Modular integration
```
@@ -536,9 +622,9 @@ Navigator.push(...); // No Modular integration
Features don't share state directly. Use:
1. **Domain Repositories:** Centralized data sources
1. **Domain Repositories:** Centralized data sources via `ApiService`
2. **Session Stores:** `StaffSessionStore`, `ClientSessionStore` for app-wide context
3. **Event Streams:** If needed, via `DataConnectService` streams
3. **Event Streams:** If needed, via `V2SessionService` streams
4. **Navigation Arguments:** Pass IDs, not full objects
## 6. App-Specific Session Management
@@ -549,11 +635,11 @@ Features don't share state directly. Use:
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
DataConnectService.instance.initializeAuthListener(
V2SessionService.instance.initializeAuthListener(
allowedRoles: ['STAFF', 'BOTH'],
);
runApp(
SessionListener(
child: ModularApp(module: StaffAppModule(), child: StaffApp()),
@@ -564,11 +650,11 @@ void main() async {
**Session Store:** `StaffSessionStore`
- Fields: `user`, `staff`, `ownerId`
- Lazy load: `getStaffById()` if staff is null
- Lazy load: fetch from `V2ApiEndpoints.staffSession` if staff is null
**Navigation:**
- Authenticated `Modular.to.toStaffHome()`
- Unauthenticated `Modular.to.toInitialPage()`
- Authenticated -> `Modular.to.toStaffHome()`
- Unauthenticated -> `Modular.to.toInitialPage()`
### Client App
@@ -576,11 +662,11 @@ void main() async {
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
DataConnectService.instance.initializeAuthListener(
V2SessionService.instance.initializeAuthListener(
allowedRoles: ['CLIENT', 'BUSINESS', 'BOTH'],
);
runApp(
SessionListener(
child: ModularApp(module: ClientAppModule(), child: ClientApp()),
@@ -591,137 +677,138 @@ void main() async {
**Session Store:** `ClientSessionStore`
- Fields: `user`, `business`
- Lazy load: `getBusinessById()` if business is null
- Lazy load: fetch from `V2ApiEndpoints.clientSession` if business is null
**Navigation:**
- Authenticated `Modular.to.toClientHome()`
- Unauthenticated `Modular.to.toInitialPage()`
- Authenticated -> `Modular.to.toClientHome()`
- Unauthenticated -> `Modular.to.toInitialPage()`
## 7. Data Connect Connectors Pattern
## 7. V2 API Repository Pattern
**Problem:** Without connectors, each feature duplicates backend queries.
**Problem:** Without a consistent pattern, each feature handles HTTP differently.
**Solution:** Centralize all backend queries in `data_connect/connectors/`.
**Solution:** Feature RepoImpl uses `ApiService` with `V2ApiEndpoints`, returning domain entities via `Entity.fromJson()`.
### Structure
Mirror backend connector structure:
Repository implementations live in the feature package:
```
data_connect/lib/src/connectors/
├── staff/
features/staff/profile/
├── lib/src/
│ ├── domain/
│ │ ── repositories/
│ │ └── staff_connector_repository.dart # Interface
│ └── usecases/
│ │ └── get_profile_completion_usecase.dart
└── data/
└── repositories/
└── staff_connector_repository_impl.dart # Implementation
├── order/
├── shifts/
└── user/
│ │ ── repositories/
│ │ └── profile_repository_interface.dart # Interface
├── data/
│ │ └── repositories_impl/
│ └── profile_repository_impl.dart # Implementation
│ └── presentation/
└── blocs/
│ └── profile_cubit.dart
```
**Maps to backend:**
```
backend/dataconnect/connector/
├── staff/
├── order/
├── shifts/
└── user/
```
### Repository Interface
### Clean Architecture in Connectors
**Domain Interface:**
```dart
// staff_connector_repository.dart
abstract interface class StaffConnectorRepository {
Future<bool> getProfileCompletion();
Future<Staff> getStaffById(String id);
// profile_repository_interface.dart
abstract interface class ProfileRepositoryInterface {
Future<Staff> getProfile();
Future<void> updatePersonalInfo(Map<String, dynamic> data);
Future<List<ProfileSection>> getProfileSections();
}
```
**Use Case:**
```dart
// get_profile_completion_usecase.dart
class GetProfileCompletionUseCase {
final StaffConnectorRepository _repository;
GetProfileCompletionUseCase({required StaffConnectorRepository repository})
: _repository = repository;
Future<bool> call() => _repository.getProfileCompletion();
}
```
### Repository Implementation
**Data Implementation:**
```dart
// staff_connector_repository_impl.dart
class StaffConnectorRepositoryImpl implements StaffConnectorRepository {
final DataConnectService _service;
// profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
final ApiService _apiService;
ProfileRepositoryImpl({required ApiService apiService})
: _apiService = apiService;
@override
Future<bool> getProfileCompletion() async {
return _service.run(() async {
final staffId = await _service.getStaffId();
final response = await _service.connector
.getStaffProfileCompletion(id: staffId)
.execute();
return _isProfileComplete(response);
});
Future<Staff> getProfile() async {
final response = await _apiService.get(V2ApiEndpoints.staffSession);
final data = response.data as Map<String, dynamic>;
return Staff.fromJson(data['staff'] as Map<String, dynamic>);
}
@override
Future<void> updatePersonalInfo(Map<String, dynamic> data) async {
await _apiService.put(
V2ApiEndpoints.staffPersonalInfo,
data: data,
);
}
@override
Future<List<ProfileSection>> getProfileSections() async {
final response = await _apiService.get(V2ApiEndpoints.staffProfileSections);
final list = response.data['sections'] as List<dynamic>;
return list
.map((e) => ProfileSection.fromJson(e as Map<String, dynamic>))
.toList();
}
}
```
### Feature Integration
### Feature Module Integration
**Step 1:** Feature registers connector repository:
```dart
// staff_main_module.dart
class StaffMainModule extends Module {
// profile_module.dart
class ProfileModule extends Module {
@override
void binds(Injector i) {
i.addLazySingleton<StaffConnectorRepository>(
StaffConnectorRepositoryImpl.new,
i.addLazySingleton<ProfileRepositoryInterface>(
() => ProfileRepositoryImpl(apiService: i.get<ApiService>()),
);
i.addLazySingleton(
() => GetProfileCompletionUseCase(
repository: i.get<StaffConnectorRepository>(),
() => GetProfileUseCase(
repository: i.get<ProfileRepositoryInterface>(),
),
);
i.addLazySingleton(
() => StaffMainCubit(
getProfileCompletionUsecase: i.get(),
() => ProfileCubit(
getProfileUseCase: i.get(),
),
);
}
}
```
**Step 2:** BLoC uses it:
### BLoC Usage
```dart
class StaffMainCubit extends Cubit<StaffMainState> {
final GetProfileCompletionUseCase _getProfileCompletionUsecase;
Future<void> loadProfileCompletion() async {
final isComplete = await _getProfileCompletionUsecase();
emit(state.copyWith(isProfileComplete: isComplete));
class ProfileCubit extends Cubit<ProfileState> with BlocErrorHandler<ProfileState> {
final GetProfileUseCase _getProfileUseCase;
Future<void> loadProfile() async {
emit(state.copyWith(status: ProfileStatus.loading));
await handleError(
emit: emit,
action: () async {
final profile = await _getProfileUseCase();
emit(state.copyWith(status: ProfileStatus.loaded, profile: profile));
},
onError: (errorKey) => state.copyWith(status: ProfileStatus.error),
);
}
}
```
### Benefits
**No Duplication** - Query implemented once, used by many features
**Single Source of Truth** - Backend change → update one place
**Reusability** - Any feature can use any connector
**Testability** - Mock connector repo to test features
**Scalability** - Easy to add connectors as backend grows
- **No Duplication** — Endpoint constants defined once in `V2ApiEndpoints`
- **Consistent Auth** — `AuthInterceptor` handles token attachment automatically
- **Idempotent Writes** — `IdempotencyInterceptor` prevents duplicate mutations
- **Domain Purity** — Entities use `fromJson`/`toJson` directly, no mapping layers
- **Testability** — Mock `ApiService` to test RepoImpl in isolation
- **Scalability** — Add new endpoints to `V2ApiEndpoints`, implement in feature RepoImpl
## 8. Avoiding Prop Drilling: Direct BLoC Access
@@ -730,23 +817,23 @@ class StaffMainCubit extends Cubit<StaffMainState> {
Passing data through intermediate widgets creates maintenance burden:
```dart
// BAD: Prop drilling
// BAD: Prop drilling
ProfilePage(status: status)
ProfileHeader(status: status)
ProfileLevelBadge(status: status) // Only widget that needs it
-> ProfileHeader(status: status)
-> ProfileLevelBadge(status: status) // Only widget that needs it
```
### The Solution: BlocBuilder in Leaf Widgets
```dart
// GOOD: Direct BLoC access
// GOOD: Direct BLoC access
class ProfileLevelBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<ProfileCubit, ProfileState>(
builder: (context, state) {
if (state.profile == null) return const SizedBox.shrink();
final level = _mapStatusToLevel(state.profile!.status);
return LevelBadgeUI(level: level);
},
@@ -765,9 +852,9 @@ class ProfileLevelBadge extends StatelessWidget {
**Decision Tree:**
```
Does this widget need data?
├─ YES, leaf widget Use BlocBuilder
├─ YES, container Use BlocBuilder in child
└─ NO Don't add prop
├─ YES, leaf widget -> Use BlocBuilder
├─ YES, container -> Use BlocBuilder in child
└─ NO -> Don't add prop
```
## 9. BLoC Lifecycle & State Emission Safety
@@ -780,7 +867,7 @@ StateError: Cannot emit new states after calling close
```
**Root Causes:**
1. Transient BLoCs created with `BlocProvider(create:)` disposed prematurely
1. Transient BLoCs created with `BlocProvider(create:)` -> disposed prematurely
2. Multiple BlocProviders disposing same singleton
3. User navigates away during async operation
@@ -789,26 +876,26 @@ StateError: Cannot emit new states after calling close
#### Step 1: Register as Singleton
```dart
// GOOD: Singleton registration
// GOOD: Singleton registration
i.addLazySingleton<ProfileCubit>(
() => ProfileCubit(useCase1, useCase2),
);
// BAD: Creates new instance each time
// BAD: Creates new instance each time
i.add(ProfileCubit.new);
```
#### Step 2: Use BlocProvider.value()
```dart
// GOOD: Reuse singleton
// GOOD: Reuse singleton
final cubit = Modular.get<ProfileCubit>();
BlocProvider<ProfileCubit>.value(
value: cubit,
child: MyWidget(),
)
// BAD: Creates duplicate
// BAD: Creates duplicate
BlocProvider<ProfileCubit>(
create: (_) => Modular.get<ProfileCubit>(),
child: MyWidget(),
@@ -839,13 +926,13 @@ mixin BlocErrorHandler<S> on Cubit<S> {
class ProfileCubit extends Cubit<ProfileState> with BlocErrorHandler<ProfileState> {
Future<void> loadProfile() async {
emit(state.copyWith(status: ProfileStatus.loading));
await handleError(
emit: emit,
action: () async {
final profile = await getProfile();
emit(state.copyWith(status: ProfileStatus.loaded, profile: profile));
// Safe even if BLoC disposed
// Safe even if BLoC disposed
},
onError: (errorKey) => state.copyWith(status: ProfileStatus.error),
);
@@ -863,43 +950,48 @@ class ProfileCubit extends Cubit<ProfileState> with BlocErrorHandler<ProfileStat
## 10. Anti-Patterns to Avoid
**Feature imports feature**
- **Feature imports feature**
```dart
import 'package:staff_profile/staff_profile.dart'; // in another feature
```
**Business logic in BLoC**
- **Business logic in BLoC**
```dart
on<LoginRequested>((event, emit) {
if (event.email.isEmpty) { // Use case responsibility
if (event.email.isEmpty) { // Use case responsibility
emit(AuthError('Email required'));
}
});
```
**Direct Data Connect in features**
- **Direct HTTP/Dio in features (use ApiService)**
```dart
final response = await FirebaseDataConnect.instance.query(); // Use repository
final response = await Dio().get('https://api.example.com/staff'); // Use ApiService
```
**Global state variables**
- **Importing krow_data_connect (deprecated package)**
```dart
User? currentUser; // Use SessionStore
import 'package:krow_data_connect/krow_data_connect.dart'; // Use krow_core instead
```
**Direct Navigator.push**
- **Global state variables**
```dart
Navigator.push(context, MaterialPageRoute(...)); // Use Modular
User? currentUser; // Use SessionStore
```
**Hardcoded navigation**
- **Direct Navigator.push**
```dart
Modular.to.navigate('/profile'); // Use safe extensions
Navigator.push(context, MaterialPageRoute(...)); // Use Modular
```
**Hardcoded user-facing strings**
- **Hardcoded navigation**
```dart
Text('Order created successfully!'); // Use t.section.key from core_localization
Modular.to.navigate('/profile'); // Use safe extensions
```
- **Hardcoded user-facing strings**
```dart
Text('Order created successfully!'); // Use t.section.key from core_localization
```
## Summary
@@ -907,17 +999,20 @@ Text('Order created successfully!'); // ← Use t.section.key from core_localiz
The architecture enforces:
- **Clean Architecture** with strict layer boundaries
- **Feature Isolation** via zero cross-feature imports
- **Session Management** via DataConnectService and SessionListener
- **Connector Pattern** for reusable backend queries
- **V2 REST API** integration via `ApiService`, `V2ApiEndpoints`, and interceptors
- **Session Management** via `V2SessionService`, session stores, and `SessionListener`
- **Repository Pattern** with feature-local RepoImpl using `ApiService`
- **BLoC Lifecycle** safety with singletons and safe emit
- **Navigation Safety** with typed navigators and fallbacks
When implementing features:
1. Follow package structure strictly
2. Use connector repositories for backend access
3. Register BLoCs as singletons with `.value()`
4. Use safe navigation extensions
5. Avoid prop drilling with direct BLoC access
6. Keep domain pure and stable
2. Use `ApiService` with `V2ApiEndpoints` for all backend access
3. Domain entities use `fromJson`/`toJson` for V2 API serialization
4. RepoImpl lives in the feature `data/` layer, not a shared package
5. Register BLoCs as singletons with `.value()`
6. Use safe navigation extensions
7. Avoid prop drilling with direct BLoC access
8. Keep domain pure and stable
Architecture is not negotiable. When in doubt, refer to existing well-structured features or ask for clarification.

View File

@@ -1,6 +1,6 @@
---
name: krow-mobile-development-rules
description: Enforce KROW mobile app development standards including file structure, naming conventions, logic placement boundaries, localization, Data Connect integration, and prototype migration rules. Use this skill whenever working on KROW Flutter mobile features, creating new packages, implementing BLoCs, integrating with backend, or migrating from prototypes. Critical for maintaining clean architecture and preventing architectural degradation.
description: Enforce KROW mobile app development standards including file structure, naming conventions, logic placement boundaries, localization, V2 REST API integration, and prototype migration rules. Use this skill whenever working on KROW Flutter mobile features, creating new packages, implementing BLoCs, integrating with backend, or migrating from prototypes. Critical for maintaining clean architecture and preventing architectural degradation.
---
# KROW Mobile Development Rules
@@ -11,7 +11,7 @@ These rules are **NON-NEGOTIABLE** enforcement guidelines for the KROW mobile ap
- Creating new mobile features or packages
- Implementing BLoCs, Use Cases, or Repositories
- Integrating with Firebase Data Connect backend
- Integrating with V2 REST API backend
- Migrating code from prototypes
- Reviewing mobile code for compliance
- Setting up new feature modules
@@ -186,15 +186,17 @@ class _LoginPageState extends State<LoginPage> {
```dart
// profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
ProfileRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
final BaseApiService _apiService;
@override
Future<Staff> getProfile(String id) async {
final response = await dataConnect.getStaffById(id: id).execute();
// Data transformation happens here
return Staff(
id: response.data.staff.id,
name: response.data.staff.name,
// Map Data Connect model to Domain entity
final ApiResponse response = await _apiService.get(
V2ApiEndpoints.staffProfile(id),
);
// Data transformation happens here
return Staff.fromJson(response.data as Map<String, dynamic>);
}
}
```
@@ -252,19 +254,19 @@ Modular.to.pop(); // ← Can crash if stack is empty
**PATTERN:** All navigation MUST have fallback to Home page. Safe extensions automatically handle this.
### Session Management → DataConnectService + SessionHandlerMixin
### Session Management → V2SessionService + SessionHandlerMixin
**✅ CORRECT:**
```dart
// In main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize session listener (pick allowed roles for app)
DataConnectService.instance.initializeAuthListener(
V2SessionService.instance.initializeAuthListener(
allowedRoles: ['STAFF', 'BOTH'], // for staff app
);
runApp(
SessionListener( // Wraps entire app
child: ModularApp(module: AppModule(), child: AppWidget()),
@@ -274,28 +276,24 @@ void main() async {
// In repository:
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
final DataConnectService _service = DataConnectService.instance;
ProfileRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
final BaseApiService _apiService;
@override
Future<Staff> getProfile(String id) async {
// _service.run() handles:
// - Auth validation
// - Token refresh (if <5 min to expiry)
// - Error handling with 3 retries
return await _service.run(() async {
final response = await _service.connector
.getStaffById(id: id)
.execute();
return _mapToStaff(response.data.staff);
});
final ApiResponse response = await _apiService.get(
V2ApiEndpoints.staffProfile(id),
);
return Staff.fromJson(response.data as Map<String, dynamic>);
}
}
```
**PATTERN:**
- **SessionListener** widget wraps app and shows dialogs for session errors
- **SessionHandlerMixin** in `DataConnectService` provides automatic token refresh
- **3-attempt retry logic** with exponential backoff (1s → 2s → 4s)
- **V2SessionService** provides automatic token refresh and auth management
- **ApiService** handles HTTP requests with automatic auth headers
- **Role validation** configurable per app
## 4. Localization Integration (core_localization)
@@ -372,7 +370,7 @@ class AppModule extends Module {
@override
List<Module> get imports => [
LocalizationModule(), // ← Required
DataConnectModule(),
CoreModule(),
];
}
@@ -387,44 +385,51 @@ runApp(
);
```
## 5. Data Connect Integration
## 5. V2 API Integration
All backend access goes through `DataConnectService`.
All backend access goes through `ApiService` with `V2ApiEndpoints`.
### Repository Pattern
**Step 1:** Define interface in feature domain:
**Step 1:** Define interface in feature domain (optional — feature-level domain layer is optional if entities from `krow_domain` suffice):
```dart
// domain/repositories/profile_repository_interface.dart
abstract interface class ProfileRepositoryInterface {
Future<Staff> getProfile(String id);
Future<bool> updateProfile(Staff profile);
// domain/repositories/shifts_repository_interface.dart
abstract interface class ShiftsRepositoryInterface {
Future<List<AssignedShift>> getAssignedShifts();
Future<AssignedShift> getShiftById(String id);
}
```
**Step 2:** Implement using `DataConnectService.run()`:
**Step 2:** Implement using `ApiService` + `V2ApiEndpoints`:
```dart
// data/repositories_impl/profile_repository_impl.dart
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
final DataConnectService _service = DataConnectService.instance;
// data/repositories_impl/shifts_repository_impl.dart
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
ShiftsRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
final BaseApiService _apiService;
@override
Future<Staff> getProfile(String id) async {
return await _service.run(() async {
final response = await _service.connector
.getStaffById(id: id)
.execute();
return _mapToStaff(response.data.staff);
});
Future<List<AssignedShift>> getAssignedShifts() async {
final ApiResponse response = await _apiService.get(V2ApiEndpoints.staffShiftsAssigned);
final List<dynamic> items = response.data['items'] as List<dynamic>;
return items.map((dynamic json) => AssignedShift.fromJson(json as Map<String, dynamic>)).toList();
}
@override
Future<AssignedShift> getShiftById(String id) async {
final ApiResponse response = await _apiService.get(V2ApiEndpoints.staffShift(id));
return AssignedShift.fromJson(response.data as Map<String, dynamic>);
}
}
```
**Benefits of `_service.run()`:**
- ✅ Automatic auth validation
- ✅ Token refresh if needed
- ✅ 3-attempt retry with exponential backoff
- ✅ Consistent error handling
### Key Conventions
- **Domain entities** have `fromJson` / `toJson` factory methods for serialization
- **Status fields** use enums from `krow_domain` (e.g., `ShiftStatus`, `OrderStatus`)
- **Money** is represented in cents as `int` (never `double`)
- **Timestamps** are `DateTime` objects (parsed from ISO 8601 strings)
- **Feature-level domain layer** is optional when `krow_domain` entities cover the need
### Session Store Pattern
@@ -448,7 +453,7 @@ ClientSessionStore.instance.setSession(
);
```
**Lazy Loading:** If session is null, fetch via `getStaffById()` or `getBusinessById()` and update store.
**Lazy Loading:** If session is null, fetch via the appropriate `ApiService.get()` endpoint and update store.
## 6. Prototype Migration Rules
@@ -462,7 +467,7 @@ When migrating from `prototypes/`:
### ❌ MUST REJECT & REFACTOR
- `GetX`, `Provider`, or `MVC` patterns
- Any state management not using BLoC
- Direct HTTP calls (must use Data Connect)
- Direct HTTP calls (must use ApiService with V2ApiEndpoints)
- Hardcoded colors/typography (must use design system)
- Global state variables
- Navigation without Modular
@@ -491,13 +496,12 @@ If requirements are unclear:
### DO NOT
- Add 3rd party packages without checking `apps/mobile/packages/core` first
- Add `firebase_auth` or `firebase_data_connect` to Feature packages (they belong in `data_connect` only)
- Use `addSingleton` for BLoCs (always use `add` method in Modular)
- Add `firebase_auth` or `firebase_data_connect` to Feature packages (they belong in `core` only)
### DO
- Use `DataConnectService.instance` for backend operations
- Use `ApiService` with `V2ApiEndpoints` for backend operations
- Use Flutter Modular for dependency injection
- Register BLoCs with `i.addSingleton<CubitType>(() => CubitType(...))`
- Register BLoCs with `i.add<CubitType>(() => CubitType(...))` (transient)
- Register Use Cases as factories or singletons as needed
## 9. Error Handling Pattern
@@ -516,15 +520,12 @@ class InvalidCredentialsFailure extends AuthFailure {
### Repository Error Mapping
```dart
// Map Data Connect exceptions to Domain failures
// Map API errors to Domain failures using ApiErrorHandler
try {
final response = await dataConnect.query();
return Right(response);
} on DataConnectException catch (e) {
if (e.message.contains('unauthorized')) {
return Left(InvalidCredentialsFailure());
}
return Left(ServerFailure(e.message));
final response = await _apiService.get(V2ApiEndpoints.staffProfile(id));
return Right(Staff.fromJson(response.data as Map<String, dynamic>));
} catch (e) {
return Left(ApiErrorHandler.mapToFailure(e));
}
```
@@ -579,7 +580,7 @@ testWidgets('shows loading indicator when logging in', (tester) async {
```
### Integration Tests
- Test full feature flows end-to-end with Data Connect
- Test full feature flows end-to-end with V2 API
- Use dependency injection to swap implementations if needed
## 11. Clean Code Principles
@@ -635,12 +636,12 @@ Before merging any mobile feature code:
- [ ] Zero analyzer warnings
### Integration
- [ ] Data Connect queries via `_service.run()`
- [ ] V2 API calls via `ApiService` + `V2ApiEndpoints`
- [ ] Error handling with domain failures
- [ ] Proper dependency injection in modules
## Summary
The key principle: **Clean Architecture with zero tolerance for violations.** Business logic in Use Cases, state in BLoCs, data access in Repositories, UI in Widgets. Features are isolated, backend is centralized, localization is mandatory, and design system is immutable.
The key principle: **Clean Architecture with zero tolerance for violations.** Business logic in Use Cases, state in BLoCs, data access in Repositories (via `ApiService` + `V2ApiEndpoints`), UI in Widgets. Features are isolated, backend access is centralized through the V2 REST API layer, localization is mandatory, and design system is immutable.
When in doubt, refer to existing features following these patterns or ask for clarification. It's better to ask than to introduce architectural debt.