Push notifications
Receive Grovs push campaigns in a React Native app, report opens, follow their links and show campaign images.
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:
{
"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"
}]
]
}
}| Property | Default | Description |
|---|---|---|
push | false | Adds the native push setup listed below. |
iosRichPush | false | Creates the GrovsNotificationService extension for campaign images. Needs push and ios.bundleIdentifier. |
appleTeamId | none | Team used to sign the extension. The plugin doesn't read ios.appleTeamId, so set it here. |
androidNotificationIcon | none | Name of a drawable used as the small notification icon. Works without push. |
With push, the plugin adds:
- iOS: the
aps-environmententitlement (developmentif not already set), adidRegisterForRemoteNotificationsWithDeviceTokenmethod that callsGrovs.setPushToken(deviceToken:), and auserNotificationCenter(_:didReceive:withCompletionHandler:)method that callsGrovs.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/messagingis installed) and thePOST_NOTIFICATIONSpermission. When@react-native-firebase/messagingorexpo-notificationsis installed, it also removes the GrovsMessagingService, 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
- In Xcode, add the Push Notifications capability to your app target.
- Ask for permission and call
registerForRemoteNotifications()from your app. - Send the token. Forward it from
AppDelegate.swift:
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Grovs.setPushToken(deviceToken: deviceToken)
}Or, with @react-native-firebase/messaging, pass the APNs token from JavaScript:
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
- 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. - For Android 13+, declare the permission in
AndroidManifest.xmland ask for it at runtime:
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />- Optional: set the small notification icon inside
<application>:
<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:
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
AppDelegateis theUNUserNotificationCenterdelegate, forward the response:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
Grovs.handleNotificationResponse(response)
completionHandler()
}- JavaScript. If a push library owns the delegate, call
handleNotificationOpenfrom 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.
// @react-native-firebase/messaging
messaging().onNotificationOpenedApp((message) => {
Grovs.handleNotificationOpen(message.data ?? {});
});
const initial = await messaging().getInitialNotification();
if (initial) {
Grovs.handleNotificationOpen(initial.data ?? {});
}// 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 plusgrovs_campaign_id, andgrovs_linkandgrovs_imagewhen set. On Android the values are strings.- Campaign with a link: the native SDK resolves it like any Grovs link.
linkis the resolved link, anddatais the link's data merged with the push's keys. On a clash the push's value wins. - Campaign without a link:
linkis missing anddatais the push's payload.
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>:
<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:
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
Podfileblock withpod 'GrovsNotificationService', '~> 3.1', using the sameuse_frameworks!linkage as the app (includingexpo-build-propertiesuseFrameworks) - the EAS
appExtensionsentry, 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:
target 'GrovsNotificationService' do
pod 'GrovsNotificationService', '~> 3.1'
endMake the extension's class a subclass and run pod install again:
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
- In-app messages
- Deep linking:
onDeeplinkReceived - API reference
- iOS push notifications and Android push notifications: native details