1 | import { CodedError, RCTDeviceEventEmitter, UnavailabilityError } from '@unimodules/core';
|
2 | import Constants from 'expo-constants';
|
3 | import { EventEmitter } from 'fbemitter';
|
4 | import invariant from 'invariant';
|
5 | import { Platform } from 'react-native';
|
6 | import ExponentNotifications from './ExponentNotifications';
|
7 | import Storage from './Storage';
|
8 | let _emitter;
|
9 | let _initialNotification;
|
10 | function _maybeInitEmitter() {
|
11 | if (!_emitter) {
|
12 | _emitter = new EventEmitter();
|
13 | RCTDeviceEventEmitter.addListener('Exponent.notification', emitNotification);
|
14 | }
|
15 | }
|
16 | export function emitNotification(notification) {
|
17 | if (typeof notification === 'string') {
|
18 | notification = JSON.parse(notification);
|
19 | }
|
20 |
|
21 | notification = { ...notification };
|
22 | if (typeof notification.data === 'string') {
|
23 | try {
|
24 | notification.data = JSON.parse(notification.data);
|
25 | }
|
26 | catch (e) {
|
27 |
|
28 | }
|
29 | }
|
30 | _emitter.emit('notification', notification);
|
31 | }
|
32 | function _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 |
|
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 |
|
52 |
|
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 | }
|
64 | function _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 | }
|
72 | const ASYNC_STORAGE_PREFIX = '__expo_internal_channel_';
|
73 |
|
74 |
|
75 | const IS_USING_NEW_BINARY = ExponentNotifications && typeof ExponentNotifications.createChannel === 'function';
|
76 | async 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 | }
|
86 | function _legacyDeleteChannel(id) {
|
87 | return Storage.removeItem(`${ASYNC_STORAGE_PREFIX}${id}`);
|
88 | }
|
89 | if (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 |
|
106 |
|
107 | function _legacySaveChannel(id, channel) {
|
108 | return Storage.setItem(`${ASYNC_STORAGE_PREFIX}${id}`, JSON.stringify(channel));
|
109 | }
|
110 | export default {
|
111 |
|
112 | _setInitialNotification(notification) {
|
113 | _initialNotification = notification;
|
114 | },
|
115 |
|
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 |
|
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 |
|
146 |
|
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 |
|
158 |
|
159 | if (!IS_USING_NEW_BINARY) {
|
160 | return Promise.resolve();
|
161 | }
|
162 | return ExponentNotifications.deleteChannel(id);
|
163 | },
|
164 |
|
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 |
|
178 | _legacyDeleteChannel(nativeNotification.channelId);
|
179 | return ExponentNotifications.presentLocalNotificationWithChannel(nativeNotification, _channel);
|
180 | }
|
181 | else {
|
182 |
|
183 |
|
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 |
|
194 | async scheduleLocalNotificationAsync(notification, options = {}) {
|
195 |
|
196 |
|
197 | const now = Date.now();
|
198 |
|
199 | _validateNotification(notification);
|
200 | const nativeNotification = _processNotification(notification);
|
201 |
|
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 |
|
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 |
|
218 |
|
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 |
|
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 |
|
259 | _legacyDeleteChannel(nativeNotification.channelId);
|
260 | return ExponentNotifications.scheduleLocalNotificationWithChannel(nativeNotification, options, _channel);
|
261 | }
|
262 | else {
|
263 |
|
264 |
|
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 |
|
275 | async dismissNotificationAsync(notificationId) {
|
276 | if (!ExponentNotifications.dismissNotification) {
|
277 | throw new UnavailabilityError('Expo.Notifications', 'dismissNotification');
|
278 | }
|
279 | return await ExponentNotifications.dismissNotification(notificationId);
|
280 | },
|
281 |
|
282 | async dismissAllNotificationsAsync() {
|
283 | if (!ExponentNotifications.dismissAllNotifications) {
|
284 | throw new UnavailabilityError('Expo.Notifications', 'dismissAllNotifications');
|
285 | }
|
286 | return await ExponentNotifications.dismissAllNotifications();
|
287 | },
|
288 |
|
289 | cancelScheduledNotificationAsync(notificationId) {
|
290 | if (Platform.OS === 'android' && typeof notificationId === 'string') {
|
291 | return ExponentNotifications.cancelScheduledNotificationWithStringIdAsync(notificationId);
|
292 | }
|
293 | return ExponentNotifications.cancelScheduledNotificationAsync(notificationId);
|
294 | },
|
295 |
|
296 | cancelAllScheduledNotificationsAsync() {
|
297 | return ExponentNotifications.cancelAllScheduledNotificationsAsync();
|
298 | },
|
299 |
|
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 | };
|
347 | function isInRangeInclusive(variable, min, max) {
|
348 | return variable >= min && variable <= max;
|
349 | }
|
350 |
|
\ | No newline at end of file |