initNotifications method
Initialises the notification system. Safe to call multiple times — subsequent calls after the first are no-ops until dispose is called.
Concurrent calls (e.g. rapid login taps or duplicate AuthenticationBloc
authenticated events) are serialised via _initCompleter — only the
first call runs the init path; all others await the same result.
Called by AuthenticationBloc when the user becomes authenticated.
Implementation
Future<void> initNotifications() async {
if (_initialized) {
_logger.debug('[Notifications] Already initialized, skipping.');
return;
}
// Concurrent call guard: if init is already in flight, await it.
// If the first caller fails, the error propagates to all concurrent waiters.
if (_initCompleter != null) {
return _initCompleter!.future;
}
_initCompleter = Completer<void>();
try {
// Request full notification authorization directly — shows the system dialog on first login.
// Provisional mode (quiet delivery, no banners) is intentionally skipped: banners and sounds
// are essential for a driver app and provisional permission silently suppresses both.
final NotificationSettings settings = await _firebaseMessaging.requestPermission(alert: true, badge: true, sound: true);
// Guard: dispose() may have been called while we were awaiting the
// permission prompt (e.g. user logged out during the prompt). Abort
// to prevent setting up listeners on a logged-out session.
if (_initCompleter == null) return;
_logger.info('[Notifications] Permission result on login: ${settings.authorizationStatus.name}');
if (settings.authorizationStatus == AuthorizationStatus.denied) {
_logger.warn('[Notifications] Permission denied — skipping setup.', escalate: false);
Sentry.metrics.count('fcm.permission.denied', 1);
// Do NOT set _initialized — allow re-attempt on next login.
_initCompleter!.complete();
_initCompleter = null;
return;
}
if (settings.authorizationStatus == AuthorizationStatus.notDetermined) {
_logger.warn('[Notifications] Permission not determined — skipping setup.', escalate: false);
Sentry.metrics.count('fcm.permission.other', 1);
// Do NOT set _initialized — allow re-attempt on next login.
_initCompleter!.complete();
_initCompleter = null;
return;
}
Sentry.metrics.count('fcm.permission.granted', 1);
await _initLocalNotifications();
// Guard: dispose() may have been called during _initLocalNotifications().
if (_initCompleter == null) return;
await _initPushNotifications();
// Guard: dispose() may have been called during _initPushNotifications()
// (e.g. user logged out during the getInitialMessage() await). Without
// this guard, _initCompleter is null and the !.complete() call below
// throws a null-check error, which the catch block logs as a false failure.
if (_initCompleter == null) return;
// Happy path only: mark as fully initialized after everything succeeded.
_initialized = true;
_initCompleter!.complete();
_initCompleter = null;
} catch (e, st) {
// Do NOT set _initialized — allow re-attempt on next login.
_logger.error('[Notifications] Initialization failed', error: e, stackTrace: st);
_initCompleter?.completeError(e, st);
_initCompleter = null;
}
}