Expo Push Handling

Initialize the SDK

When using JavaScript-controlled initialization, call initialize() before registering listeners or requesting push notifications:

import Reteno from 'expo-reteno-sdk';

await Reteno.initialize('YOUR_SDK_ACCESS_KEY');

Skip this call if sdkAccessToken is set in the platform plugin config, because that enables automatic initialization.

Register for Push Notifications

Call registerForRemoteNotifications() once at app startup:

import { useEffect } from 'react';
import Reteno from 'expo-reteno-sdk';

useEffect(() => {
  Reteno.registerForRemoteNotifications();
}, []);

Get Initial Notification

When your app is opened by clicking a push notification, you can read its payload with getInitialNotification.

import { useEffect } from 'react';
import { Alert } from 'react-native';
import Reteno from 'expo-reteno-sdk';

useEffect(() => {
  Reteno.getInitialNotification().then((data) => {
    Alert.alert('getInitialNotification', data ? JSON.stringify(data) : 'null');
  });
}, []);

Set device token manually

Use setDeviceToken() when managing the iOS FCM/APNs token manually:

const result = await Reteno.setDeviceToken(token);

The method returns Promise<boolean>. On Android it resolves successfully without changing the token because token handling is performed by the native Firebase messaging service.

Listen for New Push Notifications while App Is Active

To listen to pushes in foreground, use setOnRetenoPushReceivedListener:

import { useEffect } from 'react';
import { Alert } from 'react-native';
import Reteno from 'expo-reteno-sdk';

useEffect(() => {
  const pushListener = Reteno.setOnRetenoPushReceivedListener((event) => {
    Alert.alert('onRetenoPushReceived', event ? JSON.stringify(event) : 'null');
  });

  return () => pushListener.remove();
}, []);

Listen for Push Notification Clicks

To handle notification clicks, use setOnRetenoPushClickedListener:

import { useEffect } from 'react';
import { Alert } from 'react-native';
import Reteno from 'expo-reteno-sdk';

useEffect(() => {
  const pushClickListener = Reteno.setOnRetenoPushClickedListener((event) => {
    Alert.alert('onRetenoPushClicked', event ? JSON.stringify(event) : 'null');
  });

  return () => pushClickListener.remove();
}, []);

iOS Action Buttons

For iOS push action buttons, use setOnRetenoPushButtonClickedListener:

import { useEffect } from 'react';
import { Platform } from 'react-native';
import Reteno from 'expo-reteno-sdk';

useEffect(() => {
  if (Platform.OS !== 'ios') return;

  const listener = Reteno.setOnRetenoPushButtonClickedListener((event) => {
    console.log('onRetenoPushButtonClicked', event);
  });

  return () => listener.remove();
}, []);

Group Notifications (Android Only)

Requires Reteno Android SDK 2.10.0 or newer (bundled since expo-reteno-sdk v2.2.0).

Notifications can be grouped by a value in the push payload or by a constant group ID. The rule is persisted natively and restored before JavaScript starts, so it also applies to notifications received while the app is not running.

import Reteno from 'expo-reteno-sdk';

// Group by a payload value, e.g. all pushes for the same chat
await Reteno.setNotificationGroupingRule({ payloadKey: 'chatId' });

// Group under a constant ID, regardless of payload
await Reteno.setNotificationGroupingRule({ groupId: 'messages' });

// Disable grouping
await Reteno.setNotificationGroupingRule(null);

The rule must contain exactly one non-empty payloadKey or groupId.

If you want a summary row (the collapsed "N new messages" line shown when Android stacks the group), your app must post it itself using native Android code — this is a plain NotificationCompat API and is not exposed through the JavaScript layer. Add it to your native Android project after running npx expo prebuild (e.g. in android/app/src/main/java/.../MainApplication.kt), or via a custom Expo config plugin if you need it to survive prebuild --clean. The summary notification must use setGroup(...) with the same group ID you grouped the pushes under (either the constant groupId, or the payload value your payloadKey resolves to), in addition to setGroupSummary(true) — without a matching setGroup(...), Android will not attach the summary to the group.

Full Example

The following code listens for RetenoNotifications.received — a native SDK event that fires for every push regardless of the grouping rule — resolves the group the push belongs to via RetenoNotificationGroupingRuleProvider.resolveGroup(...), and posts a summary once at least two notifications share that group:

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.reteno.push.RetenoNotifications
import expo.modules.retenosdk.RetenoNotificationGroupingRuleProvider

class MainApplication : Application(), ReactApplication {

  companion object {
    private const val SUMMARY_CHANNEL_ID = "reteno_group_summary"
    // The "received" event fires on the main thread while the SDK posts the push's own
    // notification on a background thread — there's no ordering guarantee between the two,
    // and posting can take longer than usual (e.g. downloading a BigPictureStyle image). A
    // fixed delay is a heuristic, not a guarantee: if posting is slower than this, the summary
    // is simply skipped for this push and shown on the next one instead of undercounting silently.
    private const val SUMMARY_CHECK_DELAY_MS = 500L
  }

  override fun onCreate() {
    super.onCreate()
    // ... existing onCreate body ...
    createSummaryNotificationChannel()
    RetenoNotifications.received.addListener { bundle -> onPushReceivedForGrouping(bundle) }
  }

  private fun createSummaryNotificationChannel() {
    val channel = NotificationChannel(
      SUMMARY_CHANNEL_ID,
      "Grouped notifications summary",
      NotificationManager.IMPORTANCE_DEFAULT
    )
    NotificationManagerCompat.from(this).createNotificationChannel(channel)
  }

  private fun onPushReceivedForGrouping(bundle: Bundle) {
    val payload = bundle.keySet().associateWith { bundle.getString(it) }
    val group = RetenoNotificationGroupingRuleProvider.resolveGroup(this, payload) ?: return
    Handler(Looper.getMainLooper()).postDelayed(
      { showGroupSummaryNotification(group) },
      SUMMARY_CHECK_DELAY_MS
    )
  }

  private fun showGroupSummaryNotification(group: String) {
    val manager = NotificationManagerCompat.from(this)
    // Exclude the summary itself from the count — once posted, it also carries this group key.
    val groupedCount = manager.activeNotifications.count {
      it.notification.group == group && !NotificationCompat.isGroupSummary(it.notification)
    }
    if (groupedCount < 2) return

    if (ActivityCompat.checkSelfPermission(
        this,
        Manifest.permission.POST_NOTIFICATIONS
      ) != PackageManager.PERMISSION_GRANTED
    ) return

    val summary = NotificationCompat.Builder(this, SUMMARY_CHANNEL_ID)
      .setContentTitle("New notifications")
      .setContentText("You have $groupedCount new notifications")
      .setSmallIcon(R.mipmap.ic_launcher)
      .setGroup(group)
      .setGroupSummary(true)
      // Prevent a duplicate alert (sound/vibration) on top of the child notification's own alert.
      .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
      .setOnlyAlertOnce(true)
      .setAutoCancel(true)
      .build()

    manager.notify(group.hashCode(), summary)
  }
}

RetenoNotificationGroupingRuleProvider.resolveGroup(context, payload) is a small public helper exposed by the SDK specifically for this use case — it re-evaluates the currently configured rule (payloadKey or groupId) against a given push's payload, so you don't need to duplicate that logic yourself. This code does not survive expo prebuild --clean unless added via a custom config plugin.

Auto-open Links Behavior

Use these methods to control whether SDK opens links from push/in-app automatically:

import Reteno from 'expo-reteno-sdk';

await Reteno.setAutoOpenLinks(true); // enable
const isEnabled = await Reteno.getAutoOpenLinks();
console.log('Auto-open links:', isEnabled);

Default value:

  • iOS: true
  • Android: false

Important for Expo

  • expo-reteno-sdk requires a development build or bare app.
  • Expo Go is not supported for push features using native module integration.