1import { Platform, UnavailabilityError, uuid } from 'expo-modules-core'; 2import NotificationScheduler from './NotificationScheduler'; 3/** 4 * Schedules a notification to be triggered in the future. 5 * > **Note:** Please note that this does not mean that the notification will be presented when it is triggered. 6 * For the notification to be presented you have to set a notification handler with [`setNotificationHandler`](#notificationssetnotificationhandlerhandler) 7 * that will return an appropriate notification behavior. For more information see the example below. 8 * @param request An object describing the notification to be triggered. 9 * @return Returns a Promise resolving to a string which is a notification identifier you can later use to cancel the notification or to identify an incoming notification. 10 * @example 11 * # Schedule the notification that will trigger once, in one minute from now 12 * ```ts 13 * import * as Notifications from 'expo-notifications'; 14 * 15 * Notifications.scheduleNotificationAsync({ 16 * content: { 17 * title: "Time's up!", 18 * body: 'Change sides!', 19 * }, 20 * trigger: { 21 * seconds: 60, 22 * }, 23 * }); 24 * ``` 25 * 26 * # Schedule the notification that will trigger repeatedly, every 20 minutes 27 * ```ts 28 * import * as Notifications from 'expo-notifications'; 29 * 30 * Notifications.scheduleNotificationAsync({ 31 * content: { 32 * title: 'Remember to drink water!', 33 * }, 34 * trigger: { 35 * seconds: 60 * 20, 36 * repeats: true, 37 * }, 38 * }); 39 * ``` 40 * 41 * # Schedule the notification that will trigger once, at the beginning of next hour 42 * ```ts 43 * import * as Notifications from 'expo-notifications'; 44 * 45 * const trigger = new Date(Date.now() + 60 * 60 * 1000); 46 * trigger.setMinutes(0); 47 * trigger.setSeconds(0); 48 * 49 * Notifications.scheduleNotificationAsync({ 50 * content: { 51 * title: 'Happy new hour!', 52 * }, 53 * trigger, 54 * }); 55 * ``` 56 * @header schedule 57 */ 58export default async function scheduleNotificationAsync(request) { 59 if (!NotificationScheduler.scheduleNotificationAsync) { 60 throw new UnavailabilityError('Notifications', 'scheduleNotificationAsync'); 61 } 62 return await NotificationScheduler.scheduleNotificationAsync(request.identifier ?? uuid.v4(), request.content, parseTrigger(request.trigger)); 63} 64const DAILY_TRIGGER_EXPECTED_DATE_COMPONENTS = [ 65 'hour', 66 'minute', 67]; 68const WEEKLY_TRIGGER_EXPECTED_DATE_COMPONENTS = [ 69 'weekday', 70 'hour', 71 'minute', 72]; 73const YEARLY_TRIGGER_EXPECTED_DATE_COMPONENTS = [ 74 'day', 75 'month', 76 'hour', 77 'minute', 78]; 79export function parseTrigger(userFacingTrigger) { 80 if (userFacingTrigger === null) { 81 return null; 82 } 83 if (userFacingTrigger === undefined) { 84 throw new TypeError('Encountered an `undefined` notification trigger. If you want to trigger the notification immediately, pass in an explicit `null` value.'); 85 } 86 if (isDateTrigger(userFacingTrigger)) { 87 return parseDateTrigger(userFacingTrigger); 88 } 89 else if (isDailyTriggerInput(userFacingTrigger)) { 90 validateDateComponentsInTrigger(userFacingTrigger, DAILY_TRIGGER_EXPECTED_DATE_COMPONENTS); 91 return { 92 type: 'daily', 93 channelId: userFacingTrigger.channelId, 94 hour: userFacingTrigger.hour, 95 minute: userFacingTrigger.minute, 96 }; 97 } 98 else if (isWeeklyTriggerInput(userFacingTrigger)) { 99 validateDateComponentsInTrigger(userFacingTrigger, WEEKLY_TRIGGER_EXPECTED_DATE_COMPONENTS); 100 return { 101 type: 'weekly', 102 channelId: userFacingTrigger.channelId, 103 weekday: userFacingTrigger.weekday, 104 hour: userFacingTrigger.hour, 105 minute: userFacingTrigger.minute, 106 }; 107 } 108 else if (isYearlyTriggerInput(userFacingTrigger)) { 109 validateDateComponentsInTrigger(userFacingTrigger, YEARLY_TRIGGER_EXPECTED_DATE_COMPONENTS); 110 return { 111 type: 'yearly', 112 channelId: userFacingTrigger.channelId, 113 day: userFacingTrigger.day, 114 month: userFacingTrigger.month, 115 hour: userFacingTrigger.hour, 116 minute: userFacingTrigger.minute, 117 }; 118 } 119 else if (isSecondsPropertyMisusedInCalendarTriggerInput(userFacingTrigger)) { 120 throw new TypeError('Could not have inferred the notification trigger type: if you want to use a time interval trigger, pass in only `seconds` with or without `repeats` property; if you want to use calendar-based trigger, pass in `second`.'); 121 } 122 else if ('seconds' in userFacingTrigger) { 123 return { 124 type: 'timeInterval', 125 channelId: userFacingTrigger.channelId, 126 seconds: userFacingTrigger.seconds, 127 repeats: userFacingTrigger.repeats ?? false, 128 }; 129 } 130 else if (isCalendarTrigger(userFacingTrigger)) { 131 const { repeats, ...calendarTrigger } = userFacingTrigger; 132 return { type: 'calendar', value: calendarTrigger, repeats }; 133 } 134 else { 135 return Platform.select({ 136 default: null, 137 android: { type: 'channel', channelId: userFacingTrigger.channelId }, 138 }); 139 } 140} 141function isCalendarTrigger(trigger) { 142 const { channelId, ...triggerWithoutChannelId } = trigger; 143 return Object.keys(triggerWithoutChannelId).length > 0; 144} 145function isDateTrigger(trigger) { 146 return (trigger instanceof Date || 147 typeof trigger === 'number' || 148 (typeof trigger === 'object' && 'date' in trigger)); 149} 150function parseDateTrigger(trigger) { 151 if (trigger instanceof Date || typeof trigger === 'number') { 152 return { type: 'date', timestamp: toTimestamp(trigger) }; 153 } 154 return { type: 'date', timestamp: toTimestamp(trigger.date), channelId: trigger.channelId }; 155} 156function toTimestamp(date) { 157 if (date instanceof Date) { 158 return date.getTime(); 159 } 160 return date; 161} 162function isDailyTriggerInput(trigger) { 163 if (typeof trigger !== 'object') 164 return false; 165 const { channelId, ...triggerWithoutChannelId } = trigger; 166 return (Object.keys(triggerWithoutChannelId).length === 167 DAILY_TRIGGER_EXPECTED_DATE_COMPONENTS.length + 1 && 168 DAILY_TRIGGER_EXPECTED_DATE_COMPONENTS.every((component) => component in triggerWithoutChannelId) && 169 'repeats' in triggerWithoutChannelId && 170 triggerWithoutChannelId.repeats === true); 171} 172function isWeeklyTriggerInput(trigger) { 173 if (typeof trigger !== 'object') 174 return false; 175 const { channelId, ...triggerWithoutChannelId } = trigger; 176 return (Object.keys(triggerWithoutChannelId).length === 177 WEEKLY_TRIGGER_EXPECTED_DATE_COMPONENTS.length + 1 && 178 WEEKLY_TRIGGER_EXPECTED_DATE_COMPONENTS.every((component) => component in triggerWithoutChannelId) && 179 'repeats' in triggerWithoutChannelId && 180 triggerWithoutChannelId.repeats === true); 181} 182function isYearlyTriggerInput(trigger) { 183 if (typeof trigger !== 'object') 184 return false; 185 const { channelId, ...triggerWithoutChannelId } = trigger; 186 return (Object.keys(triggerWithoutChannelId).length === 187 YEARLY_TRIGGER_EXPECTED_DATE_COMPONENTS.length + 1 && 188 YEARLY_TRIGGER_EXPECTED_DATE_COMPONENTS.every((component) => component in triggerWithoutChannelId) && 189 'repeats' in triggerWithoutChannelId && 190 triggerWithoutChannelId.repeats === true); 191} 192function isSecondsPropertyMisusedInCalendarTriggerInput(trigger) { 193 const { channelId, ...triggerWithoutChannelId } = trigger; 194 return ( 195 // eg. { seconds: ..., repeats: ..., hour: ... } 196 ('seconds' in triggerWithoutChannelId && 197 'repeats' in triggerWithoutChannelId && 198 Object.keys(triggerWithoutChannelId).length > 2) || 199 // eg. { seconds: ..., hour: ... } 200 ('seconds' in triggerWithoutChannelId && 201 !('repeats' in triggerWithoutChannelId) && 202 Object.keys(triggerWithoutChannelId).length > 1)); 203} 204function validateDateComponentsInTrigger(trigger, components) { 205 const anyTriggerType = trigger; 206 components.forEach((component) => { 207 if (!(component in anyTriggerType)) { 208 throw new TypeError(`The ${component} parameter needs to be present`); 209 } 210 if (typeof anyTriggerType[component] !== 'number') { 211 throw new TypeError(`The ${component} parameter should be a number`); 212 } 213 switch (component) { 214 case 'month': { 215 const { month } = anyTriggerType; 216 if (month < 0 || month > 11) { 217 throw new RangeError(`The month parameter needs to be between 0 and 11. Found: ${month}`); 218 } 219 break; 220 } 221 case 'day': { 222 const { day, month } = anyTriggerType; 223 const daysInGivenMonth = daysInMonth(month); 224 if (day < 1 || day > daysInGivenMonth) { 225 throw new RangeError(`The day parameter for month ${month} must be between 1 and ${daysInGivenMonth}. Found: ${day}`); 226 } 227 break; 228 } 229 case 'weekday': { 230 const { weekday } = anyTriggerType; 231 if (weekday < 1 || weekday > 7) { 232 throw new RangeError(`The weekday parameter needs to be between 1 and 7. Found: ${weekday}`); 233 } 234 break; 235 } 236 case 'hour': { 237 const { hour } = anyTriggerType; 238 if (hour < 0 || hour > 23) { 239 throw new RangeError(`The hour parameter needs to be between 0 and 23. Found: ${hour}`); 240 } 241 break; 242 } 243 case 'minute': { 244 const { minute } = anyTriggerType; 245 if (minute < 0 || minute > 59) { 246 throw new RangeError(`The minute parameter needs to be between 0 and 59. Found: ${minute}`); 247 } 248 break; 249 } 250 } 251 }); 252} 253/** 254 * Determines the number of days in the given month (or January if omitted). 255 * If year is specified, it will include leap year logic, else it will always assume a leap year 256 */ 257function daysInMonth(month = 0, year) { 258 return new Date(year ?? 2000, month + 1, 0).getDate(); 259} 260//# sourceMappingURL=scheduleNotificationAsync.js.map