initNotifications method

Future<void> initNotifications()

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;
  }
}