UNPKG

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