connectToAuthService method
Connects to an auth service to automatically manage FCM tokens on login/logout.
Note: This is the legacy initialization approach. For new code, prefer
DreamicServices.initialize or passing authService directly to
initialize for race-free setup.
This method may miss auth events on warm start (when user is already
logged in) because callbacks are registered after Firebase listeners
have already attached. See docs/plans/auth-race/plan.auth-race.md.
Login Behavior
When the user logs in:
- If AppConfigBase.fcmAutoInitialize is true: requests permission and initializes FCM
- If AppConfigBase.fcmAutoInitialize is false: only initializes if permission is already granted
Logout Behavior
When the user logs out:
- Backend unregistration happens automatically via
onAboutToLogOutcallback (called before Firebase signOut while still authenticated) - Then performs local token cleanup (stops listener, deletes Firebase token, clears cache)
- Manual preLogoutCleanup call is no longer needed for backend cleanup
DeviceService Integration
When DeviceServiceInt is registered in GetIt, token changes are automatically
delegated to DeviceServiceInt.updateFcmToken. This ensures:
- Single canonical device document per install/profile
- Token stored alongside timezone and activity data
- Token uniqueness enforcement across device docs
Recommended setup (race-free):
// Use DreamicServices for race-free initialization
final services = await DreamicServices.initialize(
firebaseApp: Firebase.app(),
enableDeviceService: true,
enableNotifications: true,
);
Legacy setup (may miss auth events on warm start):
// 1. Register services in GetIt
GetIt.I.registerSingleton<DeviceServiceInt>(DeviceServiceImpl());
// 2. Connect DeviceService first (handles device doc lifecycle)
await deviceService.connectToAuthService();
// 3. Connect NotificationService (auto-delegates to DeviceService)
await notificationService.connectToAuthService();
If DeviceService is not registered, falls back to direct Firebase callable.
Parameters
authService is the auth service to connect to. If null, attempts to
resolve from GetIt (guarded - logs and skips if not registered).
onTokenChanged callback for syncing tokens to your backend.
If not provided and DeviceService is registered, uses DeviceService.
Otherwise, uses the Firebase callable configured in
AppConfigBase.notificationsUpdateFcmTokenFunction.
Example
// Standard setup (uses DeviceService if available)
await notificationService.connectToAuthService();
// Custom token handling
await notificationService.connectToAuthService(
onTokenChanged: (newToken, oldToken) async {
await myBackendService.updateFcmToken(newToken, oldToken);
},
);
Implementation
Future<void> connectToAuthService({
AuthServiceInt? authService,
Future<void> Function(String? newToken, String? oldToken)? onTokenChanged,
}) async {
// Cancel any existing subscription
await _authSubscription?.cancel();
// Store the token callback
_onTokenChanged = onTokenChanged ?? _defaultTokenChangedCallback;
// Try to get auth service
AuthServiceInt? auth = authService;
if (auth == null) {
try {
if (GetIt.I.isRegistered<AuthServiceInt>()) {
auth = GetIt.I.get<AuthServiceInt>();
logd('Resolved AuthServiceInt from GetIt');
} else {
logd('AuthServiceInt not registered in GetIt, skipping auth connection');
return;
}
} catch (e) {
logd('Could not resolve AuthServiceInt from GetIt: $e');
return;
}
}
// Remove any previously registered callback
if (_aboutToLogOutCallback != null && _authService != null) {
_authService!.removeOnAboutToLogOutCallback(_aboutToLogOutCallback!);
}
// Store reference to auth service for cleanup
_authService = auth;
_isConnectedToAuthService = true;
// Register onAboutToLogOut callback
// This is called BEFORE Firebase signOut while still authenticated
_aboutToLogOutCallback = () async {
// When DeviceService is registered, it handles backend cleanup by deleting
// the device doc via its own onAboutToLogOut callback. NotificationService
// should not trigger a token persistence write that would race with deletion.
if (GetIt.I.isRegistered<DeviceServiceInt>()) {
logd('onAboutToLogOut: DeviceService registered, skipping token persistence '
'(device doc will be deleted by DeviceService)');
return;
}
// Fallback for apps without DeviceService integration - perform backend
// token unregistration via the custom callback
logd('onAboutToLogOut: Performing backend token unregistration');
if (_onTokenChanged != null && _cachedFcmToken != null) {
try {
await _onTokenChanged!(null, _cachedFcmToken);
logd('Successfully unregistered FCM token on backend before logout');
} catch (e) {
logw('Failed to unregister FCM token on backend: $e');
// Continue with logout even if backend call fails
}
}
};
auth.addOnAboutToLogOutCallback(_aboutToLogOutCallback!);
logd('Added onAboutToLogOut callback for backend token cleanup');
// Subscribe to auth changes
_authSubscription = auth.isLoggedInStream.listen((isLoggedIn) async {
if (isLoggedIn) {
await _handleLogin();
} else {
// Local cleanup only - backend unregistration already happened in
// onAboutToLogOut callback (before Firebase signOut).
logd('Auth logout detected, performing local token cleanup');
try {
// Stop token refresh listener
await _tokenRefreshSubscription?.cancel();
_tokenRefreshSubscription = null;
// Delete FCM token from Firebase
try {
await FirebaseMessaging.instance.deleteToken();
logd('Deleted FCM token from Firebase');
} catch (e) {
logw('Failed to delete FCM token from Firebase: $e');
}
// Clear cached tokens
await clearFcmToken();
logd('Local token cleanup completed');
} catch (e) {
// Swallow any unexpected errors - cleanup is best-effort
logw('Unexpected error during auto logout cleanup: $e');
}
}
});
logi('Connected to auth service for FCM token management');
}