Docs
grovs.io Start for free

Push notifications

Receive Grovs push campaigns in a React Native app, report opens, follow their links and show campaign images.

Requires React Native SDK 3.1.0

The wrapper reports push campaign opens and hands every tapped Grovs push to onDeeplinkReceived, like a deep link. Campaigns come from the dashboard, a journey or the server API. To create them, see Push campaigns.

Push needs the next react-native-grovs-wrapper release, which wraps iOS SDK 3.1 and Android SDK 3.1.0. Wrapper 3.0.x only passes the push token. Push is opt-in: nothing changes until you add the setup below.

1. Upload your credentials

In the Grovs dashboard, open SDK Setup → iOS and upload your APNs key (.p8, with its Key ID and Team ID). Open SDK Setup → Android and add your Firebase credentials. Step by step: iOS and Android.

The test environment sends through the APNs sandbox, production through production APNs. Debug builds get sandbox tokens, so use useTestEnvironment: true for them and false for TestFlight and App Store builds.

2. Set up the native side

With Expo

Set "push": true in the config plugin options:

JSON
{
  "expo": {
    "ios": { "bundleIdentifier": "com.medly.app" },
    "android": { "googleServicesFile": "./google-services.json" },
    "plugins": [
      ["react-native-grovs-wrapper", {
        "apiKey": "your-api-key",
        "scheme": "your_app_scheme",
        "push": true,
        "iosRichPush": true,
        "appleTeamId": "ABCDE12345",
        "androidNotificationIcon": "ic_notification"
      }]
    ]
  }
}
PropertyDefaultDescription
pushfalseAdds the native push setup listed below.
iosRichPushfalseCreates the GrovsNotificationService extension for campaign images. Needs push and ios.bundleIdentifier.
appleTeamIdnoneTeam used to sign the extension. The plugin doesn't read ios.appleTeamId, so set it here.
androidNotificationIconnoneName of a drawable used as the small notification icon. Works without push.

With push, the plugin adds:

  • iOS: the aps-environment entitlement (development if not already set), a didRegisterForRemoteNotificationsWithDeviceToken method that calls Grovs.setPushToken(deviceToken:), and a userNotificationCenter(_:didReceive:withCompletionHandler:) method that calls Grovs.handleNotificationResponse. If those methods exist, it adds the Grovs call inside them.
  • Android: com.google.firebase:firebase-messaging:24.0.3 (skipped when @react-native-firebase/messaging is installed) and the POST_NOTIFICATIONS permission. When @react-native-firebase/messaging or expo-notifications is installed, it also removes the Grovs MessagingService, so that library keeps receiving Firebase messages.

Expo applies android.googleServicesFile itself. Your app still asks for permission and registers for remote notifications, for example with expo-notifications.

The injected userNotificationCenter method only runs if your AppDelegate is the notification center delegate. The plugin never sets the delegate. If expo-notifications or another library owns it, report taps from JavaScript instead (see step 3).

Run npx expo prebuild --clean after changing these options. Save manual native edits first.

iOS without Expo

  1. In Xcode, add the Push Notifications capability to your app target.
  2. Ask for permission and call registerForRemoteNotifications() from your app.
  3. Send the token. Forward it from AppDelegate.swift:
Swift
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    Grovs.setPushToken(deviceToken: deviceToken)
}

Or, with @react-native-firebase/messaging, pass the APNs token from JavaScript:

typescript
import messaging from '@react-native-firebase/messaging';
import Grovs from 'react-native-grovs-wrapper';
 
const token = await messaging().getAPNSToken();
if (token) {
    Grovs.setPushToken(token);
}

Android without Expo

  1. Add your google-services.json, the Google Services Gradle plugin and Firebase Messaging to your app. The wrapper compiles against Firebase Messaging but doesn't bring it in.
  2. For Android 13+, declare the permission in AndroidManifest.xml and ask for it at runtime:
XML
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
  1. Optional: set the small notification icon inside <application>:
XML
<meta-data android:name="io.grovs.NotificationIconSmall" android:resource="@drawable/ic_notification" />

Token. When Firebase Messaging is in your app, the wrapper fetches the FCM token when its native module starts. While the Grovs MessagingService is in place, it also gets new tokens. If you removed that service (another push library), pass refreshed tokens yourself:

typescript
messaging().onTokenRefresh((token) => Grovs.setPushToken(token));

3. Report taps

iOS

Wire exactly one of these paths. Wiring both counts every tap twice.

  • Native. If your AppDelegate is the UNUserNotificationCenter delegate, forward the response:
Swift
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    Grovs.handleNotificationResponse(response)
    completionHandler()
}
  • JavaScript. If a push library owns the delegate, call handleNotificationOpen from its tap listeners. Examples are below.

Android

Taps on notifications Android shows for you reach the launcher intent, which the Grovs.onStart and Grovs.onNewIntent calls in MainActivity already handle. No extra code.

If a push library shows the notification (for example expo-notifications while the app is open), its tap doesn't carry the launcher intent. Call handleNotificationOpen from that library's tap listener. A tap the launcher intent already handled isn't handled again, so calling it for every tap is safe on Android.

From a push library

handleNotificationOpen(data) takes the notification's data payload and resolves true when it was a Grovs push. Call it for taps only, never when a push is delivered, or each delivery counts as an open.

typescript
// @react-native-firebase/messaging
messaging().onNotificationOpenedApp((message) => {
    Grovs.handleNotificationOpen(message.data ?? {});
});
const initial = await messaging().getInitialNotification();
if (initial) {
    Grovs.handleNotificationOpen(initial.data ?? {});
}
typescript
// expo-notifications
import * as Notifications from 'expo-notifications';
 
let lastHandledId: string | undefined;
const handleTap = (response: Notifications.NotificationResponse) => {
    const { request } = response.notification;
    // On a cold start the same tap can reach the listener and getLastNotificationResponseAsync.
    if (request.identifier === lastHandledId) {
        return;
    }
    lastHandledId = request.identifier;
    // Stops the tap being replayed after a JS reload. Skip if other code reads the last response.
    Notifications.clearLastNotificationResponseAsync();
    // On iOS, content.data holds only the "body" key of an APNs push; the full payload is in the trigger.
    Grovs.handleNotificationOpen((request.trigger as any)?.payload ?? request.content.data);
};
Notifications.addNotificationResponseReceivedListener(handleTap);
const last = await Notifications.getLastNotificationResponseAsync();
if (last) {
    handleTap(last);
}

isGrovsNotification(data) resolves true for a Grovs payload without handling it. Both methods resolve false for an empty payload. A payload is a Grovs push when it has a non-empty grovs_campaign_id.

What your app receives

A tapped Grovs push arrives in onDeeplinkReceived:

  • data: the push's payload: your campaign's custom data keys plus grovs_campaign_id, and grovs_link and grovs_image when set. On Android the values are strings.
  • Campaign with a link: the native SDK resolves it like any Grovs link. link is the resolved link, and data is the link's data merged with the push's keys. On a clash the push's value wins.
  • Campaign without a link: link is missing and data is the push's payload.
typescript
Grovs.onDeeplinkReceived((response) => {
    if (response.data?.grovs_campaign_id) {
        // opened from a push campaign
    }
    if (response.data?.screen === 'refill') {
        navigation.navigate('Refill');
    }
});

A tap on a push with a Grovs link counts once, as a campaign open (push_open), not also as a link open. While the SDK is disabled with setSDK(false), taps deliver nothing and no opens are reported. Retry and restart behaviour: iOS delivery guarantees.

Another push library on Android

Only one service receives Firebase messages. If your app uses @react-native-firebase/messaging or expo-notifications, remove the Grovs service so that library keeps working. The Expo plugin does this for you. In a bare project, add xmlns:tools="http://schemas.android.com/tools" to <manifest>, then inside <application>:

XML
<service android:name="io.grovs.handlers.MessagingService" tools:node="remove" />

Taps and background pushes still work. Grovs no longer draws pushes while the app is open; your push library does, so forward its taps with handleNotificationOpen.

If your app has its own FirebaseMessagingService, remove the Grovs service the same way and forward to Grovs:

Kotlin
import io.grovs.GrovsMessaging
 
override fun onMessageReceived(message: RemoteMessage) {
    GrovsMessaging.handleMessage(this, message)
}
 
override fun onNewToken(token: String) {
    GrovsMessaging.onNewToken(token)
}

Campaign images (iOS)

Images in iOS pushes need a Notification Service Extension. With Expo, "iosRichPush": true creates it:

  • target GrovsNotificationService, bundle ID <app bundle id>.GrovsNotificationService
  • a Podfile block with pod 'GrovsNotificationService', '~> 3.1', using the same use_frameworks! linkage as the app (including expo-build-properties useFrameworks)
  • the EAS appExtensions entry, so EAS Build signs the extension

If another plugin forces a different linkage, align it. With EAS remote versioning, check that the extension's build number matches the app's.

Without Expo, add a Notification Service Extension target in Xcode, then add the pod to it in your Podfile:

Ruby
target 'GrovsNotificationService' do
  pod 'GrovsNotificationService', '~> 3.1'
end

Make the extension's class a subclass and run pod install again:

Swift
import GrovsNotificationService
 
class NotificationService: GrovsNotificationServiceExtension {}

Formats and an existing extension: iOS campaign images. On Android, pushes drawn by the Grovs MessagingService show the image with no extra setup.

Asking for permission from an in-app message

An in-app message can carry a button that asks for push permission. On Android 13+ the button works only if your manifest declares POST_NOTIFICATIONS.

See also

Last updated 2026-10-26
Was this page helpful?