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

@@ -34,6 +34,11 @@ export 'src/services/api_service/core_api_services/verification/verification_res
export 'src/services/api_service/core_api_services/rapid_order/rapid_order_service.dart';
export 'src/services/api_service/core_api_services/rapid_order/rapid_order_response.dart';
// Session Management
export 'src/services/session/client_session_store.dart';
export 'src/services/session/staff_session_store.dart';
export 'src/services/session/v2_session_service.dart';
// Device Services
export 'src/services/device/camera/camera_service.dart';
export 'src/services/device/gallery/gallery_service.dart';

View File

@@ -18,6 +18,15 @@ class CoreModule extends Module {
// 2. Register the base API service
i.addLazySingleton<BaseApiService>(() => ApiService(i.get<Dio>()));
// 2b. Wire the V2 session service with the API service.
// This uses a post-registration callback so the singleton gets
// its dependency as soon as the injector resolves BaseApiService.
i.addLazySingleton<V2SessionService>(() {
final V2SessionService service = V2SessionService.instance;
service.setApiService(i.get<BaseApiService>());
return service;
});
// 3. Register Core API Services (Orchestrators)
i.addLazySingleton<FileUploadService>(
() => FileUploadService(i.get<BaseApiService>()),

View File

@@ -98,6 +98,13 @@ extension StaffNavigator on IModularNavigator {
safeNavigate(StaffPaths.shiftDetails(shift.id), arguments: shift);
}
/// Navigates to shift details by ID only (no pre-fetched [Shift] object).
///
/// Used when only the shift ID is available (e.g. from dashboard list items).
void toShiftDetailsById(String shiftId) {
safeNavigate(StaffPaths.shiftDetails(shiftId));
}
void toPersonalInfo() {
safePush(StaffPaths.onboardingPersonalInfo);
}
@@ -118,7 +125,7 @@ extension StaffNavigator on IModularNavigator {
safeNavigate(StaffPaths.attire);
}
void toAttireCapture({required AttireItem item, String? initialPhotoUrl}) {
void toAttireCapture({required AttireChecklist item, String? initialPhotoUrl}) {
safeNavigate(
StaffPaths.attireCapture,
arguments: <String, dynamic>{
@@ -132,7 +139,7 @@ extension StaffNavigator on IModularNavigator {
safeNavigate(StaffPaths.documents);
}
void toDocumentUpload({required StaffDocument document, String? initialUrl}) {
void toDocumentUpload({required ProfileDocument document, String? initialUrl}) {
safeNavigate(
StaffPaths.documentUpload,
arguments: <String, dynamic>{

View File

@@ -158,7 +158,7 @@ mixin SessionHandlerMixin {
final Duration timeUntilExpiry = expiryTime.difference(now);
if (timeUntilExpiry <= _refreshThreshold) {
await user.getIdTokenResult();
await user.getIdTokenResult(true);
}
_lastTokenRefreshTime = now;
@@ -212,9 +212,9 @@ mixin SessionHandlerMixin {
final firebase_auth.IdTokenResult idToken =
await user.getIdTokenResult();
if (idToken.expirationTime != null &&
DateTime.now().difference(idToken.expirationTime!) <
idToken.expirationTime!.difference(DateTime.now()) <
const Duration(minutes: 5)) {
await user.getIdTokenResult();
await user.getIdTokenResult(true);
}
_emitSessionState(SessionState.authenticated(userId: user.uid));

View File

@@ -0,0 +1,28 @@
import 'package:krow_domain/krow_domain.dart' show ClientSession;
/// Singleton store for the authenticated client's session context.
///
/// Holds a [ClientSession] (V2 domain entity) populated after sign-in via the
/// V2 session API. Features read from this store to access business context
/// without re-fetching from the backend.
class ClientSessionStore {
ClientSessionStore._();
/// The global singleton instance.
static final ClientSessionStore instance = ClientSessionStore._();
ClientSession? _session;
/// The current client session, or `null` if not authenticated.
ClientSession? get session => _session;
/// Replaces the current session with [session].
void setSession(ClientSession session) {
_session = session;
}
/// Clears the stored session (e.g. on sign-out).
void clear() {
_session = null;
}
}

View File

@@ -0,0 +1,28 @@
import 'package:krow_domain/krow_domain.dart' show StaffSession;
/// Singleton store for the authenticated staff member's session context.
///
/// Holds a [StaffSession] (V2 domain entity) populated after sign-in via the
/// V2 session API. Features read from this store to access staff/tenant context
/// without re-fetching from the backend.
class StaffSessionStore {
StaffSessionStore._();
/// The global singleton instance.
static final StaffSessionStore instance = StaffSessionStore._();
StaffSession? _session;
/// The current staff session, or `null` if not authenticated.
StaffSession? get session => _session;
/// Replaces the current session with [session].
void setSession(StaffSession session) {
_session = session;
}
/// Clears the stored session (e.g. on sign-out).
void clear() {
_session = null;
}
}

View File

@@ -0,0 +1,101 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:flutter/foundation.dart';
import 'package:krow_domain/krow_domain.dart';
import '../api_service/api_service.dart';
import '../api_service/core_api_services/v2_api_endpoints.dart';
import '../api_service/mixins/session_handler_mixin.dart';
/// A singleton service that manages user session state via the V2 REST API.
///
/// Replaces `DataConnectService` for auth-state listening, role validation,
/// and session-state broadcasting. Uses [SessionHandlerMixin] for token
/// refresh and retry logic.
class V2SessionService with SessionHandlerMixin {
V2SessionService._();
/// The global singleton instance.
static final V2SessionService instance = V2SessionService._();
/// Optional [BaseApiService] reference set during DI initialisation.
///
/// When `null` the service falls back to a raw Dio call so that
/// `initializeAuthListener` can work before the Modular injector is ready.
BaseApiService? _apiService;
/// Injects the [BaseApiService] dependency.
///
/// Call once from `CoreModule.exportedBinds` after registering [ApiService].
void setApiService(BaseApiService apiService) {
_apiService = apiService;
}
@override
firebase_auth.FirebaseAuth get auth => firebase_auth.FirebaseAuth.instance;
/// Fetches the user role by calling `GET /auth/session`.
///
/// Returns the role string (e.g. `STAFF`, `BUSINESS`, `BOTH`) or `null` if
/// the call fails or the user has no role.
@override
Future<String?> fetchUserRole(String userId) async {
try {
// Wait for ApiService to be injected (happens after CoreModule.exportedBinds).
// On cold start, initializeAuthListener fires before DI is ready.
if (_apiService == null) {
debugPrint(
'[V2SessionService] ApiService not yet injected; '
'waiting for DI initialization...',
);
for (int i = 0; i < 10; i++) {
await Future<void>.delayed(const Duration(milliseconds: 200));
if (_apiService != null) break;
}
}
final BaseApiService? api = _apiService;
if (api == null) {
debugPrint(
'[V2SessionService] ApiService still null after waiting 2 s; '
'cannot fetch user role.',
);
return null;
}
final ApiResponse response = await api.get(V2ApiEndpoints.session);
if (response.data is Map<String, dynamic>) {
final Map<String, dynamic> data =
response.data as Map<String, dynamic>;
final String? role = data['role'] as String?;
return role;
}
return null;
} catch (e) {
debugPrint('[V2SessionService] Error fetching user role: $e');
return null;
}
}
/// Signs out the current user from Firebase Auth and clears local state.
Future<void> signOut() async {
try {
// Revoke server-side session token.
final BaseApiService? api = _apiService;
if (api != null) {
try {
await api.post(V2ApiEndpoints.signOut);
} catch (e) {
debugPrint('[V2SessionService] Server sign-out failed: $e');
}
}
await auth.signOut();
} catch (e) {
debugPrint('[V2SessionService] Error signing out: $e');
rethrow;
} finally {
handleSignOut();
}
}
}