refactor(session): migrate V2 session listener initialization to improve dependency injection and role handling

This commit is contained in:
Achintha Isuru
2026-03-17 10:55:18 -04:00
parent b289ed3b02
commit ba5bf8e1d7
6 changed files with 37 additions and 44 deletions

View File

@@ -30,16 +30,6 @@ void main() async {
logStateChanges: false, // Set to true for verbose debugging logStateChanges: false, // Set to true for verbose debugging
); );
// Initialize V2 session listener for Firebase Auth state changes.
// Role validation calls GET /auth/session via the V2 API.
V2SessionService.instance.initializeAuthListener(
allowedRoles: <String>[
'CLIENT',
'BUSINESS',
'BOTH',
], // Only allow users with CLIENT, BUSINESS, or BOTH roles
);
runApp( runApp(
ModularApp( ModularApp(
module: AppModule(), module: AppModule(),

View File

@@ -27,11 +27,20 @@ class _SessionListenerState extends State<SessionListener> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_setupSessionListener(); _initializeSession();
} }
void _setupSessionListener() { void _initializeSession() {
_sessionSubscription = V2SessionService.instance.onSessionStateChanged // Resolve V2SessionService via DI — this triggers CoreModule's lazy
// singleton, which wires setApiService(). Must happen before
// initializeAuthListener so the session endpoint is reachable.
final V2SessionService sessionService = Modular.get<V2SessionService>();
sessionService.initializeAuthListener(
allowedRoles: const <String>['CLIENT', 'BUSINESS', 'BOTH'],
);
_sessionSubscription = sessionService.onSessionStateChanged
.listen((SessionState state) { .listen((SessionState state) {
_handleSessionChange(state); _handleSessionChange(state);
}); });

View File

@@ -28,15 +28,6 @@ void main() async {
logStateChanges: false, // Set to true for verbose debugging logStateChanges: false, // Set to true for verbose debugging
); );
// Initialize V2 session listener for Firebase Auth state changes.
// Role validation calls GET /auth/session via the V2 API.
V2SessionService.instance.initializeAuthListener(
allowedRoles: <String>[
'STAFF',
'BOTH',
], // Only allow users with STAFF or BOTH roles
);
runApp( runApp(
ModularApp( ModularApp(
module: AppModule(), module: AppModule(),

View File

@@ -27,11 +27,20 @@ class _SessionListenerState extends State<SessionListener> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_setupSessionListener(); _initializeSession();
} }
void _setupSessionListener() { void _initializeSession() {
_sessionSubscription = V2SessionService.instance.onSessionStateChanged // Resolve V2SessionService via DI — this triggers CoreModule's lazy
// singleton, which wires setApiService(). Must happen before
// initializeAuthListener so the session endpoint is reachable.
final V2SessionService sessionService = Modular.get<V2SessionService>();
sessionService.initializeAuthListener(
allowedRoles: const <String>['STAFF', 'BOTH'],
);
_sessionSubscription = sessionService.onSessionStateChanged
.listen((SessionState state) { .listen((SessionState state) {
_handleSessionChange(state); _handleSessionChange(state);
}); });

View File

@@ -18,9 +18,8 @@ class CoreModule extends Module {
// 2. Register the base API service // 2. Register the base API service
i.addLazySingleton<BaseApiService>(() => ApiService(i.get<Dio>())); i.addLazySingleton<BaseApiService>(() => ApiService(i.get<Dio>()));
// 2b. Wire the V2 session service with the API service. // 2b. Register V2SessionService — wires the singleton with ApiService.
// This uses a post-registration callback so the singleton gets // Resolved eagerly by SessionListener.initState() after Modular is ready.
// its dependency as soon as the injector resolves BaseApiService.
i.addLazySingleton<V2SessionService>(() { i.addLazySingleton<V2SessionService>(() {
final V2SessionService service = V2SessionService.instance; final V2SessionService service = V2SessionService.instance;
service.setApiService(i.get<BaseApiService>()); service.setApiService(i.get<BaseApiService>());

View File

@@ -42,23 +42,10 @@ class V2SessionService with SessionHandlerMixin {
@override @override
Future<String?> fetchUserRole(String userId) async { Future<String?> fetchUserRole(String userId) async {
try { 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; final BaseApiService? api = _apiService;
if (api == null) { if (api == null) {
debugPrint( debugPrint(
'[V2SessionService] ApiService still null after waiting 2 s; ' '[V2SessionService] ApiService not injected; '
'cannot fetch user role.', 'cannot fetch user role.',
); );
return null; return null;
@@ -74,8 +61,16 @@ class V2SessionService with SessionHandlerMixin {
// Per V2 auth doc, GET /auth/session is used for app startup hydration. // Per V2 auth doc, GET /auth/session is used for app startup hydration.
_hydrateSessionStores(data); _hydrateSessionStores(data);
final String? role = data['role'] as String?; // Derive role from the presence of staff/business context.
return role; // The session endpoint returns { user, tenant, business, vendor, staff }
// — there is no explicit "role" field.
final bool hasStaff = data['staff'] is Map<String, dynamic>;
final bool hasBusiness = data['business'] is Map<String, dynamic>;
if (hasStaff && hasBusiness) return 'BOTH';
if (hasStaff) return 'STAFF';
if (hasBusiness) return 'BUSINESS';
return null;
} }
return null; return null;
} catch (e) { } catch (e) {