Flutter Push Handling

Push Notifications

Complete the common Flutter setup and the platform-specific Android or iOS guide first.

Initialize Reteno before calling instance push methods. Subscribing to the push streams does not require initialization, so register those listeners as early as possible: they are broadcast streams and do not replay earlier events.

Request Notification Permission

Use the cross-platform API to request notification permission on iOS and Android 13 or later. The result is also synchronized with Reteno:

final granted = await Reteno().requestPushPermission();

On Android 13 or later, call this only after a Flutter activity is attached; earlier Android versions show no prompt and return the current notification-enabled state.

On iOS 12 or later, provisional authorization is also supported:

final granted = await Reteno().requestPushPermission(provisional: true);

If another package or native code owns the Android permission prompt, synchronize the resulting state with Reteno:

await Reteno().updatePushPermissionStatus();

This method is Android-only and a no-op on iOS. Its Future<bool> resolves as soon as the native call is dispatched.

Get the Initial Notification

Use getInitialNotification() once during startup when the application was launched by tapping a push notification:

final payload = await Reteno().getInitialNotification();

if (payload != null) {
  // Process the notification payload.
}

The result is null when no launch payload was stored. Reading it clears the payload, so a later call also returns null.

On iOS the stored payload is not filtered by the Reteno es_interaction_id. Validate it before treating it as Reteno data when the application receives pushes from several providers.

Listen for Push Events

Every stream below is a broadcast stream. Store and cancel subscriptions with the owning object.

StreamTypeEvent
Reteno.onRetenoNotificationReceivedStream<Map<String, dynamic>>A notification arrived while the app was in the foreground
Reteno.onRetenoNotificationClickedStream<Map<String, dynamic>>The user tapped a notification
Reteno.onUserNotificationActionStream<RetenoUserNotificationAction>The user tapped an action button
Reteno.onRetenoNotificationDeletedStream<Map<String, dynamic>>Android only: the user dismissed a notification
Reteno.onRetenoCustomNotificationReceivedStream<Map<String, dynamic>>Android only: a custom push payload was received
Reteno.onRetenoInAppCustomDataReceivedStream<RetenoInAppCustomData>Android only: custom data arrived from a push-triggered in-app message
Reteno.onRetenoNotificationReceived.listen((payload) {
  // Process the notification payload.
});

Reteno.onRetenoNotificationClicked.listen((payload) {
  // Process the notification payload.
});

Use getInitialNotification() for a terminated-state launch instead of relying on the clicked stream alone.

onCorePushReceived, onCorePushClicked, and onCorePushAction are cross-platform aliases of the first three streams. Subscribe to either name, not both. See Action Buttons for the action model and In-App Messages for RetenoInAppCustomData.

On iOS these callbacks are independent of the token mode: one native coordinator processes them in all three modes while interoperating with FlutterFire. See Notification delegate coordination.

Push Token Handling

Android always handles the FCM token automatically. On iOS, choose the mode that matches the mobile app in Reteno:

ModeToken owner
automaticThe bridge reports the APNs token
manualThe plugin reports the current and refreshed FCM token without replacing Firebase's delegate
externalThe application reports the FCM token with setPushToken()

The bridge routes only the source that matches the selected mode, so an early APNs callback can never be forwarded as an FCM token. See the iOS routing guarantees.

In external mode, send the FCM registration token after initialization and APNs registration, after identifying or changing the user, and whenever Firebase refreshes it:

await Reteno().setPushToken(fcmToken);

Do not pass an APNs token to this method: it is trimmed and rejected only when empty, and the type cannot be inferred. Calling it in automatic returns a native mode error. The returned Future completes when native processing is dispatched, not when the backend has associated the token with the contact.

See Select the iOS push token mode for the complete configuration and refresh listener.

Android Notification Grouping

Configure grouping after initialization. Assign every Reteno push notification to a constant group:

await Reteno().setNotificationGroupingRule(
  const RetenoNotificationGroupingRule.constant('orders'),
);

Or derive the group ID from a push payload field:

await Reteno().setNotificationGroupingRule(
  const RetenoNotificationGroupingRule.byPayloadKey(
    'orderNumber',
    fallbackGroupId: 'orders',
  ),
);

If the key is missing or empty, the fallback is used; without a fallback the notification is not grouped.

Pass null to disable grouping:

await Reteno().setNotificationGroupingRule(null);

Payload keys and group IDs are trimmed, and a rule with no non-empty key or fallback throws ArgumentError.

The native plugin persists the rule, so it also applies when a push arrives without a running Flutter engine. The rule sets the notification group key but does not create a group summary notification; the application is responsible for summary behavior.

This API has no effect on iOS.

Integration Diagnostics

Run diagnostics after initialization:

final issues = await Reteno().diagnose();

The method can return these codes:

CodePlatformMeaning
SDK_NOT_INITIALIZEDAndroid, iOSReteno has not been initialized
NOTIFICATIONS_DISABLEDAndroidNotifications are disabled in system settings
PUSH_PERMISSION_DENIEDAndroid, iOSNotification permission was denied; Android API 33+ only
PUSH_PERMISSION_NOT_DETERMINEDiOSThe permission prompt has not been resolved
REMOTE_NOTIFICATIONS_NOT_REGISTEREDiOSPermission was granted, but APNs registration is incomplete
FCM_MESSAGING_SERVICE_MISSINGAndroidNo active MESSAGING_EVENT handler
RETENO_MESSAGING_SERVICE_MISSINGAndroidNo Reteno handler among the messaging services
FCM_MESSAGING_SERVICE_CONFLICTAndroidMultiple application-level MESSAGING_EVENT services are declared
FCM_TOKEN_MISSINGAndroidFirebase returned no current FCM token
FCM_TOKEN_FETCH_FAILEDAndroidFetching the FCM token failed

An empty list means that the plugin's local checks passed. It does not verify the access key, bundle or application ID, provider credentials, the iOS App Group and service extension, token-mode selection, or actual delivery. On Android, SDK_NOT_INITIALIZED reflects bridge state only; native initialization continues asynchronously.

APNs registration is asynchronous, so a diagnostic call made right after permission is granted can temporarily return REMOTE_NOTIFICATIONS_NOT_REGISTERED.

End-to-End Troubleshooting

If Reteno says that a contact has no app token, or a test push does not arrive:

  1. Initialize Reteno on application startup before calling other Reteno APIs.
  2. Request push permission and resolve every persistent diagnostics code.
  3. Identify the target contact with Reteno().setUserAttributes(userExternalId: ...).
  4. Verify that the Reteno mobile app, access key, bundle/application ID, and provider belong to the same environment.
  5. Send a test notification from Reteno to a physical device.

On Android, also verify that:

  • the device is API 26 or later and has working Google Play services;
  • google-services.json belongs to the current applicationId;
  • the merged manifest has exactly one effective MESSAGING_EVENT service;
  • a custom service follows the advanced Android setup.

On iOS, also verify that:

  • Firebase is initialized before Reteno in manual and external;
  • the App Group and Notification Service Extension are signed and embedded correctly;
  • APNs credentials are stored in Reteno, or the APNs key is stored in Firebase for FCM;
  • for direct APNs, the build signing and the Reteno provider use the same sandbox or production environment;
  • the selected token mode matches the provider configured in Reteno;
  • in external, setPushToken() runs after APNs registration, after identification, and on every refresh;
  • the app keeps the Flutter-first delegate registration order described in the iOS guide.