import { CodedError, RCTDeviceEventEmitter, UnavailabilityError } from '@unimodules/core'; import Constants from 'expo-constants'; import { EventEmitter } from 'fbemitter'; import invariant from 'invariant'; import { Platform } from 'react-native'; import ExponentNotifications from './ExponentNotifications'; import Storage from './Storage'; let _emitter; let _initialNotification; function _getEventEmitter() { if (!_emitter) { _emitter = new EventEmitter(); RCTDeviceEventEmitter.addListener('Exponent.notification', emitNotification); } return _emitter; } export 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 } } const emitter = _getEventEmitter(); 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); notification.data._displayInForeground = notification.ios._displayInForeground; 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'); } } const ASYNC_STORAGE_PREFIX = '__expo_internal_channel_'; // TODO: remove this before releasing // this will always be `true` for SDK 28+ const IS_USING_NEW_BINARY = ExponentNotifications && typeof ExponentNotifications.createChannel === 'function'; async function _legacyReadChannel(id) { try { const channelString = await Storage.getItem(`${ASYNC_STORAGE_PREFIX}${id}`); if (channelString) { return JSON.parse(channelString); } } catch (e) { } return null; } function _legacyDeleteChannel(id) { return Storage.removeItem(`${ASYNC_STORAGE_PREFIX}${id}`); } if (Platform.OS === 'android') { Storage.clear = async function (callback) { try { const keys = await Storage.getAllKeys(); if (keys && keys.length) { const filteredKeys = keys.filter(key => !key.startsWith(ASYNC_STORAGE_PREFIX)); await Storage.multiRemove(filteredKeys); } callback && callback(); } catch (e) { callback && callback(e); throw e; } }; } // This codepath will never be triggered in SDK 28 and above // TODO: remove before releasing function _legacySaveChannel(id, channel) { return Storage.setItem(`${ASYNC_STORAGE_PREFIX}${id}`, JSON.stringify(channel)); } export default { /* Only used internally to initialize the notification from top level props */ _setInitialNotification(notification) { _initialNotification = notification; }, // User passes set of actions titles. createCategoryAsync(categoryId, actions, previewPlaceholder) { return Platform.OS === 'ios' ? ExponentNotifications.createCategoryAsync(categoryId, actions, previewPlaceholder) : ExponentNotifications.createCategoryAsync(categoryId, actions); }, deleteCategoryAsync(categoryId) { return ExponentNotifications.deleteCategoryAsync(categoryId); }, /* Re-export */ getExpoPushTokenAsync() { if (!ExponentNotifications.getExponentPushTokenAsync) { throw new UnavailabilityError('Expo.Notifications', 'getExpoPushTokenAsync'); } if (!Constants.isDevice) { throw new Error(`Must be on a physical device to get an Expo Push Token`); } return ExponentNotifications.getExponentPushTokenAsync(); }, getDevicePushTokenAsync: (config) => { if (!ExponentNotifications.getDevicePushTokenAsync) { throw new UnavailabilityError('Expo.Notifications', 'getDevicePushTokenAsync'); } return ExponentNotifications.getDevicePushTokenAsync(config || {}); }, createChannelAndroidAsync(id, channel) { if (Platform.OS !== 'android') { console.warn(`createChannelAndroidAsync(...) has no effect on ${Platform.OS}`); return Promise.resolve(); } // This codepath will never be triggered in SDK 28 and above // TODO: remove before releasing if (!IS_USING_NEW_BINARY) { return _legacySaveChannel(id, channel); } return ExponentNotifications.createChannel(id, channel); }, deleteChannelAndroidAsync(id) { if (Platform.OS !== 'android') { console.warn(`deleteChannelAndroidAsync(...) has no effect on ${Platform.OS}`); return Promise.resolve(); } // This codepath will never be triggered in SDK 28 and above // TODO: remove before releasing if (!IS_USING_NEW_BINARY) { return Promise.resolve(); } return ExponentNotifications.deleteChannel(id); }, /* Shows a notification instantly */ async presentLocalNotificationAsync(notification) { _validateNotification(notification); const nativeNotification = _processNotification(notification); if (Platform.OS !== 'android') { return await ExponentNotifications.presentLocalNotification(nativeNotification); } else { let _channel; if (nativeNotification.channelId) { _channel = await _legacyReadChannel(nativeNotification.channelId); } if (IS_USING_NEW_BINARY) { // delete the legacy channel from AsyncStorage so this codepath isn't triggered anymore _legacyDeleteChannel(nativeNotification.channelId); return ExponentNotifications.presentLocalNotificationWithChannel(nativeNotification, _channel); } else { // TODO: remove this codepath before releasing, it will never be triggered on SDK 28+ // channel does not actually exist, so add its settings to the individual notification if (_channel) { nativeNotification.sound = _channel.sound; nativeNotification.priority = _channel.priority; nativeNotification.vibrate = _channel.vibrate; } return ExponentNotifications.presentLocalNotification(nativeNotification); } } }, /* Schedule a notification at a later date */ async scheduleLocalNotificationAsync(notification, options = {}) { // 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); const nativeNotification = _processNotification(notification); // Validate `options.time` if (options.time) { let timeAsDateObj = null; if (options.time && typeof options.time === 'number') { timeAsDateObj = new Date(options.time); if (timeAsDateObj.toString() === '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. if (timeAsDateObj.getTime() < now) { console.warn(`Provided value for "time" is before the current date. Did you possibly pass number of seconds since Unix Epoch instead of number of milliseconds?`); } options = { ...options, time: timeAsDateObj.getTime(), }; } if (options.intervalMs != null && options.repeat != null) { throw new Error(`Pass either the "repeat" option or "intervalMs" option, not both`); } // Validate options.repeat if (options.repeat != null) { const validOptions = new Set(['minute', 'hour', 'day', 'week', 'month', 'year']); if (!validOptions.has(options.repeat)) { throw new Error(`Pass one of ['minute', 'hour', 'day', 'week', 'month', 'year'] as the value for the "repeat" option`); } } if (options.intervalMs != null) { if (Platform.OS === 'ios') { throw new Error(`The "intervalMs" option is not supported on iOS`); } if (options.intervalMs <= 0 || !Number.isInteger(options.intervalMs)) { throw new Error(`Pass an integer greater than zero as the value for the "intervalMs" option`); } } if (Platform.OS !== 'android') { if (options.repeat) { console.warn('Ability to schedule an automatically repeated notification is deprecated on iOS and will be removed in the next SDK release.'); return ExponentNotifications.legacyScheduleLocalRepeatingNotification(nativeNotification, options); } return ExponentNotifications.scheduleLocalNotification(nativeNotification, options); } else { let _channel; if (nativeNotification.channelId) { _channel = await _legacyReadChannel(nativeNotification.channelId); } if (IS_USING_NEW_BINARY) { // delete the legacy channel from AsyncStorage so this codepath isn't triggered anymore _legacyDeleteChannel(nativeNotification.channelId); return ExponentNotifications.scheduleLocalNotificationWithChannel(nativeNotification, options, _channel); } else { // TODO: remove this codepath before releasing, it will never be triggered on SDK 28+ // channel does not actually exist, so add its settings to the individual notification if (_channel) { nativeNotification.sound = _channel.sound; nativeNotification.priority = _channel.priority; nativeNotification.vibrate = _channel.vibrate; } return ExponentNotifications.scheduleLocalNotification(nativeNotification, options); } } }, /* Dismiss currently shown notification with ID (Android only) */ async dismissNotificationAsync(notificationId) { if (!ExponentNotifications.dismissNotification) { throw new UnavailabilityError('Expo.Notifications', 'dismissNotification'); } return await ExponentNotifications.dismissNotification(notificationId); }, /* Dismiss all currently shown notifications (Android only) */ async dismissAllNotificationsAsync() { if (!ExponentNotifications.dismissAllNotifications) { throw new UnavailabilityError('Expo.Notifications', 'dismissAllNotifications'); } return await ExponentNotifications.dismissAllNotifications(); }, /* Cancel scheduled notification notification with ID */ cancelScheduledNotificationAsync(notificationId) { if (Platform.OS === 'android' && typeof notificationId === 'string') { return ExponentNotifications.cancelScheduledNotificationWithStringIdAsync(notificationId); } return ExponentNotifications.cancelScheduledNotificationAsync(notificationId); }, /* Cancel all scheduled notifications */ cancelAllScheduledNotificationsAsync() { return ExponentNotifications.cancelAllScheduledNotificationsAsync(); }, /* Primary public api */ addListener(listener) { const emitter = _getEventEmitter(); if (_initialNotification) { const initialNotification = _initialNotification; _initialNotification = null; setTimeout(() => { emitNotification(initialNotification); }, 0); } return emitter.addListener('notification', listener); }, async getBadgeNumberAsync() { if (!ExponentNotifications.getBadgeNumberAsync) { return 0; } return ExponentNotifications.getBadgeNumberAsync(); }, async setBadgeNumberAsync(number) { if (!ExponentNotifications.setBadgeNumberAsync) { throw new UnavailabilityError('Expo.Notifications', 'setBadgeNumberAsync'); } return ExponentNotifications.setBadgeNumberAsync(number); }, async scheduleNotificationWithCalendarAsync(notification, options = {}) { const areOptionsValid = (options.month == null || isInRangeInclusive(options.month, 1, 12)) && (options.day == null || isInRangeInclusive(options.day, 1, 31)) && (options.hour == null || isInRangeInclusive(options.hour, 0, 23)) && (options.minute == null || isInRangeInclusive(options.minute, 0, 59)) && (options.second == null || isInRangeInclusive(options.second, 0, 59)) && (options.weekDay == null || isInRangeInclusive(options.weekDay, 1, 7)) && (options.weekDay == null || options.day == null); if (!areOptionsValid) { throw new CodedError('WRONG_OPTIONS', 'Options in scheduleNotificationWithCalendarAsync call were incorrect!'); } _validateNotification(notification); const nativeNotification = _processNotification(notification); return ExponentNotifications.scheduleNotificationWithCalendar(nativeNotification, options); }, async scheduleNotificationWithTimerAsync(notification, options) { if (options.interval < 1) { throw new CodedError('WRONG_OPTIONS', 'Interval must be not less then 1'); } _validateNotification(notification); const nativeNotification = _processNotification(notification); return ExponentNotifications.scheduleNotificationWithTimer(nativeNotification, options); }, }; function isInRangeInclusive(variable, min, max) { return variable >= min && variable <= max; } //# sourceMappingURL=Notifications.js.map