initializeNotifications method

Future<NotificationInitResult> initializeNotifications({
  1. bool bypassCaptureBackoff = false,
  2. bool isReplay = false,
})

Initialize notifications manually.

Use this when AppConfigBase.fcmAutoInitialize is false and you want to trigger notification setup at a specific point in your app flow.

This method:

  1. Checks if FCM is enabled in configuration
  2. Requests notification permissions from the user
  3. Initializes FCM token management if permission is granted

Returns a NotificationInitResult indicating the outcome:

Example:

// After onboarding or at the right moment
final result = await notificationService.initializeNotifications();
if (result == NotificationInitResult.success ||
    result == NotificationInitResult.successNoTokenSync) {
  showSnackbar('Notifications enabled!');
}

bypassCaptureBackoff (A.6/FCM-039): when true, skips the persisted FCM capture backoff so an explicit user/settings trigger always re-attempts. Passed through to initializeFcmToken. Defaults to false; the auto-init / login path never bypasses (it relies on the logout backoff reset).

isReplay is INTERNAL: it is set only by the B.3 deferred-capture replay (a login event that fired before initialize completed) so initializeFcmToken selects the distinct _FcmDeferredCaptureFailure Crashlytics carrier on a persist failure (F10). Consumers should leave it false.

Implementation

Future<NotificationInitResult> initializeNotifications({
  bool bypassCaptureBackoff = false,
  bool isReplay = false,
}) async {
  if (!AppConfigBase.useFCM) {
    logd('initializeNotifications: FCM is disabled in AppConfigBase');
    return NotificationInitResult.fcmDisabledConfig;
  }

  if (_hasFcmTokenInitialized) {
    logd('initializeNotifications: Already initialized');
    return NotificationInitResult.alreadyInitialized;
  }

  try {
    // Get status before request to detect blocked requests
    final statusBefore = await getPermissionStatus();

    // Request permission
    final permissionResult = await requestPermissions();

    // Check for blocked request (status unchanged from notDetermined)
    if (statusBefore == NotificationPermissionStatus.notDetermined &&
        permissionResult == NotificationPermissionStatus.notDetermined) {
      logw('Permission request may have been blocked by system');
      return NotificationInitResult.permissionRequestBlocked;
    }

    if (permissionResult == NotificationPermissionStatus.authorized ||
        permissionResult == NotificationPermissionStatus.provisional) {
      // Permission granted - initialize FCM token if callback is set.
      if (_onTokenChanged == null) {
        // BUG 6 (BEH-10): permission granted but no token-changed callback is
        // wired, so the FCM token cannot be captured or persisted. Do NOT
        // report unqualified success — return the partial-success flavor and
        // warn loudly so this integration gap is not masked.
        logw('initializeNotifications: permission granted but no '
            'onTokenChanged callback is configured — the FCM token will NOT '
            'be captured or synced. Wire a token-changed callback (e.g. via '
            'DreamicServices.initialize) to enable token sync.');
        return NotificationInitResult.successNoTokenSync;
      }
      // A.4 (BEH-23/FCM-032/FCM-058): consume the capture signal so a
      // permission-granted-but-capture-failed outcome does not report
      // unqualified success. Reuses the successNoTokenSync granted-mapping —
      // enableNotifications keeps the enabled flag on it (FCM-014).
      final fcmResult = await initializeFcmToken(
        onTokenChanged: _onTokenChanged!,
        bypassCaptureBackoff: bypassCaptureBackoff,
        isReplay: isReplay,
      );
      switch (fcmResult) {
        case FcmTokenInitResult.success:
          logi('Notifications initialized successfully');
          return NotificationInitResult.success;
        case FcmTokenInitResult.skippedQuietly:
          // Enabled-but-unconfigured web (BEH-23 exception / BEH-26): quiet,
          // non-alarming — permission was granted, no token synced.
          logd('initializeNotifications: web FCM token capture skipped '
              '(FCM_WEB_VAPID_KEY not configured) — permission granted, no '
              'token synced');
          return NotificationInitResult.successNoTokenSync;
        case FcmTokenInitResult.captureFailed:
          logw('initializeNotifications: permission granted but FCM token '
              'capture/persist failed — reporting partial success; the token '
              'sync will be retried on the next trigger');
          return NotificationInitResult.successNoTokenSync;
      }
    } else if (permissionResult == NotificationPermissionStatus.denied) {
      // Check if this is a permanent denial
      final canPrompt = await _permissionHelper.canPromptForPermission();
      if (!canPrompt) {
        return NotificationInitResult.permissionPermanentlyDenied;
      }
      return NotificationInitResult.permissionDenied;
    } else {
      return NotificationInitResult.permissionPermanentlyDenied;
    }
  } catch (e, stackTrace) {
    loge(e, 'initializeNotifications failed', stackTrace);
    _onError?.call(e, stackTrace);
    return NotificationInitResult.error;
  }
}