initialize method

Future<void> initialize({
  1. NotificationActionCallback? onNotificationTapped,
  2. NotificationButtonActionCallback? onNotificationAction,
  3. ForegroundMessageCallback? onForegroundMessage,
  4. NotificationErrorCallback? onError,
  5. bool showNotificationsInForeground = true,
  6. int reminderIntervalDays = 30,
  7. Future<void> onTokenChanged(
    1. String? newToken,
    2. String? oldToken
    )?,
  8. NotificationSettingsDeepLinkCallback? onSystemNotificationSettingsOpened,
  9. AuthServiceInt? authService,
  10. @Deprecated('Use authService parameter instead for race-free initialization') bool autoConnectAuth = true,
})

Initializes the notification service.

This method sets up all notification handling including:

  • Local notification plugin initialization
  • FCM message listeners (foreground, background, terminated)
  • Notification action handlers
  • Platform-specific configuration
  • Auto-wiring to auth service (if registered in GetIt)

This is the only method consuming apps need to call to set up notifications.

Parameters:

  • onNotificationTapped: Callback for when user taps a notification
  • onNotificationAction: Callback for when user taps an action button
  • onForegroundMessage: Callback for when notification arrives in foreground
  • onError: Callback for handling errors
  • showNotificationsInForeground: Whether to display notifications in foreground (default: true)
  • reminderIntervalDays: Days between permission reminders (default: 30)
  • onTokenChanged: Callback for FCM token changes. If not provided and auth service is available, uses the default Firebase callable function implementation.
  • authService: Pass auth service directly for race-free initialization. When provided, sets up auth connection immediately without using GetIt. This is the recommended approach for new code - see plan.auth-race.md.
  • autoConnectAuth: Whether to automatically connect to auth service if available in GetIt (default: true). Set to false if you want to call connectToAuthService manually with custom configuration. Deprecated: Use authService parameter instead.

For race-free initialization, pass authService directly and set autoConnectAuth: false:

await notificationService.initialize(
  authService: auth,
  autoConnectAuth: false,
  onNotificationTapped: (route, data) async { ... },
);

Legacy Initialization

await NotificationService().initialize(
  onNotificationTapped: (route, data) async {
    if (route != null) {
      Navigator.of(context).pushNamed(route, arguments: data);
    }
  },
  showNotificationsInForeground: true,
);

Implementation

Future<void> initialize({
  NotificationActionCallback? onNotificationTapped,
  NotificationButtonActionCallback? onNotificationAction,
  ForegroundMessageCallback? onForegroundMessage,
  NotificationErrorCallback? onError,
  bool showNotificationsInForeground = true,
  int reminderIntervalDays = 30,
  Future<void> Function(String? newToken, String? oldToken)? onTokenChanged,
  NotificationSettingsDeepLinkCallback? onSystemNotificationSettingsOpened,
  // NEW: Pass auth service directly for race-free initialization
  AuthServiceInt? authService,
  // DEPRECATED: Use authService parameter instead
  @Deprecated('Use authService parameter instead for race-free initialization')
  bool autoConnectAuth = true,
}) async {
  // CRITICAL: Set auth reference FIRST, before any await statements.
  //
  // The auth callback (handleAuthenticated) may fire immediately after
  // AuthService construction via a microtask. If we await anything before
  // setting _authService, the callback could execute while _authService
  // is still null.
  //
  // See: "Critical implementation constraints" section in plan.auth-race.md
  if (authService != null) {
    _authService = authService;
    _isConnectedToAuthService = true;
    logd('NotificationService: Initialized with auth service (race-free path)');
  }

  if (_initialized) {
    logi('NotificationService already initialized');
    return;
  }

  try {
    _onNotificationTapped = onNotificationTapped;
    _onNotificationAction = onNotificationAction;
    _onForegroundMessage = onForegroundMessage;
    _onError = onError;
    _showNotificationsInForeground = showNotificationsInForeground;
    _reminderIntervalDays = reminderIntervalDays;
    _onSystemNotificationSettingsOpened = onSystemNotificationSettingsOpened;

    // Initialize local notifications plugin
    await _initializeLocalNotifications();

    // Set up FCM message handlers
    await _setupFCMHandlers();

    // Check for initial message (app opened from terminated state via notification)
    await _checkInitialMessage();

    _initialized = true;
    logi('NotificationService initialized successfully');

    // Store token callback if provided (used by both paths)
    if (onTokenChanged != null) {
      _onTokenChanged = onTokenChanged;
    }

    // Set up notification settings deep link channel
    if (onSystemNotificationSettingsOpened != null) {
      _settingsChannel =
          const MethodChannel(notificationSettingsChannelName);
      _settingsChannel!.setMethodCallHandler(_handleSettingsMethodCall);
      logi('Notification settings deep link channel initialized');

      // Check for pending cold-launch intent.
      // On cold launch, native code stores the settings intent/delegate call
      // because the Dart handler isn't registered yet when the engine starts.
      // This pull-based check retrieves any pending event.
      try {
        final pending = await _settingsChannel!
            .invokeMethod<Map<dynamic, dynamic>?>('getPendingSettingsIntent');
        if (pending != null) {
          final channelId = pending['channelId'] as String?;
          logi('Found pending notification settings deep link from cold launch');
          // Use addPostFrameCallback to ensure the widget tree (including the
          // Navigator/Router) is built before the consuming app's callback
          // navigates.
          //
          // TIMING NOTE: addPostFrameCallback guarantees the widget tree is
          // mounted, but NOT that the consuming app's async initialization is
          // complete (auth state, router configuration, initial data fetch,
          // etc.). This is the same constraint as onNotificationTapped
          // cold-launch handling. If the consuming app's callback needs to
          // navigate after its own async setup, it should queue the navigation
          // (e.g., store the info and process it when the home screen mounts).
          WidgetsBinding.instance.addPostFrameCallback((_) {
            _handleSettingsDeepLink(channelId);
          });
        }
      } on MissingPluginException {
        // Native side hasn't set up the handler — feature not configured.
        // This is expected when the consuming app hasn't added the native setup.
        logd('No native handler for getPendingSettingsIntent — cold launch '
            'deep links will not be detected');
      }
    }

    // Skip auto-connect if authService was provided directly (race-free path)
    if (authService != null) {
      logd('NotificationService: Auth service provided directly, skipping auto-connect');
      // Token callback already stored above, auth service already set at top
    } else if (autoConnectAuth) {
      // Legacy path: Auto-wire to auth service if available in GetIt
      // Check if auth service is available before attempting to connect
      bool authAvailable = false;
      try {
        authAvailable = GetIt.I.isRegistered<AuthServiceInt>();
      } catch (e) {
        // GetIt not initialized or other error
        logd('GetIt check failed: $e');
      }

      if (authAvailable) {
        await connectToAuthService(onTokenChanged: onTokenChanged);
      } else {
        // This is a configuration error - report it but don't crash
        const errorMsg = 'autoConnectAuth is enabled but AuthServiceInt is not '
            'registered in GetIt. FCM token management will not work automatically. '
            'Either register AuthServiceInt before initializing NotificationService, '
            'or set autoConnectAuth: false and call connectToAuthService() manually later.';
        loge(errorMsg);
        _onError?.call(errorMsg, null);
      }
    }
    // Note: If neither authService nor autoConnectAuth, token callback is still
    // stored above for manual connection later via connectToAuthService()
  } catch (e, stackTrace) {
    loge(e, 'Failed to initialize NotificationService', stackTrace);
    _onError?.call(e, stackTrace);
    rethrow;
  }
}