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:
@@ -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';
|
||||
|
||||
@@ -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>()),
|
||||
|
||||
@@ -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>{
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user