Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs(ios): - Improve Remote Notification Support section #1120

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 141 additions & 88 deletions docs-react-native/react-native/docs/ios/remote-notification-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,31 @@ next: /
previous: /react-native/docs/ios/permissions
---

It's possible to display a notification with Notifee features from outside the app using remote notifications (a.k.a push notifications) in two ways:
- using APNs keys
- using `notifee_options` with our notification service extension helper
> It is recommended to only use a notification service extension if you require an image or need to modify the contents of the notification before display.

It is recommended to only use a notification service extension if you require an image or need to modify the contents of the notification before its displayed.
It is possible to display a notification with Notifee features from outside the app using remote notifications (a.k.a push notifications) by using the combination of a`notifee_options` section in your remote notification message (see below for example) and the Notifee notification service extension helper.

### Using APNs keys
Adding a custom key `notifee_options` in the remote notification message (as shown below) enables Notifee to modify the notification before it is finally displayed to the end user. If your use case requires it, you may also use custom native code as shown below to change the notification before display.

Notification messages sent through APNs follow the APNs payload format which allows us to be able to specify a category or a custom sound with no extra configuration on the client:
> ⭐️ In order to bring the iOS experience - where data-only messages are rarely delivered - closer to the Android one where data-only messages have high delivery rates, you may still send **data-only** notifications for Android while using messages with a **notification** payload for iOS [by configuring the right payload.](#configure-the-payload)

```json
// FCM
{
notification: {
title: 'A notification title!',
body: 'A notification body',
},
apns: {
payload: {
aps: {
category: 'post', // A category that's already been created by your app
sound: 'media/kick.wav', // A local sound file you have inside your app's bundle
... // any other properties
},
},
},
...
};
```
To get started, you will need to implement a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) for your iOS app, the following steps will guide you through how to set it up.

### Using `notifee_options`
### Expo

By adding a custom key `notifee_options` in the message payload, the notification will be modified by Notifee before it is finally displayed to the end user.

To get started, you will need to implement a [Notification Service Extension](https://developer.apple.com/documentation/usernotifications/unnotificationserviceextension) for your iOS app, the following steps will guide you through how to set it up.
You can jump straight to [configuring the payload](#configure-the-payload) if you use Expo with one of the community plugins:
- https://github.com/LunatiqueCoder/expo-notifee-plugin
- https://github.com/evennit/notifee-expo-plugin

## Add the notification service extension

* From Xcode top menu go to: File > New > Target...
* A modal will present a list of possible targets, scroll down or use the filter to select Notification Service Extension. Press Next.
* Add a product name (use `NotifeeNotificationService` to follow along) and click Finish
LunatiqueCoder marked this conversation as resolved.
Show resolved Hide resolved
* You may use Swift or Objective-C where it says 'language' but be sure to follow the correct section below depending on your implementation language selection
* Make sure that the **deployment target** of your newly added extension (e.g. `NotifeeNotificationService`) matches the **deployment target** of your app. You can check it by selecting you app's target -> `Build Settings` -> `Deployment`


<!-- <Vimeo id="remote-notification-support-1" caption="Step 1 - Add Your Notification Service Extension" /> -->

## Add target to the Podfile
Expand All @@ -64,7 +46,7 @@ target 'NotifeeNotificationService' do
end

```

* Assuming you are using `react-native-firebase`, you must also add `use_frameworks!` inside the target above as [you did when you followed their docs](https://rnfirebase.io/#altering-cocoapods-to-use-frameworks).
* Install or update your pods using pod install from the ios folder

`pod install --repo-update`
Expand All @@ -87,87 +69,158 @@ At this point everything should still be running normally. This is the final ste
```objectivec
- // Modify the notification content here...
- self.bestAttemptContent.title = [NSString stringWithFormat:@"%@ [modified]", self.bestAttemptContent.title];

- self.contentHandler(self.bestAttemptContent);
+ [NotifeeExtensionHelper populateNotificationContent:request
withContent: self.bestAttemptContent
withContentHandler:contentHandler];
```
<!-- <Vimeo id="remote-notification-support-3" caption="Step 3 - Edit NotificationService.m" /> -->

Before, moving to the next step, run the app and check it builds successfully – make sure you have the correct target selected.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this suggestion for people to do a build test? We used to get a lot of problems from people where they had a mix of "did not even add the extension correctly" and "added custom code that did not work"

It was useful to separate the concerns and make sure they added things correctly before they jumped in to custom code

Should probably make that suggestion after we suggest to add the import as the build test will then make sure they have altered their Podfile correctly to have the dependency on the notifee pod and everything is linked up correctly

Copy link
Author

@LunatiqueCoder LunatiqueCoder Oct 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was useful to separate the concerns and make sure they added things correctly before they jumped in to custom code.

Hmm, that's a fair point.

However I decided to leave this "build and run app" step after all the native code changes because some people have some very slow machines and running multiple builds can be frustrating.

Also I think they might be able to do something wrong here, run the app successfully but realise it at the next steps when they will run the app a second time with the extension.

But it's up to you, reverting this line will be easy 🤞

## Mutate the content with Notifee:
> 🔗 [Apple Docs: Modifying content in newly delivered notifications](https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications#Implement-your-extensions-handler-methods)

## Edit the payload
Now everything is setup in your app, you can alter your notification payload in two ways:
#### Objective-C:

LunatiqueCoder marked this conversation as resolved.
Show resolved Hide resolved
1. Update the message payload, sent via your backend
2. In a Notification Service Extension in your app when a device receives a remote message
In your `NotifeeNotificationService.m` file you should have method `didReceiveNotificationRequest` where we are calling `NotifeeExtensionHelper`. Now you can modify
`bestAttemptContent` before you send it to `NotifeeExtensionHelper`:

> Make sure that you will set `mutable-content: 1` (mutableContent if you are using firebase admin sdk) when sending notification otherwise Notification Service Extension will NOT be triggered
```objectivec
#import "NotificationService.h"
#import "NotifeeExtensionHelper.h"

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];

// You can also modify the payload as you please here.
// You have around 30 seconds

// Notifee will mutate the notification according to notifee_options
[NotifeeExtensionHelper populateNotificationContent:request
withContent: self.bestAttemptContent
withContentHandler:contentHandler];
}
```

> Make sure that you will set `content-available: 1` (contentAvailable if you are using firebase admin sdk) if you want to receive notification when your app is in foreground
#### Swift:

In your `NotifeeNotificationService.swift` file, use the `didReceive(_:withContentHandler:)` method to enable the `NotifeeExtensionHelper`.

### 1. Update the message payload, sent via your backend
```swift
import UIKit
import RNNotifeeCore

```json
// FCM
{
notification: {
title: 'A notification title!',
body: 'A notification body',
typealias ContentHandler = (UNNotificationContent) -> Void

override func didReceive(_ request: UNNotificationRequest,
LunatiqueCoder marked this conversation as resolved.
Show resolved Hide resolved
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
self.bestAttempt = (request.content.mutableCopy() as? UNMutableNotificationContent)

// Here you can also modify the content of the notification as you please here.
// You have around 30 seconds


// Notifee will mutate the notification according to notifee_options
NotifeeExtensionHelper.populateNotificationContent(request,
with: self.bestAttempt!,
withContentHandler: contentHandler)
}
```

<br>

Before moving to the next step, run the app and check it builds successfully – make sure you have the correct target selected.

## Configure the payload
> Make sure that you will set `mutable-content: 1` (mutableContent if you are using firebase admin sdk) when sending notification otherwise Notification Service Extension will NOT be triggered
>
> 🔗 [**Apple docs:** Configure-the-payload-for-thenotification](https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications#Configure-the-payload-for-thenotification)

### Here's an example of a payload sent from the backend with Firebase Admin Node.js SDK

```ts

import type {Notification} from '@notifee/react-native/src/types/Notification';
import {AndroidImportance} from '@notifee/react-native/src/types/NotificationAndroid';
import {MulticastMessage} from 'firebase-admin/lib/messaging/messaging-api';
import admin from '../src/firebase-admin';

/**
* @link https://notifee.app/react-native/reference/notification
*/
const notifeeOptions: Notification = {
title: 'Title',
subtitle: 'Subtitle',
body: 'Main body content of the notification',
image: 'https://placeimg.com/640/480/any', // URL to pointing to a remote image
android: {
channelId: 'default',
importance: AndroidImportance.HIGH,
lightUpScreen: true,
pressAction: {
id: 'default',
},
apns: {
payload: {
aps: {
// Payloads coming from Admin SDK should specify params in camelCase.
// Payloads from REST API should specify in kebab-case
// see their respective reference documentation
'contentAvailable': 1, // Important, to receive `onMessage` event in the foreground when message is incoming
'mutableContent': 1, // Important, without this the extension won't fire
},
notifee_options: {
image: 'https://placeimg.com/640/480/any', // URL to pointing to a remote image
ios: {
sound: 'media/kick.wav', // A local sound file you have inside your app's bundle
foregroundPresentationOptions: {alert: true, badge: true, sound: true, banner: true, list: true},
categoryId: 'post', // A category that's already been created by your app
attachments: [{url: 'https://placeimg.com/640/480/any', thumbnailHidden: true}] // array of attachments of type `IOSNotificationAttachment`
... // any other api properties for NotificationIOS
},
... // any other api properties for Notification, excluding `id`
},
},
sound: 'default',
},
ios: {
sound: 'media/kick.wav', // A local sound file you have inside your app's bundle
categoryId: 'post', // Must be a category that has already been created by your app
attachments: [{url: 'https://placeimg.com/640/480/any', thumbnailHidden: true}] // array of attachments of type `IOSNotificationAttachment`
// 🚧 Adding `foregroundPresentationOptions` controls how to
// 👇 behave when app is UP AND RUNNING, not terminated, AND not in background!
foregroundPresentationOptions: {
badge: true,
banner: true,
list: true,
sound: true,
},
...
},
};
```

### 2. In a Notification Service Extension in your app when a device receives a remote message

In your NotifeeNotificationService.m file you should have method `didReceiveNotificationRequest` where we are calling `NotifeeExtensionHelper`. Now you can modify
`bestAttemptContent` before you send it to `NotifeeExtensionHelper`:

```objectivec
- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
self.contentHandler = contentHandler;
self.bestAttemptContent = [request.content mutableCopy];

NSMutableDictionary *userInfoDict = [self.bestAttemptContent.userInfo mutableCopy];
userInfoDict[@"notifee_options"] = [NSMutableDictionary dictionary];
userInfoDict[@"notifee_options"][@"title"] = @"Modified Title";

self.bestAttemptContent.userInfo = userInfoDict;
/**
* @description Firebase Message
* @link https://firebase.google.com/docs/reference/admin/node/firebase-admin.messaging.basemessage.md#basemessage_interface
*/
const message: MulticastMessage = {
// ✅ We can continue using local/data-only notification for Android
// 👍 while triggering iOS remote notifications from `apns`
data: {notifee_options: JSON.stringify(notifeeOptions)},
tokens: [],
android: {
priority: 'high', // Needed to achieve reliable delivery for data-only notifications on Android
},
apns: {
payload: {
notifee_options: notifeeOptions,
aps: {
// Payloads coming from Admin SDK should specify params in camelCase.
// Payloads from REST API should specify in kebab-case
// see their respective reference documentation
alert: {
// 🚧 This is needed to achieve reliable delivery only for iOS
// 👍 but Android will continue using data-only notifications
title: 'ANY_DUMMY_STRING', // Or notifeeOptions.title :)
},
'mutableContent': 1, // Important, without this the extension won't fire
},
},
},
};

[NotifeeExtensionHelper populateNotificationContent:request
withContent: self.bestAttemptContent
withContentHandler:contentHandler];
try {
admin.messaging().sendEachForMulticast(message)
res.status(200).end();
} catch (e) {
res.status(400).end();
}
```

Please note, the `id` of the notification is the `request.identifier` and cannot be changed. For this reason, the `id` property in `notifee_options` should be excluded.

> if both `attachments` and `image` are present, `attachments` will take precedence over `image`
> If both `attachments` and `image` are present, `attachments` will take precedence over `image`

### Handling Events

Expand All @@ -176,6 +229,8 @@ Currently, notifee supports the following events for remote notifications:
- `ACTION_PRESSED`
- `DISMISSED`

> On iOS, only notifications with a `categoryId` will receive a `DISMISSED` event.

To know identify when an interaction is from a remote notification, we can check if `notification.remote` is populated:

```jsx
Expand All @@ -198,5 +253,3 @@ function App() {
}, []);
}
```

> On iOS, only notifications with a `categoryId` will receive a `DISMISSED` event.