Skip to content

Commit

Permalink
feat(messaging, ios): add provideAppNotificationSettings iOS permissi…
Browse files Browse the repository at this point in the history
…on / handler (#5972)

* feat: add provideAppNotificationSettings iOS perm
* feat: impl. openSettingsForNotification handler
* feat: add impl. for quit state
* doc: improve documentation for feature
* fix: return silently on android
  • Loading branch information
baylesa-dev authored Dec 31, 2021
1 parent 8efda50 commit 59cbe9f
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 10 deletions.
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

1 comment on commit 59cbe9f

@vercel
Copy link

@vercel vercel bot commented on 59cbe9f Dec 31, 2021

Choose a reason for hiding this comment

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

Please sign in to comment.