Skip to content

Commit

Permalink
Add ability to request/check more iOS permissions (#752)
Browse files Browse the repository at this point in the history
Co-authored-by: Vincent Pizzo <[email protected]>
  • Loading branch information
DanielEliraz and Vincent Pizzo authored Jun 29, 2021
1 parent f5b25b5 commit 48a95d5
Show file tree
Hide file tree
Showing 16 changed files with 130 additions and 70 deletions.
17 changes: 13 additions & 4 deletions example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ class NotificationsExampleApp extends Component {
}

requestPermissionsIos(options) {
Notifications.ios.registerRemoteNotifications(options);
Notifications.ios.registerRemoteNotifications(
Object.fromEntries(options.map(opt => [opt, true]))
);
}

requestPermissions() {
Expand Down Expand Up @@ -146,6 +148,12 @@ class NotificationsExampleApp extends Component {
);
}

checkPermissions() {
Notifications.ios.checkPermissions().then((currentPermissions) => {
console.warn(currentPermissions);
});
}

render() {
const notifications = this.state.notifications.map((notification, idx) =>
(
Expand All @@ -163,9 +171,10 @@ class NotificationsExampleApp extends Component {
<View style={styles.container}>
<Button title={'Request permissions'} onPress={this.requestPermissions} testID={'requestPermissions'} />
{Platform.OS === 'ios' && Platform.Version > '12.0' && (<>
<Button title={'Request permissions with app notification settings'} onPress={() => this.requestPermissionsIos(['ProvidesAppNotificationSettings'])} testID={'requestPermissionsWithAppSettings'} />
<Button title={'Request permissions with provisional'} onPress={() => this.requestPermissionsIos(['Provisional'])} testID={'requestPermissionsWithAppSettings'} />
<Button title={'Request permissions with app notification settings and provisional'} onPress={() => this.requestPermissionsIos(['ProvidesAppNotificationSettings', 'Provisional'])} testID={'requestPermissionsWithAppSettings'} />
<Button title={'Request permissions with app notification settings'} onPress={() => this.requestPermissionsIos(['providesAppNotificationSettings'])} testID={'requestPermissionsWithAppSettings'} />
<Button title={'Request permissions with provisional'} onPress={() => this.requestPermissionsIos(['provisional'])} testID={'requestPermissionsWithAppSettings'} />
<Button title={'Request permissions with app notification settings and provisional'} onPress={() => this.requestPermissionsIos(['providesAppNotificationSettings', 'provisional'])} testID={'requestPermissionsWithAppSettings'} />
<Button title={'Check permissions'} onPress={this.checkPermissions} />
</>)}
{Platform.OS === 'android' &&
<Button title={'Set channel'} onPress={this.setNotificationChannel} testID={'setNotificationChannel'} />
Expand Down
2 changes: 1 addition & 1 deletion lib/ios/RNBridgeModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ - (dispatch_queue_t)methodQueue {

#pragma mark - JS interface

RCT_EXPORT_METHOD(requestPermissions:(NSArray *)options) {
RCT_EXPORT_METHOD(requestPermissions:(NSDictionary *)options) {
[_commandsHandler requestPermissions:options];
}

Expand Down
2 changes: 1 addition & 1 deletion lib/ios/RNCommandsHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

- (instancetype)init;

- (void)requestPermissions:(NSArray *)options;
- (void)requestPermissions:(NSDictionary *)options;

- (void)setCategories:(NSArray *)categories;

Expand Down
2 changes: 1 addition & 1 deletion lib/ios/RNCommandsHandler.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ - (instancetype)init {
return self;
}

- (void)requestPermissions:(NSArray *)options {
- (void)requestPermissions:(NSDictionary *)options {
[_notificationCenter requestPermissions:options];
}

Expand Down
2 changes: 1 addition & 1 deletion lib/ios/RNNotificationCenter.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError

- (void)isRegisteredForRemoteNotifications:(RCTPromiseResolveBlock)resolve;

- (void)requestPermissions:(NSArray *)options;
- (void)requestPermissions:(NSDictionary *)options;

- (void)setCategories:(NSArray *)json;

Expand Down
56 changes: 38 additions & 18 deletions lib/ios/RNNotificationCenter.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@

@implementation RNNotificationCenter

- (void)requestPermissions:(NSArray *)options {
- (void)requestPermissions:(NSDictionary *)options {
BOOL carPlay = [options[@"carPlay"] boolValue];
BOOL criticalAlert = [options[@"criticalAlert"] boolValue];
BOOL providesAppNotificationSettings = [options[@"providesAppNotificationSettings"] boolValue];
BOOL provisional = [options[@"provisional"] boolValue];
BOOL announcement = [options[@"announcement"] boolValue];
UNAuthorizationOptions authOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert);
if ([options count] > 0) {
for (NSString* option in options) {
if ([option isEqualToString:@"ProvidesAppNotificationSettings"]) {
if (@available(iOS 12.0, *)) {
authOptions = authOptions | UNAuthorizationOptionProvidesAppNotificationSettings;
}
}
if ([option isEqualToString:@"Provisional"]) {
if (@available(iOS 12.0, *)) {
authOptions = authOptions | UNAuthorizationOptionProvisional;
}
}
if (carPlay) {
authOptions = authOptions | UNAuthorizationOptionCarPlay;
}
if (@available(iOS 12.0, *)) {
if (criticalAlert) {
authOptions = authOptions | UNAuthorizationOptionCriticalAlert;
}
if (providesAppNotificationSettings) {
authOptions = authOptions | UNAuthorizationOptionProvidesAppNotificationSettings;
}
if (provisional) {
authOptions = authOptions | UNAuthorizationOptionProvisional;
}
}
if (@available(iOS 13.0, *)) {
if (announcement) {
authOptions = authOptions | UNAuthorizationOptionAnnouncement;
}
}

Expand Down Expand Up @@ -93,11 +103,21 @@ - (void)isRegisteredForRemoteNotifications:(RCTPromiseResolveBlock)resolve {

- (void)checkPermissions:(RCTPromiseResolveBlock)resolve {
[[UNUserNotificationCenter currentNotificationCenter] getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
resolve(@{
@"badge": [NSNumber numberWithBool:settings.badgeSetting == UNNotificationSettingEnabled],
@"sound": [NSNumber numberWithBool:settings.soundSetting == UNNotificationSettingEnabled],
@"alert": [NSNumber numberWithBool:settings.alertSetting == UNNotificationSettingEnabled],
});
NSMutableDictionary* allSettings = [NSMutableDictionary new];
[allSettings addEntriesFromDictionary:@{
@"badge": [NSNumber numberWithBool:settings.badgeSetting == UNNotificationSettingEnabled],
@"sound": [NSNumber numberWithBool:settings.soundSetting == UNNotificationSettingEnabled],
@"alert": [NSNumber numberWithBool:settings.alertSetting == UNNotificationSettingEnabled],
@"carPlay": [NSNumber numberWithBool:settings.carPlaySetting == UNNotificationSettingEnabled],
}];
if (@available(iOS 12.0, *)) {
allSettings[@"criticalAlert"] = [NSNumber numberWithBool:settings.criticalAlertSetting == UNNotificationSettingEnabled];
allSettings[@"providesAppNotificationSettings"] = [NSNumber numberWithBool:settings.providesAppNotificationSettings];
}
if (@available(iOS 13.0, *)) {
allSettings[@"announcement"] = [NSNumber numberWithBool:settings.announcementSetting == UNNotificationSettingEnabled];
}
resolve(allSettings);
}];
}
@end
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,32 @@ - (void)testRequestPermissions_userAuthorizedPermissions {
[[_notificationCenter expect] getNotificationSettingsWithCompletionHandler:[OCMArg invokeBlockWithArgs:settings, nil]];
[[(id)[UIApplication sharedApplication] expect] registerForRemoteNotifications];

[_uut requestPermissions:nil];
[_uut requestPermissions:@{}];
[_notificationCenter verify];
}

- (void)testRequestPermissions_withProvidesAppNotificationSettings {
UNAuthorizationOptions authOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionProvidesAppNotificationSettings);
UNNotificationSettings* settings = [UNNotificationSettings new];
[settings setValue:@(UNAuthorizationStatusAuthorized) forKey:@"authorizationStatus"];

[[_notificationCenter expect] requestAuthorizationWithOptions:authOptions completionHandler:[OCMArg invokeBlockWithArgs:@(YES), [NSNull null], nil]];
[[_notificationCenter expect] getNotificationSettingsWithCompletionHandler:[OCMArg invokeBlockWithArgs:settings, nil]];
[[(id)[UIApplication sharedApplication] expect] registerForRemoteNotifications];
- (void)testRequestPermissions_userAuthorizedPermissionsExtraOptions {
UNAuthorizationOptions authOptions = (UNAuthorizationOptionBadge |
UNAuthorizationOptionSound |
UNAuthorizationOptionAlert |
UNAuthorizationOptionAnnouncement |
UNAuthorizationOptionProvidesAppNotificationSettings |
UNAuthorizationOptionCriticalAlert |
UNAuthorizationOptionProvisional |
UNAuthorizationOptionCarPlay);

[_uut requestPermissions:@[@"ProvidesAppNotificationSettings"]];
[_notificationCenter verify];
}

- (void)testRequestPermissions_withProvisional {
UNAuthorizationOptions authOptions = (UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionProvisional);
UNNotificationSettings* settings = [UNNotificationSettings new];
[settings setValue:@(UNAuthorizationStatusAuthorized) forKey:@"authorizationStatus"];

[[_notificationCenter expect] requestAuthorizationWithOptions:authOptions completionHandler:[OCMArg invokeBlockWithArgs:@(YES), [NSNull null], nil]];
[[_notificationCenter expect] getNotificationSettingsWithCompletionHandler:[OCMArg invokeBlockWithArgs:settings, nil]];
[[(id)[UIApplication sharedApplication] expect] registerForRemoteNotifications];

[_uut requestPermissions:@[@"Provisional"]];
[_uut requestPermissions:@{@"carPlay": @YES,
@"criticalAlert": @YES,
@"providesAppNotificationSettings": @YES,
@"announcement": @YES,
@"provisional": @YES}];
[_notificationCenter verify];
}

Expand All @@ -73,7 +72,7 @@ - (void)testRequestPermissions_userDeniedPermissions {
[[_notificationCenter expect] getNotificationSettingsWithCompletionHandler:[OCMArg invokeBlockWithArgs:settings, nil]];
[[(id)[UIApplication sharedApplication] reject] registerForRemoteNotifications];

[_uut requestPermissions:nil];
[_uut requestPermissions:@{}];
[_notificationCenter verify];
}

Expand Down
5 changes: 3 additions & 2 deletions lib/src/Notifications.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NativeCommandsSender, RequestPermissionsOptions } from './adapters/NativeCommandsSender';
import { NativeCommandsSender } from './adapters/NativeCommandsSender';
import { NativeEventsReceiver } from './adapters/NativeEventsReceiver';
import { Commands } from './commands/Commands';
import { EventsRegistry } from './events/EventsRegistry';
Expand All @@ -11,6 +11,7 @@ import { NotificationChannel } from './interfaces/NotificationChannel';
import { NotificationsIOS } from './NotificationsIOS';
import { NotificationsAndroid } from './NotificationsAndroid';
import { NotificationFactory } from './DTO/NotificationFactory';
import { NotificationPermissionOptions } from './interfaces/NotificationPermissions';

export class NotificationsRoot {
public readonly _ios: NotificationsIOS;
Expand Down Expand Up @@ -46,7 +47,7 @@ export class NotificationsRoot {
/**
* registerRemoteNotifications
*/
public registerRemoteNotifications(options?: RequestPermissionsOptions[]) {
public registerRemoteNotifications(options?: NotificationPermissionOptions) {
this.ios.registerRemoteNotifications(options);
this.android.registerRemoteNotifications();
}
Expand Down
5 changes: 2 additions & 3 deletions lib/src/NotificationsIOS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { Notification } from './DTO/Notification';
import { Commands } from './commands/Commands';
import { Platform } from 'react-native';
import { EventsRegistryIOS } from './events/EventsRegistryIOS';
import { RequestPermissionsOptions } from './adapters/NativeCommandsSender';

import { NotificationPermissionOptions } from './interfaces/NotificationPermissions';
export class NotificationsIOS {
constructor(private readonly commands: Commands, private readonly eventsRegistry: EventsRegistryIOS) {
return new Proxy(this, {
Expand All @@ -20,7 +19,7 @@ export class NotificationsIOS {
/**
* Request permissions to send remote notifications
*/
public registerRemoteNotifications(options?: RequestPermissionsOptions[]) {
public registerRemoteNotifications(options?: NotificationPermissionOptions) {
return this.commands.requestPermissions(options);
}

Expand Down
11 changes: 5 additions & 6 deletions lib/src/adapters/NativeCommandsSender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { NotificationCompletion } from '../interfaces/NotificationCompletion';
import { NotificationPermissions } from '../interfaces/NotificationPermissions';
import { NotificationCategory } from '../interfaces/NotificationCategory';
import { NotificationChannel } from '../interfaces/NotificationChannel';
import { NotificationPermissionOptions } from '../interfaces/NotificationPermissions';

interface NativeCommandsModule {
getInitialNotification(): Promise<Object>;
postLocalNotification(notification: Notification, id: number): void;
requestPermissions(options?: RequestPermissionsOptions[]): void;
requestPermissions(options: NotificationPermissionOptions): void;
abandonPermissions(): void;
refreshToken(): void;
registerPushKit(): void;
Expand All @@ -28,8 +29,6 @@ interface NativeCommandsModule {
finishHandlingBackgroundAction(notificationId: string, backgroundFetchResult: string): void;
}

export type RequestPermissionsOptions = 'ProvidesAppNotificationSettings' | 'Provisional';

export class NativeCommandsSender {
private readonly nativeCommandsModule: NativeCommandsModule;
constructor() {
Expand All @@ -43,9 +42,9 @@ export class NativeCommandsSender {
getInitialNotification(): Promise<Object> {
return this.nativeCommandsModule.getInitialNotification();
}
requestPermissions(options?: RequestPermissionsOptions[]) {
return this.nativeCommandsModule.requestPermissions(options);

requestPermissions(options?: NotificationPermissionOptions) {
return this.nativeCommandsModule.requestPermissions(options || {});
}

abandonPermissions() {
Expand Down
22 changes: 19 additions & 3 deletions lib/src/commands/Commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,15 @@ describe('Commands', () => {

describe('requestPermissions', () => {
it('sends to native', () => {
uut.requestPermissions();
verify(mockedNativeCommandsSender.requestPermissions(undefined)).called();
const opts = {};
uut.requestPermissions(opts);
verify(mockedNativeCommandsSender.requestPermissions(opts)).called();
});

it('sends to native with options', () => {
const opts = { criticalAlert: true };
uut.requestPermissions(opts);
verify(mockedNativeCommandsSender.requestPermissions(opts)).called();
});
});

Expand Down Expand Up @@ -186,7 +193,16 @@ describe('Commands', () => {
});

it('return negative response from native', async () => {
const expectedPermissions: NotificationPermissions = {badge: false, alert: true, sound: false};
const expectedPermissions: NotificationPermissions = {
badge: false,
alert: true,
sound: false,
carPlay: false,
criticalAlert: false,
providesAppNotificationSettings: false,
provisional: false,
announcement: false,
};
when(mockedNativeCommandsSender.checkPermissions()).thenResolve(
expectedPermissions
);
Expand Down
5 changes: 3 additions & 2 deletions lib/src/commands/Commands.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { NativeCommandsSender, RequestPermissionsOptions } from '../adapters/NativeCommandsSender';
import { NativeCommandsSender } from '../adapters/NativeCommandsSender';
import { Notification } from '../DTO/Notification';
import { NotificationCategory } from '../interfaces/NotificationCategory';
import { NotificationChannel } from '../interfaces/NotificationChannel';
import { NotificationPermissions } from '../interfaces/NotificationPermissions';
import { UniqueIdProvider } from '../adapters/UniqueIdProvider';
import { NotificationFactory } from '../DTO/NotificationFactory';
import { NotificationPermissionOptions } from '../interfaces/NotificationPermissions';

export class Commands {
constructor(
Expand All @@ -29,7 +30,7 @@ export class Commands {
});
}

public requestPermissions(options?: RequestPermissionsOptions[]) {
public requestPermissions(options?: NotificationPermissionOptions) {
const result = this.nativeCommandsSender.requestPermissions(options);
return result;
}
Expand Down
9 changes: 8 additions & 1 deletion lib/src/interfaces/NotificationPermissions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
export interface NotificationPermissions {
export interface NotificationPermissionOptions {
carPlay?: boolean;
criticalAlert?: boolean;
providesAppNotificationSettings?: boolean;
provisional?: boolean;
announcement?: boolean;
}
export interface NotificationPermissions extends NotificationPermissionOptions {
badge: boolean;
alert: boolean;
sound: boolean;
Expand Down
4 changes: 2 additions & 2 deletions website/docs/api/general-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ title: General Commands
sidebar_label: General
---

## registerRemoteNotifications()
Requests remote notification permissions, prompting the user's dialog box on iOS and request a token on Android.
## registerRemoteNotifications(options?)
Requests remote notification permissions, prompting the user's dialog box on iOS and request a token on Android. See iOS specific [`registerRemoteNotifications`](ios-api.md) for definition of `options`.
If the user accept the remote notifications permissions, `registerRemoteNotificationsRegistered` event will get called with the device token.

```js
Expand Down
21 changes: 13 additions & 8 deletions website/docs/api/ios-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@ title: iOS Specific Commands
sidebar_label: iOS specific
---

## registerRemoteNotifications(options?: string[])
## registerRemoteNotifications(options?: NotificationPermissionOptions)
Requests notification permissions from iOS, prompting the user's dialog box if needed.

Available options:
- **`ProvidesAppNotificationSettings`** - An option indicating the iOS notification settings to display a button for in-app notification settings and to be [informed in the app on this event](ios-events.md#appNotificationSettingsLinked).
- **`Provisional`** - Use provisional authorization to send notifications on a trial basis. Users can then evaluate the notifications and decide whether to authorize them.
Notification permission options:
- **`providesAppNotificationSettings`** - An option indicating the iOS notification settings to display a button for in-app notification settings and to be [informed in the app on this event](ios-events.md#appNotificationSettingsLinked).
- **`provisional`** - Use provisional authorization to send notifications on a trial basis. Users can then evaluate the notifications and decide whether to authorize them.
- **`carPlay`** - The ability to display notifications in a CarPlay environment.
- **`criticalAlert`** - Requests notification permissions from iOS, prompting the user's dialog box. Options may request
`criticalAlert` but you must first [Request a Critical Alert Notifications Entitlement](https://developer.apple.com/contact/request/notifications-critical-alerts-entitlement/).


```js
Notifications.ios.registerRemoteNotifications([
'ProvidesAppNotificationSettings',
'Provisional',
]);
Notifications.ios.registerRemoteNotifications({
providesAppNotificationSettings: true,
provisional: true,
carPlay: true,
criticalAlert: true,
});
```

## checkPermissions()
Expand Down
Loading

0 comments on commit 48a95d5

Please sign in to comment.