UNPKG

15.9 kBJavaScriptView Raw
1import { CodedError, RCTDeviceEventEmitter, UnavailabilityError } from '@unimodules/core';
2import Constants from 'expo-constants';
3import { EventEmitter } from 'fbemitter';
4import invariant from 'invariant';
5import { Platform } from 'react-native';
6import ExponentNotifications from './ExponentNotifications';
7import Storage from './Storage';
8let _emitter;
9let _initialNotification;
10function _maybeInitEmitter() {
11 if (!_emitter) {
12 _emitter = new EventEmitter();
13 RCTDeviceEventEmitter.addListener('Exponent.notification', emitNotification);
14 }
15}
16export function emitNotification(notification) {
17 if (typeof notification === 'string') {
18 notification = JSON.parse(notification);
19 }
20 /* Don't mutate the original notification */
21 notification = { ...notification };
22 if (typeof notification.data === 'string') {
23 try {
24 notification.data = JSON.parse(notification.data);
25 }
26 catch (e) {
27 // It's actually just a string, that's fine
28 }
29 }
30 _emitter.emit('notification', notification);
31}
32function _processNotification(notification) {
33 notification = Object.assign({}, notification);
34 if (!notification.data) {
35 notification.data = {};
36 }
37 if (notification.hasOwnProperty('count')) {
38 delete notification.count;
39 }
40 // Delete any Android properties on iOS and merge the iOS properties on root notification object
41 if (Platform.OS === 'ios') {
42 if (notification.android) {
43 delete notification.android;
44 }
45 if (notification.ios) {
46 notification = Object.assign(notification, notification.ios);
47 notification.data._displayInForeground = notification.ios._displayInForeground;
48 delete notification.ios;
49 }
50 }
51 // Delete any iOS properties on Android and merge the Android properties on root notification
52 // object
53 if (Platform.OS === 'android') {
54 if (notification.ios) {
55 delete notification.ios;
56 }
57 if (notification.android) {
58 notification = Object.assign(notification, notification.android);
59 delete notification.android;
60 }
61 }
62 return notification;
63}
64function _validateNotification(notification) {
65 if (Platform.OS === 'ios') {
66 invariant(!!notification.title && !!notification.body, 'Local notifications on iOS require both a title and a body');
67 }
68 else if (Platform.OS === 'android') {
69 invariant(!!notification.title, 'Local notifications on Android require a title');
70 }
71}
72const ASYNC_STORAGE_PREFIX = '__expo_internal_channel_';
73// TODO: remove this before releasing
74// this will always be `true` for SDK 28+
75const IS_USING_NEW_BINARY = ExponentNotifications && typeof ExponentNotifications.createChannel === 'function';
76async function _legacyReadChannel(id) {
77 try {
78 const channelString = await Storage.getItem(`${ASYNC_STORAGE_PREFIX}${id}`);
79 if (channelString) {
80 return JSON.parse(channelString);
81 }
82 }
83 catch (e) { }
84 return null;
85}
86function _legacyDeleteChannel(id) {
87 return Storage.removeItem(`${ASYNC_STORAGE_PREFIX}${id}`);
88}
89if (Platform.OS === 'android') {
90 Storage.clear = async function (callback) {
91 try {
92 const keys = await Storage.getAllKeys();
93 if (keys && keys.length) {
94 const filteredKeys = keys.filter(key => !key.startsWith(ASYNC_STORAGE_PREFIX));
95 await Storage.multiRemove(filteredKeys);
96 }
97 callback && callback();
98 }
99 catch (e) {
100 callback && callback(e);
101 throw e;
102 }
103 };
104}
105// This codepath will never be triggered in SDK 28 and above
106// TODO: remove before releasing
107function _legacySaveChannel(id, channel) {
108 return Storage.setItem(`${ASYNC_STORAGE_PREFIX}${id}`, JSON.stringify(channel));
109}
110export default {
111 /* Only used internally to initialize the notification from top level props */
112 _setInitialNotification(notification) {
113 _initialNotification = notification;
114 },
115 // User passes set of actions titles.
116 createCategoryAsync(categoryId, actions, previewPlaceholder) {
117 return Platform.OS === 'ios'
118 ? ExponentNotifications.createCategoryAsync(categoryId, actions, previewPlaceholder)
119 : ExponentNotifications.createCategoryAsync(categoryId, actions);
120 },
121 deleteCategoryAsync(categoryId) {
122 return ExponentNotifications.deleteCategoryAsync(categoryId);
123 },
124 /* Re-export */
125 getExpoPushTokenAsync() {
126 if (!ExponentNotifications.getExponentPushTokenAsync) {
127 throw new UnavailabilityError('Expo.Notifications', 'getExpoPushTokenAsync');
128 }
129 if (!Constants.isDevice) {
130 throw new Error(`Must be on a physical device to get an Expo Push Token`);
131 }
132 return ExponentNotifications.getExponentPushTokenAsync();
133 },
134 getDevicePushTokenAsync: (config) => {
135 if (!ExponentNotifications.getDevicePushTokenAsync) {
136 throw new UnavailabilityError('Expo.Notifications', 'getDevicePushTokenAsync');
137 }
138 return ExponentNotifications.getDevicePushTokenAsync(config || {});
139 },
140 createChannelAndroidAsync(id, channel) {
141 if (Platform.OS !== 'android') {
142 console.warn(`createChannelAndroidAsync(...) has no effect on ${Platform.OS}`);
143 return Promise.resolve();
144 }
145 // This codepath will never be triggered in SDK 28 and above
146 // TODO: remove before releasing
147 if (!IS_USING_NEW_BINARY) {
148 return _legacySaveChannel(id, channel);
149 }
150 return ExponentNotifications.createChannel(id, channel);
151 },
152 deleteChannelAndroidAsync(id) {
153 if (Platform.OS !== 'android') {
154 console.warn(`deleteChannelAndroidAsync(...) has no effect on ${Platform.OS}`);
155 return Promise.resolve();
156 }
157 // This codepath will never be triggered in SDK 28 and above
158 // TODO: remove before releasing
159 if (!IS_USING_NEW_BINARY) {
160 return Promise.resolve();
161 }
162 return ExponentNotifications.deleteChannel(id);
163 },
164 /* Shows a notification instantly */
165 async presentLocalNotificationAsync(notification) {
166 _validateNotification(notification);
167 const nativeNotification = _processNotification(notification);
168 if (Platform.OS !== 'android') {
169 return await ExponentNotifications.presentLocalNotification(nativeNotification);
170 }
171 else {
172 let _channel;
173 if (nativeNotification.channelId) {
174 _channel = await _legacyReadChannel(nativeNotification.channelId);
175 }
176 if (IS_USING_NEW_BINARY) {
177 // delete the legacy channel from AsyncStorage so this codepath isn't triggered anymore
178 _legacyDeleteChannel(nativeNotification.channelId);
179 return ExponentNotifications.presentLocalNotificationWithChannel(nativeNotification, _channel);
180 }
181 else {
182 // TODO: remove this codepath before releasing, it will never be triggered on SDK 28+
183 // channel does not actually exist, so add its settings to the individual notification
184 if (_channel) {
185 nativeNotification.sound = _channel.sound;
186 nativeNotification.priority = _channel.priority;
187 nativeNotification.vibrate = _channel.vibrate;
188 }
189 return ExponentNotifications.presentLocalNotification(nativeNotification);
190 }
191 }
192 },
193 /* Schedule a notification at a later date */
194 async scheduleLocalNotificationAsync(notification, options = {}) {
195 // set now at the beginning of the method, to prevent potential weird warnings when we validate
196 // options.time later on
197 const now = Date.now();
198 // Validate and process the notification data
199 _validateNotification(notification);
200 const nativeNotification = _processNotification(notification);
201 // Validate `options.time`
202 if (options.time) {
203 let timeAsDateObj = null;
204 if (options.time && typeof options.time === 'number') {
205 timeAsDateObj = new Date(options.time);
206 if (timeAsDateObj.toString() === 'Invalid Date') {
207 timeAsDateObj = null;
208 }
209 }
210 else if (options.time && options.time instanceof Date) {
211 timeAsDateObj = options.time;
212 }
213 // If we couldn't convert properly, throw an error
214 if (!timeAsDateObj) {
215 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.`);
216 }
217 // If someone passes in a value that is too small, say, by an order of 1000 (it's common to
218 // accidently pass seconds instead of ms), display a warning.
219 if (timeAsDateObj.getTime() < now) {
220 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?`);
221 }
222 options = {
223 ...options,
224 time: timeAsDateObj.getTime(),
225 };
226 }
227 if (options.intervalMs != null && options.repeat != null) {
228 throw new Error(`Pass either the "repeat" option or "intervalMs" option, not both`);
229 }
230 // Validate options.repeat
231 if (options.repeat != null) {
232 const validOptions = new Set(['minute', 'hour', 'day', 'week', 'month', 'year']);
233 if (!validOptions.has(options.repeat)) {
234 throw new Error(`Pass one of ['minute', 'hour', 'day', 'week', 'month', 'year'] as the value for the "repeat" option`);
235 }
236 }
237 if (options.intervalMs != null) {
238 if (Platform.OS === 'ios') {
239 throw new Error(`The "intervalMs" option is not supported on iOS`);
240 }
241 if (options.intervalMs <= 0 || !Number.isInteger(options.intervalMs)) {
242 throw new Error(`Pass an integer greater than zero as the value for the "intervalMs" option`);
243 }
244 }
245 if (Platform.OS !== 'android') {
246 if (options.repeat) {
247 console.warn('Ability to schedule an automatically repeated notification is deprecated on iOS and will be removed in the next SDK release.');
248 return ExponentNotifications.legacyScheduleLocalRepeatingNotification(nativeNotification, options);
249 }
250 return ExponentNotifications.scheduleLocalNotification(nativeNotification, options);
251 }
252 else {
253 let _channel;
254 if (nativeNotification.channelId) {
255 _channel = await _legacyReadChannel(nativeNotification.channelId);
256 }
257 if (IS_USING_NEW_BINARY) {
258 // delete the legacy channel from AsyncStorage so this codepath isn't triggered anymore
259 _legacyDeleteChannel(nativeNotification.channelId);
260 return ExponentNotifications.scheduleLocalNotificationWithChannel(nativeNotification, options, _channel);
261 }
262 else {
263 // TODO: remove this codepath before releasing, it will never be triggered on SDK 28+
264 // channel does not actually exist, so add its settings to the individual notification
265 if (_channel) {
266 nativeNotification.sound = _channel.sound;
267 nativeNotification.priority = _channel.priority;
268 nativeNotification.vibrate = _channel.vibrate;
269 }
270 return ExponentNotifications.scheduleLocalNotification(nativeNotification, options);
271 }
272 }
273 },
274 /* Dismiss currently shown notification with ID (Android only) */
275 async dismissNotificationAsync(notificationId) {
276 if (!ExponentNotifications.dismissNotification) {
277 throw new UnavailabilityError('Expo.Notifications', 'dismissNotification');
278 }
279 return await ExponentNotifications.dismissNotification(notificationId);
280 },
281 /* Dismiss all currently shown notifications (Android only) */
282 async dismissAllNotificationsAsync() {
283 if (!ExponentNotifications.dismissAllNotifications) {
284 throw new UnavailabilityError('Expo.Notifications', 'dismissAllNotifications');
285 }
286 return await ExponentNotifications.dismissAllNotifications();
287 },
288 /* Cancel scheduled notification notification with ID */
289 cancelScheduledNotificationAsync(notificationId) {
290 if (Platform.OS === 'android' && typeof notificationId === 'string') {
291 return ExponentNotifications.cancelScheduledNotificationWithStringIdAsync(notificationId);
292 }
293 return ExponentNotifications.cancelScheduledNotificationAsync(notificationId);
294 },
295 /* Cancel all scheduled notifications */
296 cancelAllScheduledNotificationsAsync() {
297 return ExponentNotifications.cancelAllScheduledNotificationsAsync();
298 },
299 /* Primary public api */
300 addListener(listener) {
301 _maybeInitEmitter();
302 if (_initialNotification) {
303 const initialNotification = _initialNotification;
304 _initialNotification = null;
305 setTimeout(() => {
306 emitNotification(initialNotification);
307 }, 0);
308 }
309 return _emitter.addListener('notification', listener);
310 },
311 async getBadgeNumberAsync() {
312 if (!ExponentNotifications.getBadgeNumberAsync) {
313 return 0;
314 }
315 return ExponentNotifications.getBadgeNumberAsync();
316 },
317 async setBadgeNumberAsync(number) {
318 if (!ExponentNotifications.setBadgeNumberAsync) {
319 throw new UnavailabilityError('Expo.Notifications', 'setBadgeNumberAsync');
320 }
321 return ExponentNotifications.setBadgeNumberAsync(number);
322 },
323 async scheduleNotificationWithCalendarAsync(notification, options = {}) {
324 const areOptionsValid = (options.month == null || isInRangeInclusive(options.month, 1, 12)) &&
325 (options.day == null || isInRangeInclusive(options.day, 1, 31)) &&
326 (options.hour == null || isInRangeInclusive(options.hour, 0, 23)) &&
327 (options.minute == null || isInRangeInclusive(options.minute, 0, 59)) &&
328 (options.second == null || isInRangeInclusive(options.second, 0, 59)) &&
329 (options.weekDay == null || isInRangeInclusive(options.weekDay, 1, 7)) &&
330 (options.weekDay == null || options.day == null);
331 if (!areOptionsValid) {
332 throw new CodedError('WRONG_OPTIONS', 'Options in scheduleNotificationWithCalendarAsync call were incorrect!');
333 }
334 _validateNotification(notification);
335 const nativeNotification = _processNotification(notification);
336 return ExponentNotifications.scheduleNotificationWithCalendar(nativeNotification, options);
337 },
338 async scheduleNotificationWithTimerAsync(notification, options) {
339 if (options.interval < 1) {
340 throw new CodedError('WRONG_OPTIONS', 'Interval must be not less then 1');
341 }
342 _validateNotification(notification);
343 const nativeNotification = _processNotification(notification);
344 return ExponentNotifications.scheduleNotificationWithTimer(nativeNotification, options);
345 },
346};
347function isInRangeInclusive(variable, min, max) {
348 return variable >= min && variable <= max;
349}
350//# sourceMappingURL=Notifications.js.map
\No newline at end of file