UNPKG

expo

Version:
288 lines (243 loc) • 8.18 kB
// @flow import { EventEmitter, EventSubscription } from 'fbemitter'; import invariant from 'invariant'; import warning from 'fbjs/lib/warning'; import { DeviceEventEmitter, NativeModules, Platform } from 'react-native'; const { ExponentNotifications } = NativeModules; type Notification = { origin: 'selected' | 'received', data: any, remote: boolean, isMultiple: boolean, }; type LocalNotification = { title: string, // How should we deal with body being required on iOS but not on Android? body?: string, data?: any, ios?: { sound?: boolean, }, android?: { sound?: boolean, icon?: string, color?: string, priority?: string, sticky?: boolean, vibrate?: boolean | Array<number>, link?: string, }, }; // Android assigns unique number to each notification natively. // Since that's not supported on iOS, we generate an unique string. type LocalNotificationId = string | number; let _emitter; let _initialNotification; function _maybeInitEmitter() { if (!_emitter) { _emitter = new EventEmitter(); DeviceEventEmitter.addListener('Exponent.notification', _emitNotification); } } function _emitNotification(notification) { if (typeof notification === 'string') { notification = JSON.parse(notification); } /* Don't mutate the original notification */ notification = { ...notification }; if (typeof notification.data === 'string') { try { notification.data = JSON.parse(notification.data); } catch (e) { // It's actually just a string, that's fine } } _emitter.emit('notification', notification); } function _processNotification(notification) { notification = Object.assign({}, notification); if (!notification.data) { notification.data = {}; } if (notification.hasOwnProperty('count')) { delete notification.count; } // Delete any Android properties on iOS and merge the iOS properties on root // notification object if (Platform.OS === 'ios') { if (notification.android) { delete notification.android; } if (notification.ios) { notification = Object.assign(notification, notification.ios); delete notification.ios; } } // Delete any iOS properties on Android and merge the Android properties on // root notification object if (Platform.OS === 'android') { if (notification.ios) { delete notification.ios; } if (notification.android) { notification = Object.assign(notification, notification.android); delete notification.android; } } return notification; } function _validateNotification(notification) { if (Platform.OS === 'ios') { invariant( !!notification.title && !!notification.body, 'Local notifications on iOS require both a title and a body' ); } else if (Platform.OS === 'android') { invariant( !!notification.title, 'Local notifications on Android require a title' ); } } export default { /* Only used internally to initialize the notification from top level props */ _setInitialNotification(notification: Notification) { _initialNotification = notification; }, /* Re-export, we can add flow here if we want as well */ getExponentPushTokenAsync: ExponentNotifications.getExponentPushTokenAsync, /* Re-export, we can add flow here if we want as well */ getDevicePushTokenAsync: ExponentNotifications.getDevicePushTokenAsync, /* Shows a notification instantly */ presentLocalNotificationAsync( notification: LocalNotification ): Promise<LocalNotificationId> { _validateNotification(notification); notification = _processNotification(notification); return ExponentNotifications.presentLocalNotification(notification); }, /* Schedule a notification at a later date */ async scheduleLocalNotificationAsync( notification: LocalNotification, options: { time?: Date | number, repeat?: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year', } = {} ): Promise<LocalNotificationId> { // set now at the beginning of the method, to prevent potential // weird warnings when we validate options.time later on const now = Date.now(); // Validate and process the notification data _validateNotification(notification); notification = _processNotification(notification); // Validate `options.time` if (options.time) { let timeAsDateObj = null; if (options.time && typeof options.time === 'number') { timeAsDateObj = new Date(options.time); // god, JS is the worst if (((timeAsDateObj: any): string) == 'Invalid Date') { timeAsDateObj = null; } } else if (options.time && options.time instanceof Date) { timeAsDateObj = options.time; } // If we couldn't convert properly, throw an error if (!timeAsDateObj) { throw new Error( `Provided value for "time" is invalid. Please verify that it's either a number representing Unix Epoch time in milliseconds, or a valid date object.` ); } // If someone passes in a value that is too small, say, by an order of 1000 // (it's common to accidently pass seconds instead of ms), display a warning. warning( timeAsDateObj >= now, `Provided value for "time" is before the current date. Did you possibly pass number of seconds since Unix Epoch instead of number of milliseconds?` ); // If iOS, pass time as milliseconds if (Platform.OS === 'ios') { options = { ...options, time: timeAsDateObj.getTime(), }; } else { options = { ...options, time: timeAsDateObj, }; } } // Validate options.repeat if (options.repeat) { const validOptions = new Set([ 'minute', 'hour', 'day', 'week', 'month', 'year', ]); if (!validOptions.has(options.repeat)) { throw new Error( `Please pass one of ['minute', 'hour', 'day', 'week', 'month', 'year'] as the value for the "repeat" option` ); } } return ExponentNotifications.scheduleLocalNotification( notification, options ); }, /* Dismiss currently shown notification with ID (Android only) */ async dismissNotificationAsync( notificationId: LocalNotificationId ): Promise<void> { if (Platform.OS === 'android') { return ExponentNotifications.dismissNotification(notificationId); } else { return Promise.reject('Dismissing notifications is not supported on iOS'); } }, /* Dismiss all currently shown notifications (Android only) */ async dismissAllNotificationsAsync(): Promise<void> { if (Platform.OS === 'android') { return ExponentNotifications.dismissAllNotifications(); } else { return Promise.reject('Dismissing notifications is not supported on iOS'); } }, /* Cancel scheduled notification notification with ID */ async cancelScheduledNotificationAsync( notificationId: LocalNotificationId ): Promise<void> { return ExponentNotifications.cancelScheduledNotification(notificationId); }, /* Cancel all scheduled notifications */ async cancelAllScheduledNotificationsAsync(): Promise<void> { return ExponentNotifications.cancelAllScheduledNotifications(); }, /* Primary public api */ addListener(listener: Function): EventSubscription { _maybeInitEmitter(); if (_initialNotification) { const initialNotification = _initialNotification; _initialNotification = null; setTimeout(() => { _emitNotification(initialNotification); }, 0); } return _emitter.addListener('notification', listener); }, async getBadgeNumberAsync(): Promise<number> { if (!ExponentNotifications.getBadgeNumberAsync) { return 0; } return ExponentNotifications.getBadgeNumberAsync(); }, async setBadgeNumberAsync(number: number): Promise<void> { if (!ExponentNotifications.setBadgeNumberAsync) { return; } return ExponentNotifications.setBadgeNumberAsync(number); }, };