# Push notifications

> Receive Grovs push campaigns in a Flutter app: pass the token, count taps, follow campaign links and show images.

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

The plugin counts taps on Grovs push campaigns and delivers each tapped 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).

Tap handling, `isGrovsNotification` and `handleNotificationOpen` need plugin **3.2.0** or later.

> **Note:** Push needs a physical device. It doesn't work in the iOS Simulator, and on Android it needs a device or emulator with Google Play services.

## iOS

### 1. Upload your APNs key to Grovs

1. In your [Apple Developer account](https://developer.apple.com/account/resources/authkeys/list), create a key with **Apple Push Notifications service (APNs)** enabled.
2. Download the `.p8` file. Note the **Key ID** and your **Team ID**.
3. In the [Grovs dashboard](https://app.grovs.io), open **SDK Setup → iOS**. In the **Push Notifications** step, upload the `.p8` file and enter the Key ID and Team ID.

> **Warning:** **Test** sends through the APNs sandbox, **production** through production APNs. Builds run from Xcode or `flutter run` get sandbox tokens, so set `GrovsUseTestEnvironment` to `true` for them and `false` for TestFlight and App Store builds. APNs rejects a token from the other environment.

### 2. Register for pushes

In Xcode, select the **Runner** target, open **Signing & Capabilities** and add **Push Notifications**. Grovs pushes are regular alerts, so Grovs doesn't need the **Remote notifications** background mode.

The app must ask for permission and register with APNs, or it never gets a token. [`firebase_messaging`](https://pub.dev/packages/firebase_messaging) registers for you; call its `requestPermission()` for the permission. Without it, add this to `ios/Runner/AppDelegate.swift`:

```swift
// In application(_:didFinishLaunchingWithOptions:), before returning
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in }
application.registerForRemoteNotifications()
```

### 3. Set the notification center delegate

The plugin shows and counts Grovs pushes through the system notification center. Set the delegate in `ios/Runner/AppDelegate.swift`:

```swift
import UserNotifications

// In application(_:didFinishLaunchingWithOptions:), before returning
UNUserNotificationCenter.current().delegate = self
```

`FlutterAppDelegate` passes the notification center callbacks on to plugins. Without this line, taps on Grovs pushes aren't counted and Grovs pushes don't show while the app is open.

### 4. Token

Once the app is registered, the plugin passes the APNs token to Grovs. You don't need `setPushToken` on iOS. Calling it still works, but causes one extra attributes update.

### Foreground pushes

While the app is open, the plugin shows Grovs pushes as a banner, in the notification list and with sound (alert and sound on iOS 13). Pushes from other providers are left to your own code. Grovs pushes also show while the SDK is disabled; that is display only, nothing is tracked.

With `firebase_messaging` 16.x, one more step is needed. That version answers every notification before Grovs can, so nothing shows by default. Upgrade to `firebase_messaging` 17 or later, or call this once after Firebase starts:

```dart
await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
  alert: true,
  badge: true,
  sound: true,
);
```

With 16.x this also shows other Firebase pushes while the app is open.

### Campaign images (optional)

Campaign images need a **Notification Service Extension**. Add one as a new target in Xcode, then add the Grovs extension library to it in `ios/Podfile`:

```ruby
target 'NotificationService' do
  pod 'GrovsNotificationService', '~> 3.1'
end
```

Make the extension's principal class inherit from the Grovs one:

```swift
import GrovsNotificationService

class NotificationService: GrovsNotificationServiceExtension {}
```

Use JPEG, PNG or GIF images. The iOS Simulator never runs the extension, so test on a device. If you already have an extension, see [iOS campaign images](https://docs.grovs.io/docs/sdk/ios/push-notifications#if-you-already-have-an-extension).

## Android

Android push needs Firebase Cloud Messaging in your app. Add `firebase_messaging`, or add `com.google.firebase:firebase-messaging` and the `google-services` plugin yourself. The Grovs SDK doesn't bring Firebase with it.

### 1. Upload your Firebase credentials

In the [Firebase Console](https://console.firebase.google.com), open **Project Settings → Service Accounts** and generate a private key. In the [Grovs dashboard](https://app.grovs.io), open **SDK Setup → Android** and, in the **Push Notifications** step, upload the JSON key and enter your Firebase Project ID.

On Android 13 and later, the app must also ask for the `POST_NOTIFICATIONS` permission. `firebase_messaging` declares it and `requestPermission()` asks for it.

### 2. Token

When Firebase is in your app, the plugin fetches the first FCM token for you, and again each time the SDK is turned on with `setSDK(true)`. With `firebase_messaging`, its messaging service receives token refreshes, so pass them on:

```dart
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:grovs_flutter_plugin/grovs.dart';

final grovs = Grovs();

FirebaseMessaging.instance.onTokenRefresh.listen((newToken) {
  grovs.setPushToken(newToken);
});

final token = await FirebaseMessaging.instance.getToken();
if (token != null) await grovs.setPushToken(token);
```

### 3. Foreground pushes

The plugin declares the Grovs messaging service at priority -1, so `firebase_messaging` handles messages first when your app uses it.

- **Without `firebase_messaging`:** Grovs draws its pushes while the app is open.
- **With `firebase_messaging`:** Grovs doesn't draw pushes while the app is open. Draw them in your `onMessage` handler, and use `isGrovsNotification` to tell them apart:

```dart
FirebaseMessaging.onMessage.listen((message) async {
  if (await grovs.isGrovsNotification(message.data)) {
    // Show it yourself, for example with flutter_local_notifications
  }
});
```

A notification you draw yourself doesn't carry the Grovs data, so its tap isn't counted. In its tap handler, call `grovs.handleNotificationOpen(message.data)`.

In the background, Firebase shows the push. The notification channel and small icon come from your manifest; see [Android push notifications](https://docs.grovs.io/docs/sdk/android/push-notifications) for the `meta-data` keys.

### 4. Taps

Taps on Grovs pushes that Grovs or the system drew are counted with no code. For notifications you draw yourself, see step 3.

## What your app receives

A tapped Grovs push arrives on `onDeeplinkReceived`:

- **Campaign with a link:** `link` is the resolved link. `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 `null` and `data` is the push's data.

The push's keys are your campaign's custom data plus `grovs_campaign_id`, and `grovs_link` and `grovs_image` when set. On Android the push's values are strings.

```dart
grovs.onDeeplinkReceived.listen((details) {
  final link = details.link; // null for a push without a link
  final screen = details.data?['screen'];
  // route to the screen your campaign or link points at
});
```

A tap on a push with a Grovs link counts once, as a campaign open. The link isn't also counted as a link open.

While the SDK is disabled, taps aren't delivered or reported. On iOS they're dropped. On Android, a tap that opened the app is handled when `setSDK(true)` is called while the app is still in the foreground.

## Handling taps yourself

`handleNotificationOpen(data)` counts a tap and opens the push's link. Use it only when the plugin can't see the tap: on Android when you draw the notification yourself, or when the native tap path can't be used.

```dart
final handled = await grovs.handleNotificationOpen(message.data);
```

- Returns `true` when the data belongs to a Grovs push and the tap was handled.
- Returns `false` for other pushes and while the SDK is disabled.
- `isGrovsNotification(data)` checks without handling. A push is a Grovs push when it has a non-empty `grovs_campaign_id`.

> **Warning:** Call `handleNotificationOpen` only from a tap handler, never when a push is delivered, or each delivery counts as an open. Use one path per platform. On iOS, setting the notification center delegate and also calling `handleNotificationOpen` counts every tap twice.

## Testing

Create a campaign whose audience is your own user ID (the value you pass to `setUserIdentifier`). Send it to a build on the matching environment and tap it. The open shows in the campaign report.

## Asking for permission from an in-app message

An [in-app message](https://docs.grovs.io/docs/sdk/flutter/in-app-messages) can carry a button that asks for push permission. On Android 13 and later this needs `POST_NOTIFICATIONS` in your manifest; `firebase_messaging` already declares it.

## See also

- [Push campaigns](https://docs.grovs.io/docs/engagement/push-campaigns): create and send campaigns
- [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 behavior underneath
- [API reference](https://docs.grovs.io/docs/sdk/flutter/api-reference#push-notifications): every push call
- [Upgrading to 3.2](https://docs.grovs.io/docs/upgrade-guides/flutter-3-2)
