# Push notifications

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

Source: https://docs.grovs.io/docs/sdk/react-native/push-notifications

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](https://docs.grovs.io/docs/engagement/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](https://app.grovs.io), 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](https://docs.grovs.io/docs/sdk/ios/push-notifications#2-upload-your-apns-key-to-grovs) and [Android](https://docs.grovs.io/docs/sdk/android/push-notifications).

> **Warning:** 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"
      }]
    ]
  }
}
```

| Property                  | Default | Description                                                                                                                            |
| ------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `push`                    | `false` | Adds the native push setup listed below.                                                                                               |
| `iosRichPush`             | `false` | Creates the `GrovsNotificationService` extension for [campaign images](#campaign-images-ios). 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-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`.

> **Warning:** 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](#3-report-taps)).

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" />
```

3. 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](#another-push-library-on-android)), 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](#from-a-push-library).

### 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`](https://docs.grovs.io/docs/sdk/react-native/deep-linking):

- **`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](https://docs.grovs.io/docs/sdk/ios/push-notifications#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`](#from-a-push-library).

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](https://docs.grovs.io/docs/sdk/ios/push-notifications#4-campaign-images-optional). 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](https://docs.grovs.io/docs/sdk/react-native/in-app-messages) 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](https://docs.grovs.io/docs/sdk/react-native/in-app-messages)
- [Deep linking](https://docs.grovs.io/docs/sdk/react-native/deep-linking): `onDeeplinkReceived`
- [API reference](https://docs.grovs.io/docs/sdk/react-native/api-reference#push-notifications)
- [iOS push notifications](https://docs.grovs.io/docs/sdk/ios/push-notifications) and [Android push notifications](https://docs.grovs.io/docs/sdk/android/push-notifications): native details
