From 27da4aa238c12bd04d66795a64e2a7855719cc0d Mon Sep 17 00:00:00 2001 From: yogevbd Date: Wed, 2 Oct 2019 16:23:21 +0300 Subject: [PATCH] Split specific iOS events, Update docs --- docs/advanced-ios.md | 165 +++++++++++++++++++++ docs/general-api.md | 2 +- docs/general-events.md | 4 +- docs/ios-events.md | 6 +- docs/notification-object.md | 18 +++ e2e/Notifications.test.js | 4 +- example/index.js | 15 +- lib/src/Notifications.ts | 5 +- lib/src/NotificationsIOS.ts | 10 +- lib/src/events/EventsRegistry.test.ts | 14 -- lib/src/events/EventsRegistry.ts | 10 -- lib/src/events/EventsRegistryIOS.test.ts | 25 ++++ lib/src/events/EventsRegistryIOS.ts | 19 +++ lib/src/index.ts | 1 + lib/src/interfaces/NotificationCategory.ts | 20 ++- website/i18n/en.json | 24 ++- website/sidebars.json | 6 +- 17 files changed, 287 insertions(+), 61 deletions(-) create mode 100644 docs/advanced-ios.md create mode 100755 docs/notification-object.md create mode 100644 lib/src/events/EventsRegistryIOS.test.ts create mode 100644 lib/src/events/EventsRegistryIOS.ts diff --git a/docs/advanced-ios.md b/docs/advanced-ios.md new file mode 100644 index 0000000..4dd22ba --- /dev/null +++ b/docs/advanced-ios.md @@ -0,0 +1,165 @@ +--- +id: advanced-ios +title: iOS Advanced API +sidebar_label: iOS +--- + +## PushKit API + +The PushKit framework provides the classes for your iOS apps to receive background pushes from remote servers. it has better support for background notifications compared to regular push notifications with `content-available: 1`. More info in [iOS PushKit documentation](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Reference/PushKit_Framework/). + +### Register to PushKit +[Prepare your app to receive VoIP push notifications](https://developer.apple.com/library/ios/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html) + +### Listen to PushKit notifications +On receiving PushKit notification, a `pushKitNotificationReceived` event will be fired with the notification payload. + +```js +Notifications.ios.events().registerPushKitNotificationReceived((payload: object) => { + console.log(JSON.stringify(payload)); +}); +``` + +In your ReactNative code, add event handler for `pushKitRegistered` event and call to `registerPushKit()`: + +```javascript +constructor() { + Notifications.ios.events().registerPushKitRegistered((event: RegisteredPushKit) => { + console.log("PushKit Token Received: " + event.pushKitToken); + }); + + Notifications.ios.events().registerPushKitNotificationReceived((payload: object) => { + console.log('PushKit notification Received: ' + JSON.stringify(payload)); + }); + + Notifications.ios.registerPushKit(); +} +``` + +> 1. Notice that PushKit device token and regular notifications device token are different, so you must handle two different tokens in the server side in order to support this feature. +> 2. PushKit will not request permissions from the user for push notifications. + + +--- + +## Interactive / Actionable Notifications + +> This section provides description for iOS. For notifications customization on Android, refer to [our wiki](https://github.com/wix/react-native-notifications/wiki/Android-Customizations#customizing-notifications-layout). + +Interactive notifications allow you to reply to a message right from the notification banner or take action right from the lock screen. + +On the Lock screen and within Notification Center, you swipe from right to left +to reveal actions. Destructive actions, like trashing an email, are color-coded red. Relatively neutral actions, like dismissing an alert or declining an invitation, are color-coded gray. + +For banners, you pull down to reveal actions as buttons. For popups, the actions are immediately visible — the buttons are right there. + +You can find more info about interactive notifications [here](http://www.imore.com/interactive-notifications-ios-8-explained). + +![Interactive Notifications](http://i.imgur.com/XrVzy9w.gif) + + +Notification **actions** allow the user to interact with a given notification. + +Notification **categories** allow you to group multiple actions together, and to connect the actions with the push notification itself. + +Follow the basic workflow of adding interactive notifications to your app: + +1. Config the actions. +2. Group actions together into categories. +3. Register to push notifications with the configured categories. +4. Push a notification (or trigger a [local](#triggering-local-notifications) one) with the configured category name. + +### Example +#### Config the Actions +We will config two actions: upvote and reply. + +```javascript +import { Notifications, NotificationAction, NotificationCategory } from 'react-native-notifications'; + +let upvoteAction = new NotificationAction({ + activationMode: "background", + title: String.fromCodePoint(0x1F44D), + identifier: "UPVOTE_ACTION", + textInput: { + buttonTitle: 'title', + placeholder: 'placeholder text' + } +}); + +let replyAction = new NotificationAction({ + activationMode: "background", + title: "Reply", + authenticationRequired: true, + identifier: "REPLY_ACTION" +}); + +``` + +#### Config the Category +We will group `upvote` action and `reply` action into a single category: `EXAMPLE_CATEGORY `. If the notification contains `EXAMPLE_CATEGORY ` under `category` field, those actions will appear. + +```javascript +let exampleCategory = new NotificationCategory({ + identifier: "EXAMPLE_CATEGORY", + actions: [upvoteAction, replyAction] +}); +``` + +#### Register to Push Notifications +Instead of basic registration like we've done before, we will register the device to push notifications with the category we've just created. + +```javascript +Notifications.setCategories([exampleCategory]); +``` + +#### Push an Interactive Notification +Notification payload should look like this: + +```javascript +{ + aps: { + // ... (alert, sound, badge, etc) + category: "EXAMPLE_CATEGORY" + } +} +``` + +The [example app](https://github.com/wix/react-native-notifications/tree/master/example) contains this interactive notification example, you can follow there. + +### `NotificationAction` Payload + +- `title` - Action button title. +- `identifier` - Action identifier (must be unique). +- `activationMode` - Indicating whether the app should activate to the foreground or background. + - `foreground` (default) - Activate the app and put it in the foreground. + - `background` - Activate the app and put it in the background. If the app is already in the foreground, it remains in the foreground. +- `textInput` - `TextInput` payload, when supplied, the system will present text input in this action. +- `destructive` - A Boolean value indicating whether the action is destructive. When the value of this property is `true`, the system displays the corresponding button differently to indicate that the action is destructive. +- `authenticationRequired` - A Boolean value indicating whether the user must unlock the device before the action is performed. + +### `NotificationCategory` Payload + +- `identifier` - The name of the action group (must be unique). +- `actions` - An array of `NotificationAction` objects, which related to this category. + +### `TextInput` Payload + +- `buttonTitle` - Title of the `send` button. +- `placeholder` - Placeholder for the `textInput`. + + +#### Get and set application icon badges count (iOS only) + +Get the current number: +```javascript +Notifications.ios.getBadgesCount((count) => console.log(count)); +``` + +Set to specific number: +```javascript +Notifications.ios.setBadgesCount(2); +``` +Clear badges icon: +```javascript +Notifications.ios.setBadgesCount(0); +``` diff --git a/docs/general-api.md b/docs/general-api.md index 06b787a..851b213 100755 --- a/docs/general-api.md +++ b/docs/general-api.md @@ -13,7 +13,7 @@ Notifications.registerRemoteNotifications(); ``` ## getInitialNotification() -This method returns a promise. If the app was launched by a push notification, this promise resolves to an object of type Notification. Otherwise, it resolves to undefined. +This method returns a promise. If the app was launched by a push notification, this promise resolves to an object of type [Notification](http://localhost:3000/react-native-notifications/docs/notification-object). Otherwise, it resolves to undefined. ```js const notification: Notification = await Notifications.getInitialNotification(); diff --git a/docs/general-events.md b/docs/general-events.md index fbb1f27..14ffe54 100755 --- a/docs/general-events.md +++ b/docs/general-events.md @@ -14,7 +14,7 @@ Notifications.events().registerRemoteNotificationsRegistered((event: Registered) ``` ## registerNotificationReceived() -Fired when a remote notification is received in foreground state. The handler will be invoked with an instance of `Notification`. +Fired when a remote notification is received in foreground state. The handler will be invoked with an instance of [Notification](http://localhost:3000/react-native-notifications/docs/notification-object). Should call completion function on iOS, will be ignored on Android. ```js @@ -27,7 +27,7 @@ Notifications.events().registerNotificationReceived((notification: Notification, ``` ## registerRemoteNotificationOpened() -Fired when a remote notification is opened from dead or background state. The handler will be invoked with an instance of `Notification`. +Fired when a remote notification is opened from dead or background state. The handler will be invoked with an instance of [Notification](http://localhost:3000/react-native-notifications/docs/notification-object). Should call completion function on iOS, will be ignored on Android. ```js diff --git a/docs/ios-events.md b/docs/ios-events.md index 603bd7d..8a93919 100755 --- a/docs/ios-events.md +++ b/docs/ios-events.md @@ -8,7 +8,7 @@ sidebar_label: iOS specific Fired when the user registers for PushKit notifications. The handler will be invoked with an event holding the hex string representing the `pushKitToken` ```js -Notifications.events().registerPushKitRegistered((event: RegisteredPushKit) => { +Notifications.ios.events().registerPushKitRegistered((event: RegisteredPushKit) => { console.log(event.pushKitToken); }); ``` @@ -17,8 +17,8 @@ Notifications.events().registerPushKitRegistered((event: RegisteredPushKit) => { Fired when a PushKit notification is received. The handler will be invoked with the notification object. ```js -Notifications.events().registerPushKitNotificationReceived((event: object) => { - console.log(JSON.stringify(event)); +Notifications.ios.events().registerPushKitNotificationReceived((payload: object) => { + console.log(JSON.stringify(payload)); }); ``` diff --git a/docs/notification-object.md b/docs/notification-object.md new file mode 100755 index 0000000..7628e55 --- /dev/null +++ b/docs/notification-object.md @@ -0,0 +1,18 @@ +--- +id: notification-object +title: Notification object +sidebar_label: Notification +--- + +Contains the payload data. + + +Example: +```js +Notifications.events().registerNotificationReceived((notification: Notification, completion: (response: NotificationCompletion) => void) => { + // Prints the notification payload + console.log(JSON.stringify(notification.data)); + + completion({alert: false, sound: false, badge: false}); +}); +``` \ No newline at end of file diff --git a/e2e/Notifications.test.js b/e2e/Notifications.test.js index cc954c1..0988140 100644 --- a/e2e/Notifications.test.js +++ b/e2e/Notifications.test.js @@ -28,19 +28,17 @@ describe('Notifications', () => { }); describe('Dead state', () => { - it.only('Should receive notification', async () => { + it('Should receive notification', async () => { await device.launchApp({newInstance: true, userNotification: createNotification({link: 'deadState/notification'})}); await linkShouldBeVisible('deadState/notification'); }); }); - async function linkShouldBeVisible(link) { return await expect(elementByLabel(`Extra Link Param: ${link}`)).toBeVisible(); } }); - function createNotification({link, showAlert}) { return { trigger: { diff --git a/example/index.js b/example/index.js index 4dbd7d0..b0bbf36 100644 --- a/example/index.js +++ b/example/index.js @@ -6,7 +6,7 @@ import { Button } from 'react-native'; import React, {Component} from 'react'; -import {Notifications} from 'react-native-notifications'; +import {Notifications, NotificationAction, NotificationCategory} from 'react-native-notifications'; class NotificationsExampleApp extends Component { constructor() { @@ -43,13 +43,13 @@ class NotificationsExampleApp extends Component { } setCategories() { - const upvoteAction = { + const upvoteAction = new NotificationAction({ activationMode: 'background', title: String.fromCodePoint(0x1F44D), identifier: 'UPVOTE_ACTION' - }; + }); - const replyAction = { + const replyAction = new NotificationAction({ activationMode: 'background', title: 'Reply', authenticationRequired: true, @@ -58,12 +58,13 @@ class NotificationsExampleApp extends Component { placeholder: 'Insert message' }, identifier: 'REPLY_ACTION' - }; + }); - const category = { + + const category = new NotificationCategory({ identifier: 'SOME_CATEGORY', actions: [upvoteAction, replyAction] - }; + }); Notifications.setCategories([category]); } diff --git a/lib/src/Notifications.ts b/lib/src/Notifications.ts index 58e9f85..ca0016c 100644 --- a/lib/src/Notifications.ts +++ b/lib/src/Notifications.ts @@ -2,6 +2,7 @@ import { NativeCommandsSender } from './adapters/NativeCommandsSender'; import { NativeEventsReceiver } from './adapters/NativeEventsReceiver'; import { Commands } from './commands/Commands'; import { EventsRegistry } from './events/EventsRegistry'; +import { EventsRegistryIOS } from './events/EventsRegistryIOS'; import { Notification } from './DTO/Notification'; import { UniqueIdProvider } from './adapters/UniqueIdProvider'; import { CompletionCallbackWrapper } from './adapters/CompletionCallbackWrapper'; @@ -17,6 +18,7 @@ export class NotificationsRoot { private readonly nativeCommandsSender: NativeCommandsSender; private readonly commands: Commands; private readonly eventsRegistry: EventsRegistry; + private readonly eventsRegistryIOS: EventsRegistryIOS; private readonly uniqueIdProvider: UniqueIdProvider; private readonly completionCallbackWrapper: CompletionCallbackWrapper; @@ -30,8 +32,9 @@ export class NotificationsRoot { this.uniqueIdProvider ); this.eventsRegistry = new EventsRegistry(this.nativeEventsReceiver, this.completionCallbackWrapper); + this.eventsRegistryIOS = new EventsRegistryIOS(this.nativeEventsReceiver); - this.ios = new NotificationsIOS(this.commands); + this.ios = new NotificationsIOS(this.commands, this.eventsRegistryIOS); this.android = new NotificationsAndroid(this.commands); } diff --git a/lib/src/NotificationsIOS.ts b/lib/src/NotificationsIOS.ts index 73f1e1d..2d713b6 100644 --- a/lib/src/NotificationsIOS.ts +++ b/lib/src/NotificationsIOS.ts @@ -1,9 +1,10 @@ import { Notification } from './DTO/Notification'; import { Commands } from './commands/Commands'; import { Platform } from 'react-native'; +import { EventsRegistryIOS } from './events/EventsRegistryIOS'; export class NotificationsIOS { - constructor(private readonly commands: Commands) { + constructor(private readonly commands: Commands, private readonly eventsRegistry: EventsRegistryIOS) { return new Proxy(this, { get(target, name) { if (Platform.OS === 'ios') { @@ -86,4 +87,11 @@ export class NotificationsIOS { public getDeliveredNotifications(): Array { return this.commands.getDeliveredNotifications(); } + + /** + * Obtain the events registry instance + */ + public events(): EventsRegistryIOS { + return this.eventsRegistry; + } } diff --git a/lib/src/events/EventsRegistry.test.ts b/lib/src/events/EventsRegistry.test.ts index 3b864a4..77bf7c3 100644 --- a/lib/src/events/EventsRegistry.test.ts +++ b/lib/src/events/EventsRegistry.test.ts @@ -146,20 +146,6 @@ describe('EventsRegistry', () => { expect(mockNativeEventsReceiver.registerRemoteNotificationsRegistered).toHaveBeenCalledWith(cb); }); - it('delegates registerPushKitRegistered to nativeEventsReceiver', () => { - const cb = jest.fn(); - uut.registerPushKitRegistered(cb); - expect(mockNativeEventsReceiver.registerPushKitRegistered).toHaveBeenCalledTimes(1); - expect(mockNativeEventsReceiver.registerPushKitRegistered).toHaveBeenCalledWith(cb); - }); - - it('delegates registerPushKitNotificationReceived to nativeEventsReceiver', () => { - const cb = jest.fn(); - uut.registerPushKitNotificationReceived(cb); - expect(mockNativeEventsReceiver.registerPushKitNotificationReceived).toHaveBeenCalledTimes(1); - expect(mockNativeEventsReceiver.registerPushKitNotificationReceived).toHaveBeenCalledWith(cb); - }); - it('delegates registerRemoteNotificationsRegistrationFailed to nativeEventsReceiver', () => { const cb = jest.fn(); uut.registerRemoteNotificationsRegistrationFailed(cb); diff --git a/lib/src/events/EventsRegistry.ts b/lib/src/events/EventsRegistry.ts index 8fc076c..7bbe9bf 100644 --- a/lib/src/events/EventsRegistry.ts +++ b/lib/src/events/EventsRegistry.ts @@ -3,7 +3,6 @@ import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver'; import { Registered, RegistrationError, - RegisteredPushKit, NotificationResponse } from '../interfaces/NotificationEvents'; import { CompletionCallbackWrapper } from '../adapters/CompletionCallbackWrapper'; @@ -19,18 +18,10 @@ export class EventsRegistry { public registerRemoteNotificationsRegistered(callback: (event: Registered) => void): EmitterSubscription { return this.nativeEventsReceiver.registerRemoteNotificationsRegistered(callback); } - - public registerPushKitRegistered(callback: (event: RegisteredPushKit) => void): EmitterSubscription { - return this.nativeEventsReceiver.registerPushKitRegistered(callback); - } public registerNotificationReceived(callback: (notification: Notification, completion: (response: NotificationCompletion) => void) => void): EmitterSubscription { return this.nativeEventsReceiver.registerRemoteNotificationReceived(this.completionCallbackWrapper.wrapReceivedCallback(callback)); } - - public registerPushKitNotificationReceived(callback: (event: object) => void): EmitterSubscription { - return this.nativeEventsReceiver.registerPushKitNotificationReceived(callback); - } public registerRemoteNotificationOpened(callback: (response: NotificationResponse, completion: () => void) => void): EmitterSubscription { return this.nativeEventsReceiver.registerRemoteNotificationOpened(this.completionCallbackWrapper.wrapOpenedCallback(callback)); @@ -39,5 +30,4 @@ export class EventsRegistry { public registerRemoteNotificationsRegistrationFailed(callback: (event: RegistrationError) => void): EmitterSubscription { return this.nativeEventsReceiver.registerRemoteNotificationsRegistrationFailed(callback); } - } diff --git a/lib/src/events/EventsRegistryIOS.test.ts b/lib/src/events/EventsRegistryIOS.test.ts new file mode 100644 index 0000000..51be841 --- /dev/null +++ b/lib/src/events/EventsRegistryIOS.test.ts @@ -0,0 +1,25 @@ +import { EventsRegistryIOS } from './EventsRegistryIOS'; +import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver.mock'; + +describe('EventsRegistryIOS', () => { + let uut: EventsRegistryIOS; + const mockNativeEventsReceiver = new NativeEventsReceiver(); + + beforeEach(() => { + uut = new EventsRegistryIOS(mockNativeEventsReceiver); + }); + + it('delegates registerPushKitRegistered to nativeEventsReceiver', () => { + const cb = jest.fn(); + uut.registerPushKitRegistered(cb); + expect(mockNativeEventsReceiver.registerPushKitRegistered).toHaveBeenCalledTimes(1); + expect(mockNativeEventsReceiver.registerPushKitRegistered).toHaveBeenCalledWith(cb); + }); + + it('delegates registerPushKitNotificationReceived to nativeEventsReceiver', () => { + const cb = jest.fn(); + uut.registerPushKitNotificationReceived(cb); + expect(mockNativeEventsReceiver.registerPushKitNotificationReceived).toHaveBeenCalledTimes(1); + expect(mockNativeEventsReceiver.registerPushKitNotificationReceived).toHaveBeenCalledWith(cb); + }); +}); diff --git a/lib/src/events/EventsRegistryIOS.ts b/lib/src/events/EventsRegistryIOS.ts new file mode 100644 index 0000000..4ea49fe --- /dev/null +++ b/lib/src/events/EventsRegistryIOS.ts @@ -0,0 +1,19 @@ +import { EmitterSubscription } from 'react-native'; +import { NativeEventsReceiver } from '../adapters/NativeEventsReceiver'; +import { + RegisteredPushKit +} from '../interfaces/NotificationEvents'; + +export class EventsRegistryIOS { + constructor( + private nativeEventsReceiver: NativeEventsReceiver) + {} + + public registerPushKitRegistered(callback: (event: RegisteredPushKit) => void): EmitterSubscription { + return this.nativeEventsReceiver.registerPushKitRegistered(callback); + } + + public registerPushKitNotificationReceived(callback: (event: object) => void): EmitterSubscription { + return this.nativeEventsReceiver.registerPushKitNotificationReceived(callback); + } +} diff --git a/lib/src/index.ts b/lib/src/index.ts index c6964f5..583fe0c 100644 --- a/lib/src/index.ts +++ b/lib/src/index.ts @@ -6,3 +6,4 @@ export const Notifications = notificationsSingleton; export * from './interfaces/EventSubscription'; export * from './DTO/Notification'; export * from './interfaces/NotificationEvents'; +export * from './interfaces/NotificationCategory'; diff --git a/lib/src/interfaces/NotificationCategory.ts b/lib/src/interfaces/NotificationCategory.ts index 69a8497..e9356d1 100644 --- a/lib/src/interfaces/NotificationCategory.ts +++ b/lib/src/interfaces/NotificationCategory.ts @@ -1,18 +1,30 @@ -export interface NotificationCategory { +export class NotificationCategory { identifier: string actions: [NotificationAction?]; -} + constructor(identifier: string, actions: [NotificationAction?]) { + this.identifier = identifier; + this.actions = actions; + } +} export interface NotificationTextInput { buttonTitle: string; placeholder: string; } -export interface NotificationAction { +export class NotificationAction { identifier: string; activationMode: 'foreground' | 'authenticationRequired' | 'destructive'; title: string; authenticationRequired: boolean; - textInput: NotificationTextInput + textInput: NotificationTextInput; + + constructor(identifier: string, activationMode: 'foreground' | 'authenticationRequired' | 'destructive', title: string, authenticationRequired: boolean, textInput: NotificationTextInput) { + this.identifier = identifier; + this.activationMode = activationMode; + this.title = title; + this.authenticationRequired = authenticationRequired; + this.textInput = textInput; + } } \ No newline at end of file diff --git a/website/i18n/en.json b/website/i18n/en.json index 5ff47e5..6972d71 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -5,22 +5,14 @@ "previous": "Previous", "tagline": "Documentation", "docs": { + "advanced-ios": { + "title": "iOS Advanced API", + "sidebar_label": "iOS" + }, "android-api": { "title": "Android Specific Commands", "sidebar_label": "Android specific" }, - "doc2": { - "title": "document number 2" - }, - "doc3": { - "title": "This is document number 3" - }, - "doc4": { - "title": "Other Document" - }, - "doc5": { - "title": "Fifth Document" - }, "general-api": { "title": "General Commands", "sidebar_label": "General" @@ -44,6 +36,10 @@ "ios-events": { "title": "iOS", "sidebar_label": "iOS specific" + }, + "notification-object": { + "title": "Notification object", + "sidebar_label": "Notification" } }, "links": { @@ -52,8 +48,10 @@ }, "categories": { "Installation": "Installation", + "Advanced": "Advanced", "Commands": "Commands", - "Events": "Events" + "Events": "Events", + "Objects": "Objects" } }, "pages-strings": { diff --git a/website/sidebars.json b/website/sidebars.json index a12d5d4..ca0e0f4 100755 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -1,9 +1,11 @@ { "docs": { - "Installation": ["installation-ios", "installation-android"] + "Installation": ["installation-ios", "installation-android"], + "Advanced": ["advanced-ios"] }, "api": { "Commands": ["general-api", "ios-api", "android-api"], - "Events": ["general-events", "ios-events"] + "Events": ["general-events", "ios-events"], + "Objects": ["notification-object"] } } -- 2.26.2