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

feat(messaging, ios): add provideAppNotificationSettings iOS permission / handler #5972

82 changes: 74 additions & 8 deletions docs/messaging/ios-permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,15 @@ await messaging().requestPermission({

The full list of permission settings can be seen in the table below along with their default values:

| Permission | Default | Description |
| -------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `alert` | `true` | Sets whether notifications can be displayed to the user on the device. |
| `announcement` | `false` | If enabled, Siri will read the notification content out when devices are connected to AirPods. |
| `badge` | `true` | Sets whether a notification dot will appear next to the app icon on the device when there are unread notifications. |
| `carPlay` | `true` | Sets whether notifications will appear when the device is connected to [CarPlay](https://www.apple.com/ios/carplay/). |
| `provisional` | `false` | Sets whether provisional permissions are granted. See [Provisional permission](#provisional-permission) for more information. |
| `sound` | `true` | Sets whether a sound will be played when a notification is displayed on the device. |
| Permission | Default | Description |
| --------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `alert` | `true` | Sets whether notifications can be displayed to the user on the device. |
| `announcement` | `false` | If enabled, Siri will read the notification content out when devices are connected to AirPods. |
| `badge` | `true` | Sets whether a notification dot will appear next to the app icon on the device when there are unread notifications. |
| `carPlay` | `true` | Sets whether notifications will appear when the device is connected to [CarPlay](https://www.apple.com/ios/carplay/). |
| `provisional` | `false` | Sets whether provisional permissions are granted. See [Provisional permission](#provisional-permission) for more information. |
| `sound` | `true` | Sets whether a sound will be played when a notification is displayed on the device. |
| `providesAppNotificationSettings` | `false` | Indicates the system to display a button for in-app notification settings. |

The settings provided will be stored by the device and will be visible in the iOS Settings UI for your application.

Expand Down Expand Up @@ -119,3 +120,68 @@ await messaging().requestPermission({
```

Users can then choose a permission option via the notification itself, and select whether they can continue to display quietly, display prominently or not at all.

### Handle button for in-app notifications settings

Devices on iOS 12+ can provide a button in iOS Notifications Settings _(at OS level: `Settings -> [App name] -> Notifications`)_ to redirect users to in-app notifications settings.

1. Request `providesAppNotificationSettings` permissions:

```typescript
await messaging().requestPermission({ providesAppNotificationSettings: true });
```

2. Handle interaction when app is in background state:

```typescript
// index.js
import { AppRegistry } from 'react-native'
import messaging from '@react-native-firebase/messaging'

...

messaging().setOpenSettingsForNotificationsHandler(async () => {
// Set persistent value, using the MMKV package just as an example of how you might do it
MMKV.setBool(openSettingsForNotifications, true)
})

...

AppRegistry.registerComponent(appName, () => App)
```

```typescript
// App.tsx

const App = () => {
const [openSettingsForNotifications] = useMMKVStorage('openSettingsForNotifications', MMKV, false)

useEffect(() => {
if (openSettingsForNotifications) {
navigate('NotificationsSettingsScreen')
}
}, [openSettingsForNotifications])

...
}
```

3. Handle interaction when app is in quit state:

```typescript
// App.tsx

const App = () => {
useEffect(() => {
messaging()
.getDidOpenSettingsForNotification()
.then(async didOpenSettingsForNotification => {
if (didOpenSettingsForNotification) {
navigate('NotificationsSettingsScreen')
}
})
}, [])

...
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,16 @@ NS_ASSUME_NONNULL_BEGIN
@interface RNFBMessagingUNUserNotificationCenter : NSObject <UNUserNotificationCenterDelegate>

@property NSDictionary *initialNotification;
@property BOOL didOpenSettingsForNotification;
@property(nonatomic, nullable, weak) id<UNUserNotificationCenterDelegate> originalDelegate;

+ (_Nonnull instancetype)sharedInstance;

- (void)observe;

- (nullable NSDictionary *)getInitialNotification;

- (NSNumber *)getDidOpenSettingsForNotification;
@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ + (instancetype)sharedInstance {
dispatch_once(&once, ^{
sharedInstance = [[RNFBMessagingUNUserNotificationCenter alloc] init];
sharedInstance.initialNotification = nil;
sharedInstance.didOpenSettingsForNotification = NO;
});
return sharedInstance;
}
Expand Down Expand Up @@ -68,6 +69,15 @@ - (nullable NSDictionary *)getInitialNotification {
return nil;
}

- (NSNumber *)getDidOpenSettingsForNotification {
if (_didOpenSettingsForNotification != NO) {
_didOpenSettingsForNotification = NO;
return @YES;
}

return @NO;
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center
willPresentNotification:(UNNotification *)notification
withCompletionHandler:
Expand Down Expand Up @@ -119,7 +129,15 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
openSettingsForNotification:(nullable UNNotification *)notification {
if (_originalDelegate != nil && originalDelegateRespondsTo.openSettingsForNotification) {
[_originalDelegate userNotificationCenter:center openSettingsForNotification:notification];
if (@available(iOS 12.0, *)) {
[_originalDelegate userNotificationCenter:center openSettingsForNotification:notification];
}
} else {
NSDictionary *notificationDict = [RNFBMessagingSerializer notificationToDict:notification];
[[RNFBRCTEventEmitter shared] sendEventWithName:@"messaging_settings_for_notification_opened"
body:notificationDict];

_didOpenSettingsForNotification = YES;
}
}

Expand Down
11 changes: 11 additions & 0 deletions packages/messaging/ios/RNFBMessaging/RNFBMessagingModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ - (NSDictionary *)constantsToExport {
resolve([[RNFBMessagingUNUserNotificationCenter sharedInstance] getInitialNotification]);
}

RCT_EXPORT_METHOD(getDidOpenSettingsForNotification
: (RCTPromiseResolveBlock)resolve
: (RCTPromiseRejectBlock)reject) {
resolve(
[[RNFBMessagingUNUserNotificationCenter sharedInstance] getDidOpenSettingsForNotification]);
}

RCT_EXPORT_METHOD(setAutoInitEnabled
: (BOOL)enabled
: (RCTPromiseResolveBlock)resolve
Expand Down Expand Up @@ -217,6 +224,10 @@ - (NSDictionary *)constantsToExport {
options |= UNAuthorizationOptionCarPlay;
}

if ([permissions[@"providesAppNotificationSettings"] isEqual:@(YES)]) {
options |= UNAuthorizationOptionProvidesAppNotificationSettings;
}

UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
[center requestAuthorizationWithOptions:options
completionHandler:^(BOOL granted, NSError *_Nullable error) {
Expand Down
31 changes: 31 additions & 0 deletions packages/messaging/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,15 @@ export namespace FirebaseMessagingTypes {
* Defaults to true.
*/
sound?: boolean;

/**
* Request permission to display a button for in-app notification settings.
*
* Default to false
*
* @platform ios iOS >= 12
*/
providesAppNotificationSettings?: boolean;
}

/**
Expand Down Expand Up @@ -558,6 +567,17 @@ export namespace FirebaseMessagingTypes {
*/
getInitialNotification(): Promise<RemoteMessage | null>;

/**
* When the app is opened from iOS notifications settings from a quit state,
* this method will return `true` or `false` if the app was opened via another method.
*
* See `setOpenSettingsForNotificationsHandler` to subscribe to when the notificiation is opened when the app
* is in background state.
*
* @ios iOS >= 12
*/
getDidOpenSettingsForNotification(): Promise<boolean>;

/**
* Returns an FCM token for this device. Optionally you can specify a custom authorized entity
* or scope to tailor tokens to your own use-case.
Expand Down Expand Up @@ -886,6 +906,17 @@ export namespace FirebaseMessagingTypes {
*/
setBackgroundMessageHandler(handler: (message: RemoteMessage) => Promise<any>): void;

/**
* Set a handler function which is called when the `${App Name} notifications settings`
* link in iOS settings is clicked.
*
* This method must be called **outside** of your application lifecycle, e.g. alongside your
* `AppRegistry.registerComponent()` method call at the the entry point of your application code.
*
* @ios iOS >= 12
*/
setOpenSettingsForNotificationsHandler(handler: (message: RemoteMessage) => any): void;

/**
* Send a new `RemoteMessage` to the FCM server.
*
Expand Down
38 changes: 37 additions & 1 deletion packages/messaging/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const namespace = 'messaging';
const nativeModuleName = 'RNFBMessagingModule';

let backgroundMessageHandler;
let openSettingsForNotificationHandler;

class FirebaseMessagingModule extends FirebaseModule {
constructor(...args) {
Expand Down Expand Up @@ -92,6 +93,19 @@ class FirebaseMessagingModule extends FirebaseModule {

return backgroundMessageHandler(remoteMessage);
});

this.emitter.addListener('messaging_settings_for_notification_opened', remoteMessage => {
if (!openSettingsForNotificationHandler) {
// eslint-disable-next-line no-console
console.warn(
'No handler for notification settings link has been set. Set a handler via the "setOpenSettingsForNotificationsHandler" method',
);

return Promise.resolve();
}

return openSettingsForNotificationHandler(remoteMessage);
});
}
}

Expand Down Expand Up @@ -130,6 +144,11 @@ class FirebaseMessagingModule extends FirebaseModule {
});
}

getDidOpenSettingsForNotification() {
if (!isIOS) return Promise.resolve(false);
return this.native.getDidOpenSettingsForNotification().then(value => value);
}

getIsHeadless() {
return this.native.getIsHeadless();
}
Expand Down Expand Up @@ -190,6 +209,7 @@ class FirebaseMessagingModule extends FirebaseModule {
provisional: false,
sound: true,
criticalAlert: false,
providesAppNotificationSettings: false,
};

if (!permissions) {
Expand Down Expand Up @@ -311,6 +331,20 @@ class FirebaseMessagingModule extends FirebaseModule {
}
}

setOpenSettingsForNotificationsHandler(handler) {
if (!isIOS) {
return;
}

if (!isFunction(handler)) {
throw new Error(
"firebase.messaging().setOpenSettingsForNotificationsHandler(*) 'handler' expected a function.",
);
}

openSettingsForNotificationHandler = handler;
}

sendMessage(remoteMessage) {
if (isIOS) {
throw new Error(`firebase.messaging().sendMessage() is only supported on Android devices.`);
Expand Down Expand Up @@ -389,7 +423,9 @@ export default createModuleNamespace({
'messaging_message_received',
'messaging_message_send_error',
'messaging_notification_opened',
...(isIOS ? ['messaging_message_received_background'] : []),
...(isIOS
? ['messaging_message_received_background', 'messaging_settings_for_notification_opened']
: []),
],
hasMultiAppSupport: false,
hasCustomUrlOrRegionSupport: false,
Expand Down