352 lines
16 KiB
JavaScript
352 lines
16 KiB
JavaScript
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
|