From fb57dc54822455a71b4e060284b5bb404c400c25 Mon Sep 17 00:00:00 2001 From: Chris Bianca Date: Fri, 9 Feb 2018 17:00:03 +0000 Subject: [PATCH] [notifications] First pass at notifications JS API --- .../RNFirebaseNotifications.java | 25 + .../RNFirebaseNotificationsPackage.java | 37 ++ .../notifications/AndroidNotification.js | 546 ++++++++++++++++++ lib/modules/notifications/IOSNotification.js | 159 +++++ lib/modules/notifications/Notification.js | 120 ++++ lib/modules/notifications/index.js | 125 ++-- 6 files changed, 971 insertions(+), 41 deletions(-) create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java create mode 100644 android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java create mode 100644 lib/modules/notifications/AndroidNotification.js create mode 100644 lib/modules/notifications/IOSNotification.js create mode 100644 lib/modules/notifications/Notification.js diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java new file mode 100644 index 00000000..5aee7f17 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotifications.java @@ -0,0 +1,25 @@ +package io.invertase.firebase.notifications; + +import android.support.v4.app.NotificationCompat; + +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class RNFirebaseNotifications extends ReactContextBaseJavaModule { + public RNFirebaseNotifications(ReactApplicationContext context) { + super(context); + } + + @Override + public String getName() { + return "RNFirebaseNotifications"; + } + + @ReactMethod + public void sendNotification(Promise promise) { + // + NotificationCompat.Builder builder = new NotificationCompat.Builder() + } +} diff --git a/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java new file mode 100644 index 00000000..74d4de23 --- /dev/null +++ b/android/src/main/java/io/invertase/firebase/notifications/RNFirebaseNotificationsPackage.java @@ -0,0 +1,37 @@ +package io.invertase.firebase.notifications; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RNFirebaseNotificationsPackage implements ReactPackage { + public RNFirebaseNotificationsPackage() { + } + + /** + * @param reactContext react application context that can be used to create modules + * @return list of native modules to register with the newly created catalyst instance + */ + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + modules.add(new RNFirebaseNotifications(reactContext)); + + return modules; + } + + /** + * @param reactContext + * @return a list of view managers that should be registered with {@link UIManagerModule} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} diff --git a/lib/modules/notifications/AndroidNotification.js b/lib/modules/notifications/AndroidNotification.js new file mode 100644 index 00000000..12ed4331 --- /dev/null +++ b/lib/modules/notifications/AndroidNotification.js @@ -0,0 +1,546 @@ +/** + * @flow + * AndroidNotification representation wrapper + */ +import type Notification from './Notification'; + +type Lights = { + argb: number, + onMs: number, + offMs: number, +}; + +type Progress = { + max: number, + progress: number, + indeterminate: boolean, +}; + +type SmallIcon = { + icon: number, + level?: number, +}; + +export type NativeAndroidNotification = { + // TODO actions: Action[], + autoCancel: boolean, + badgeIconType: BadgeIconTypeType, + category: CategoryType, + channelId: string, + color: number, + colorized: boolean, + contentInfo: string, + defaults: DefaultsType[], + group: string, + groupAlertBehaviour: GroupAlertType, + groupSummary: boolean, + largeIcon: string, + lights: Lights, + localOnly: boolean, + number: number, + ongoing: boolean, + onlyAlertOnce: boolean, + people: string[], + priority: PriorityType, + progress: Progress, + publicVersion: Notification, + remoteInputHistory: string[], + shortcutId: string, + showWhen: boolean, + smallIcon: SmallIcon, + sortKey: string, + // TODO: style: Style, + ticker: string, + timeoutAfter: number, + usesChronometer: boolean, + vibrate: number[], + visibility: VisibilityType, + when: number, +}; + +export const BadgeIconType = { + Large: 2, + None: 0, + Small: 1, +}; + +export const Category = { + Alarm: 'alarm', + Call: 'call', + Email: 'email', + Error: 'err', + Event: 'event', + Message: 'msg', + Progress: 'progress', + Promo: 'promo', + Recommendation: 'recommendation', + Reminder: 'reminder', + Service: 'service', + Social: 'social', + Status: 'status', + System: 'system', + Transport: 'transport', +}; + +export const Defaults = { + All: -1, + Lights: 4, + Sound: 1, + Vibrate: 2, +}; + +export const GroupAlert = { + All: 0, + Children: 2, + Summary: 1, +}; + +export const Priority = { + Default: 0, + High: 1, + Low: -1, + Max: 2, + Min: -2, +}; + +export const Visibility = { + Private: 0, + Public: 1, + Secret: -1, +}; + +type BadgeIconTypeType = $Values; +type CategoryType = $Values; +type DefaultsType = $Values; +type GroupAlertType = $Values; +type PriorityType = $Values; +type VisibilityType = $Values; + +export default class AndroidNotification { + // TODO actions: Action[]; // icon, title, ??pendingIntent??, allowGeneratedReplies, extender, extras, remoteinput (ugh) + _autoCancel: boolean; + _badgeIconType: BadgeIconTypeType; + _category: CategoryType; + _channelId: string; + _color: number; + _colorized: boolean; + _contentInfo: string; + _defaults: DefaultsType[]; + _group: string; + _groupAlertBehaviour: GroupAlertType; + _groupSummary: boolean; + _largeIcon: string; + _lights: Lights; + _localOnly: boolean; + _notification: Notification; + _number: number; + _ongoing: boolean; + _onlyAlertOnce: boolean; + _people: string[]; + _priority: PriorityType; + _progress: Progress; + _publicVersion: Notification; + _remoteInputHistory: string[]; + _shortcutId: string; + _showWhen: boolean; + _smallIcon: SmallIcon; + _sortKey: string; + // TODO: style: Style; // Need to figure out if this can work + _ticker: string; + _timeoutAfter: number; + _usesChronometer: boolean; + _vibrate: number[]; + _visibility: VisibilityType; + _when: number; + + // android unsupported + // content: RemoteViews + // contentIntent: PendingIntent - need to look at what this is + // customBigContentView: RemoteViews + // customContentView: RemoteViews + // customHeadsUpContentView: RemoteViews + // deleteIntent: PendingIntent + // fullScreenIntent: PendingIntent + // sound.streamType + + constructor(notification: Notification) { + this._notification = notification; + this._people = []; + } + + /** + * + * @param identifier + * @param identifier + * @param identifier + * @returns {Notification} + */ + addPerson(person: string): Notification { + this._people.push(person); + return this._notification; + } + + /** + * + * @param autoCancel + * @returns {Notification} + */ + setAutoCancel(autoCancel: boolean): Notification { + this._autoCancel = autoCancel; + return this._notification; + } + + /** + * + * @param badgeIconType + * @returns {Notification} + */ + setBadgeIconType(badgeIconType: BadgeIconTypeType): Notification { + this._badgeIconType = badgeIconType; + return this._notification; + } + + /** + * + * @param category + * @returns {Notification} + */ + setCategory(category: CategoryType): Notification { + if (!Object.values(Category).includes(category)) { + throw new Error(`AndroidNotification: Invalid Category: ${category}`); + } + this._category = category; + return this._notification; + } + + /** + * + * @param channelId + * @returns {Notification} + */ + setChannelId(channelId: string): Notification { + this._channelId = channelId; + return this._notification; + } + + /** + * + * @param color + * @returns {Notification} + */ + setColor(color: number): Notification { + this._color = color; + return this._notification; + } + + /** + * + * @param colorized + * @returns {Notification} + */ + setColorized(colorized: boolean): Notification { + this._colorized = colorized; + return this._notification; + } + + /** + * + * @param contentInfo + * @returns {Notification} + */ + setContentInfo(contentInfo: string): Notification { + this._contentInfo = contentInfo; + return this._notification; + } + + /** + * + * @param defaults + * @returns {Notification} + */ + setDefaults(defaults: DefaultsType[]): Notification { + this._defaults = defaults; + return this._notification; + } + + /** + * + * @param group + * @returns {Notification} + */ + setGroup(group: string): Notification { + this._group = group; + return this._notification; + } + + /** + * + * @param groupAlertBehaviour + * @returns {Notification} + */ + setGroupAlertBehaviour(groupAlertBehaviour: GroupAlertType): Notification { + this._groupAlertBehaviour = groupAlertBehaviour; + return this._notification; + } + + /** + * + * @param groupSummary + * @returns {Notification} + */ + setGroupSummary(groupSummary: boolean): Notification { + this._groupSummary = groupSummary; + return this._notification; + } + + /** + * + * @param largeIcon + * @returns {Notification} + */ + setLargeIcon(largeIcon: string): Notification { + this._largeIcon = largeIcon; + return this._notification; + } + + /** + * + * @param argb + * @param onMs + * @param offMs + * @returns {Notification} + */ + setLights(argb: number, onMs: number, offMs: number): Notification { + this._lights = { + argb, + onMs, + offMs, + }; + return this._notification; + } + + /** + * + * @param localOnly + * @returns {Notification} + */ + setLocalOnly(localOnly: boolean): Notification { + this._localOnly = localOnly; + return this._notification; + } + + /** + * + * @param number + * @returns {Notification} + */ + setNumber(number: number): Notification { + this._number = number; + return this._notification; + } + + /** + * + * @param ongoing + * @returns {Notification} + */ + setOngoing(ongoing: boolean): Notification { + this._ongoing = ongoing; + return this._notification; + } + + /** + * + * @param onlyAlertOnce + * @returns {Notification} + */ + setOnlyAlertOnce(onlyAlertOnce: boolean): Notification { + this._onlyAlertOnce = onlyAlertOnce; + return this._notification; + } + + /** + * + * @param priority + * @returns {Notification} + */ + setPriority(priority: PriorityType): Notification { + this._priority = priority; + return this._notification; + } + + /** + * + * @param max + * @param progress + * @param indeterminate + * @returns {Notification} + */ + setProgress( + max: number, + progress: number, + indeterminate: boolean + ): Notification { + this._progress = { + max, + progress, + indeterminate, + }; + return this._notification; + } + + /** + * + * @param publicVersion + * @returns {Notification} + */ + setPublicVersion(publicVersion: Notification): Notification { + this._publicVersion = publicVersion; + return this._notification; + } + + /** + * + * @param remoteInputHistory + * @returns {Notification} + */ + setRemoteInputHistory(remoteInputHistory: string[]): Notification { + this._remoteInputHistory = remoteInputHistory; + return this._notification; + } + + /** + * + * @param shortcutId + * @returns {Notification} + */ + setShortcutId(shortcutId: string): Notification { + this._shortcutId = shortcutId; + return this._notification; + } + + /** + * + * @param showWhen + * @returns {Notification} + */ + setShowWhen(showWhen: boolean): Notification { + this._showWhen = showWhen; + return this._notification; + } + + /** + * + * @param icon + * @param level + * @returns {Notification} + */ + setSmallIcon(icon: number, level?: number): Notification { + this._smallIcon = { + icon, + level, + }; + return this._notification; + } + + /** + * + * @param sortKey + * @returns {Notification} + */ + setSortKey(sortKey: string): Notification { + this._sortKey = sortKey; + return this._notification; + } + + /** + * + * @param ticker + * @returns {Notification} + */ + setTicker(ticker: string): Notification { + this._ticker = ticker; + return this._notification; + } + + /** + * + * @param timeoutAfter + * @returns {Notification} + */ + setTimeoutAfter(timeoutAfter: number): Notification { + this._timeoutAfter = timeoutAfter; + return this._notification; + } + + /** + * + * @param usesChronometer + * @returns {Notification} + */ + setUsesChronometer(usesChronometer: boolean): Notification { + this._usesChronometer = usesChronometer; + return this._notification; + } + + /** + * + * @param vibrate + * @returns {Notification} + */ + setVibrate(vibrate: number[]): Notification { + this._vibrate = vibrate; + return this._notification; + } + + /** + * + * @param when + * @returns {Notification} + */ + setWhen(when: number): Notification { + this._when = when; + return this._notification; + } + + build(): NativeAndroidNotification { + // TODO: Validation + + return { + // TODO actions: Action[], + autoCancel: this._autoCancel, + badgeIconType: this._badgeIconType, + category: this._category, + channelId: this._channelId, + color: this._color, + colorized: this._colorized, + contentInfo: this._contentInfo, + defaults: this._defaults, + group: this._group, + groupAlertBehaviour: this._groupAlertBehaviour, + groupSummary: this._groupSummary, + largeIcon: this._largeIcon, + lights: this._lights, + localOnly: this._localOnly, + number: this._number, + ongoing: this._ongoing, + onlyAlertOnce: this._onlyAlertOnce, + people: this._people, + priority: this._priority, + progress: this._progress, + publicVersion: this._publicVersion, + remoteInputHistory: this._remoteInputHistory, + shortcutId: this._shortcutId, + showWhen: this._showWhen, + smallIcon: this._smallIcon, + sortKey: this._sortKey, + // TODO: style: Style, + ticker: this._ticker, + timeoutAfter: this._timeoutAfter, + usesChronometer: this._usesChronometer, + vibrate: this._vibrate, + visibility: this._visibility, + when: this._when, + }; + } +} diff --git a/lib/modules/notifications/IOSNotification.js b/lib/modules/notifications/IOSNotification.js new file mode 100644 index 00000000..607518e7 --- /dev/null +++ b/lib/modules/notifications/IOSNotification.js @@ -0,0 +1,159 @@ +/** + * @flow + * IOSNotification representation wrapper + */ +import { generatePushID } from '../../utils'; +import type Notification from './Notification'; + +type AttachmentOptions = {| + TypeHint: string, + ThumbnailHidden: boolean, + ThumbnailClippingRect: { + height: number, + width: number, + x: number, + y: number, + }, + ThumbnailTime: number, +|}; + +type Attachment = {| + identifier: string, + options?: AttachmentOptions, + url: string, +|}; + +export type NativeIOSNotification = { + alertAction?: string, + attachments: Attachment[], + badge?: number, + category?: string, + hasAction?: boolean, + identifier?: string, + launchImage?: string, + threadIdentifier?: string, +}; + +export default class IOSNotification { + _alertAction: string; // alertAction | N/A + _attachments: Attachment[]; // N/A | attachments + _badge: number; // applicationIconBadgeNumber | badge + _category: string; + _hasAction: boolean; // hasAction | N/A + _identifier: string; // N/A | identifier + _launchImage: string; // alertLaunchImage | launchImageName + _notification: Notification; + _threadIdentifier: string; // N/A | threadIdentifier + + constructor(notification: Notification) { + this._attachments = []; + // TODO: Is this the best way to generate an ID? + this._identifier = generatePushID(); + this._notification = notification; + } + + /** + * + * @param identifier + * @param identifier + * @param identifier + * @returns {Notification} + */ + addAttachment( + identifier: string, + url: string, + options?: AttachmentOptions + ): Notification { + this._attachments.push({ + identifier, + options, + url, + }); + return this._notification; + } + + /** + * + * @param alertAction + * @returns {Notification} + */ + setAlertAction(alertAction: string): Notification { + this._alertAction = alertAction; + return this._notification; + } + + /** + * + * @param badge + * @returns {Notification} + */ + setBadge(badge: number): Notification { + this._badge = badge; + return this._notification; + } + + /** + * + * @param category + * @returns {Notification} + */ + setCategory(category: string): Notification { + this._category = category; + return this._notification; + } + + /** + * + * @param hasAction + * @returns {Notification} + */ + setHasAction(hasAction: boolean): Notification { + this._hasAction = hasAction; + return this._notification; + } + + /** + * + * @param identifier + * @returns {Notification} + */ + setIdentifier(identifier: string): Notification { + this._identifier = identifier; + return this._notification; + } + + /** + * + * @param launchImage + * @returns {Notification} + */ + setLaunchImage(launchImage: string): Notification { + this._launchImage = launchImage; + return this._notification; + } + + /** + * + * @param threadIdentifier + * @returns {Notification} + */ + setThreadIdentifier(threadIdentifier: string): Notification { + this._threadIdentifier = threadIdentifier; + return this._notification; + } + + build(): NativeIOSNotification { + // TODO: Validation + + return { + alertAction: this._alertAction, + attachments: this._attachments, + badge: this._badge, + category: this._category, + hasAction: this._hasAction, + identifier: this._identifier, + launchImage: this._launchImage, + threadIdentifier: this._threadIdentifier, + }; + } +} diff --git a/lib/modules/notifications/Notification.js b/lib/modules/notifications/Notification.js new file mode 100644 index 00000000..ab7dbc6c --- /dev/null +++ b/lib/modules/notifications/Notification.js @@ -0,0 +1,120 @@ +/** + * @flow + * Notification representation wrapper + */ +import AndroidNotification from './AndroidNotification'; +import IOSNotification from './IOSNotification'; +import { isObject } from '../../utils'; + +import type { NativeAndroidNotification } from './AndroidNotification'; +import type { NativeIOSNotification } from './IOSNotification'; + +type NativeNotification = {| + android: NativeAndroidNotification, + body: string, + data: { [string]: string }, + ios: NativeIOSNotification, + sound?: string, + subtitle?: string, + title: string, +|}; + +export default class Notification { + // iOS 8/9 | 10+ | Android + _android: AndroidNotification; + _body: string; // alertBody | body | contentText + _data: { [string]: string }; // userInfo | userInfo | extras + _ios: IOSNotification; + _sound: string | void; // soundName | sound | sound + _subtitle: string | void; // N/A | subtitle | subText + _title: string; // alertTitle | title | contentTitle + + constructor() { + this._android = new AndroidNotification(this); + this._data = {}; + this._ios = new IOSNotification(this); + } + + get android(): AndroidNotification { + return this._android; + } + + get ios(): IOSNotification { + return this._ios; + } + + /** + * + * @param body + * @returns {Notification} + */ + setBody(body: string): Notification { + this._body = body; + return this; + } + + /** + * + * @param data + * @returns {Notification} + */ + setData(data: Object = {}): Notification { + if (!isObject(data)) { + throw new Error( + `Notification:withData expects an object but got type '${typeof data}'.` + ); + } + this._data = data; + return this; + } + + /** + * + * @param sound + * @returns {Notification} + */ + setSound(sound: string): Notification { + this._sound = sound; + return this; + } + + /** + * + * @param subtitle + * @returns {Notification} + */ + setSubtitle(subtitle: string): Notification { + this._subtitle = subtitle; + return this; + } + + /** + * + * @param title + * @returns {Notification} + */ + setTitle(title: string): Notification { + this._title = title; + return this; + } + + build(): NativeNotification { + // Android required fields: body, title, smallicon + // iOS required fields: TODO + if (!this.body) { + throw new Error('Notification: Missing required `body` property'); + } else if (!this.title) { + throw new Error('Notification: Missing required `title` property'); + } + + return { + android: this._android.build(), + body: this._body, + data: this._data, + ios: this._ios.build(), + sound: this._sound, + subtitle: this._subtitle, + title: this._title, + }; + } +} diff --git a/lib/modules/notifications/index.js b/lib/modules/notifications/index.js index 94b1753c..84fb39ab 100644 --- a/lib/modules/notifications/index.js +++ b/lib/modules/notifications/index.js @@ -7,28 +7,49 @@ import { getLogger } from '../../utils/log'; import ModuleBase from '../../utils/ModuleBase'; import { getNativeModule } from '../../utils/native'; import { isFunction, isObject } from '../../utils'; +import Notification from './Notification'; +import { + BadgeIconType, + Category, + Defaults, + GroupAlert, + Priority, + Visibility, +} from './AndroidNotification'; import type App from '../core/firebase-app'; -type CreateNotification = { - // TODO -}; - -type Notification = { - // TODO -}; - +// TODO: Received notification type will be different from sent notification type OnNotification = Notification => any; type OnNotificationObserver = { next: OnNotification, }; +// TODO: Schedule type +type Schedule = { + build: () => Object, +}; + const NATIVE_EVENTS = ['notifications_notification_received']; export const MODULE_NAME = 'RNFirebaseNotifications'; export const NAMESPACE = 'notifications'; +// iOS 8/9 scheduling +// fireDate: Date; +// timeZone: TimeZone; +// repeatInterval: NSCalendar.Unit; +// repeatCalendar: Calendar; +// region: CLRegion; +// regionTriggersOnce: boolean; + +// iOS 10 scheduling +// TODO + +// Android scheduling +// TODO + /** * @class Notifications */ @@ -51,28 +72,34 @@ export default class Notifications extends ModuleBase { ); } - /** - * Cancel a local notification by id - using '*' will cancel - * all local notifications. - * @param id - * @returns {*} - */ - cancelNotification(id: string): Promise { - if (!id) return Promise.reject(new Error('Missing notification id')); - if (id === '*') return getNativeModule(this).cancelAllLocalNotifications(); - return getNativeModule(this).cancelLocalNotification(id); + cancelAllNotifications(): Promise { + return getNativeModule(this).cancelAllLocalNotifications(); } /** - * Create and display a local notification + * Cancel a local notification by id. + * @param id + * @returns {*} + */ + cancelNotification(notificationId: string): Promise { + if (!notificationId) { + return Promise.reject(new Error('Missing notificationId')); + } + return getNativeModule(this).cancelLocalNotification(notificationId); + } + + /** + * Display a local notification * @param notification * @returns {*} */ - createNotification(notification: CreateNotification): Promise { - const _notification = Object.assign({}, notification); - _notification.id = _notification.id || new Date().getTime().toString(); - _notification.local_notification = true; - return getNativeModule(this).createLocalNotification(_notification); + displayNotification(notification: Notification): Promise { + if (!(notification instanceof Notification)) { + throw new Error( + `Notifications:displayNotification expects a 'Notification' but got type ${typeof notification}` + ); + } + return getNativeModule(this).displayNotification(notification.build()); } /** @@ -108,22 +135,23 @@ export default class Notifications extends ModuleBase { } /** - * Remove a delivered notification. - * @param id + * Remove all delivered notifications. * @returns {*} */ - removeDeliveredNotification(id: string): Promise { - if (!id) return Promise.reject(new Error('Missing notification id')); - return getNativeModule(this).removeDeliveredNotification(id); + removeAllDeliveredNotifications(): Promise { + return getNativeModule(this).removeAllDeliveredNotifications(); } /** - * Remove all delivered notifications. - * @param id + * Remove a delivered notification. + * @param notificationId * @returns {*} */ - removeDeliveredNotifications(): Promise { - return getNativeModule(this).removeDeliveredNotifications(); + removeDeliveredNotification(notificationId: string): Promise { + if (!notificationId) { + return Promise.reject(new Error('Missing notificationId')); + } + return getNativeModule(this).removeDeliveredNotification(notificationId); } /** @@ -131,15 +159,30 @@ export default class Notifications extends ModuleBase { * @param notification * @returns {*} */ - scheduleNotification(notification: CreateNotification): Promise { - const _notification = Object.assign({}, notification); - if (!notification.id) - return Promise.reject( - new Error('An id is required to schedule a local notification.') + scheduleNotification( + notification: Notification, + schedule: Schedule + ): Promise { + if (!(notification instanceof Notification)) { + throw new Error( + `Notifications:scheduleNotification expects a 'Notification' but got type ${typeof notification}` ); - _notification.local_notification = true; - return getNativeModule(this).scheduleLocalNotification(_notification); + } + return getNativeModule(this).scheduleNotification( + notification.build(), + schedule.build() + ); } } -export const statics = {}; +export const statics = { + Android: { + BadgeIconType, + Category, + Defaults, + GroupAlert, + Priority, + Visibility, + }, + Notification, +};