UNPKG

21 kBPlain TextView Raw
1import { UnavailabilityError } from '@unimodules/core';
2import { Platform, processColor } from 'react-native';
3import { PermissionResponse, PermissionStatus } from 'unimodules-permissions-interface';
4
5import ExpoCalendar from './ExpoCalendar';
6
7export type RecurringEventOptions = {
8 futureEvents?: boolean;
9 instanceStartDate?: string | Date;
10}; // iOS
11
12export interface Calendar {
13 id: string;
14 title: string;
15 sourceId?: string; // iOS
16 source: Source;
17 type?: string; // iOS
18 color: string;
19 entityType?: string; // iOS
20 allowsModifications: boolean;
21 allowedAvailabilities: string[];
22 isPrimary?: boolean; // Android
23 name?: string | null; // Android
24 ownerAccount?: string; // Android
25 timeZone?: string; // Android
26 allowedReminders?: string[]; // Android
27 allowedAttendeeTypes?: string[]; // Android
28 isVisible?: boolean; // Android
29 isSynced?: boolean; // Android
30 accessLevel?: string; // Android
31}
32
33export type Source = {
34 id?: string; // iOS only ??
35 type: string;
36 name: string;
37 isLocalAccount?: boolean; // Android
38};
39
40export type Event = {
41 id: string;
42 calendarId: string;
43 title: string;
44 location: string;
45 creationDate?: string | Date; // iOS
46 lastModifiedDate?: string | Date; // iOS
47 timeZone: string;
48 endTimeZone?: string; // Android
49 url?: string; // iOS
50 notes: string;
51 alarms: Alarm[];
52 recurrenceRule: RecurrenceRule;
53 startDate: string | Date;
54 endDate: string | Date;
55 originalStartDate?: string | Date; // iOS
56 isDetached?: boolean; // iOS
57 allDay: boolean;
58 availability: string; // Availability
59 status: string; // Status
60 organizer?: string; // Organizer - iOS
61 organizerEmail?: string; // Android
62 accessLevel?: string; // Android,
63 guestsCanModify?: boolean; // Android,
64 guestsCanInviteOthers?: boolean; // Android
65 guestsCanSeeGuests?: boolean; // Android
66 originalId?: string; // Android
67 instanceId?: string; // Android
68};
69
70export interface Reminder {
71 id?: string;
72 calendarId?: string;
73 title?: string;
74 location?: string;
75 creationDate?: string | Date;
76 lastModifiedDate?: string | Date;
77 timeZone?: string;
78 url?: string;
79 notes?: string;
80 alarms?: Alarm[];
81 recurrenceRule?: RecurrenceRule;
82 startDate?: string | Date;
83 dueDate?: string | Date;
84 completed?: boolean;
85 completionDate?: string | Date;
86}
87
88export type Attendee = {
89 id?: string; // Android
90 isCurrentUser?: boolean; // iOS
91 name: string;
92 role: string;
93 status: string;
94 type: string;
95 url?: string; // iOS
96 email?: string; // Android
97};
98
99export type Alarm = {
100 absoluteDate?: string; // iOS
101 relativeOffset?: number;
102 structuredLocation?: {
103 // iOS
104 title?: string;
105 proximity?: string; // Proximity
106 radius?: number;
107 coords?: {
108 latitude?: number;
109 longitude?: number;
110 };
111 };
112 method?: string; // Method, Android
113};
114
115export enum DayOfTheWeek {
116 Sunday = 1,
117 Monday = 2,
118 Tuesday = 3,
119 Wednesday = 4,
120 Thursday = 5,
121 Friday = 6,
122 Saturday = 7,
123}
124
125export enum MonthOfTheYear {
126 January = 1,
127 February = 2,
128 March = 3,
129 April = 4,
130 May = 5,
131 June = 6,
132 July = 7,
133 August = 8,
134 September = 9,
135 October = 10,
136 November = 11,
137 December = 12,
138}
139
140export type RecurrenceRule = {
141 frequency: string; // Frequency
142 interval?: number;
143 endDate?: string | Date;
144 occurrence?: number;
145
146 daysOfTheWeek?: { dayOfTheWeek: DayOfTheWeek; weekNumber?: number }[];
147 daysOfTheMonth?: number[];
148 monthsOfTheYear?: MonthOfTheYear[];
149 weeksOfTheYear?: number[];
150 daysOfTheYear?: number[];
151 setPositions?: number[];
152};
153
154export { PermissionResponse, PermissionStatus };
155
156type OptionalKeys<T> = {
157 [P in keyof T]?: T[P] | null;
158};
159
160/**
161 * Returns whether the Calendar API is enabled on the current device. This does not check the app permissions.
162 *
163 * @returns Async `boolean`, indicating whether the Calendar API is available on the current device. Currently this resolves `true` on iOS and Android only.
164 */
165export async function isAvailableAsync(): Promise<boolean> {
166 return !!ExpoCalendar.getCalendarsAsync;
167}
168
169export async function getCalendarsAsync(entityType?: string): Promise<Calendar[]> {
170 if (!ExpoCalendar.getCalendarsAsync) {
171 throw new UnavailabilityError('Calendar', 'getCalendarsAsync');
172 }
173 if (!entityType) {
174 return ExpoCalendar.getCalendarsAsync(null);
175 }
176 return ExpoCalendar.getCalendarsAsync(entityType);
177}
178
179export async function createCalendarAsync(details: OptionalKeys<Calendar> = {}): Promise<string> {
180 if (!ExpoCalendar.saveCalendarAsync) {
181 throw new UnavailabilityError('Calendar', 'createCalendarAsync');
182 }
183 const color = details.color ? processColor(details.color) : undefined;
184 const newDetails = { ...details, id: undefined, color };
185 return ExpoCalendar.saveCalendarAsync(newDetails);
186}
187
188export async function updateCalendarAsync(
189 id: string,
190 details: OptionalKeys<Calendar> = {}
191): Promise<string> {
192 if (!ExpoCalendar.saveCalendarAsync) {
193 throw new UnavailabilityError('Calendar', 'updateCalendarAsync');
194 }
195 if (!id) {
196 throw new Error(
197 'updateCalendarAsync must be called with an id (string) of the target calendar'
198 );
199 }
200 const color = details.color ? processColor(details.color) : undefined;
201
202 if (Platform.OS === 'android') {
203 if (
204 details.hasOwnProperty('source') ||
205 details.hasOwnProperty('color') ||
206 details.hasOwnProperty('allowsModifications') ||
207 details.hasOwnProperty('allowedAvailabilities') ||
208 details.hasOwnProperty('isPrimary') ||
209 details.hasOwnProperty('ownerAccount') ||
210 details.hasOwnProperty('timeZone') ||
211 details.hasOwnProperty('allowedReminders') ||
212 details.hasOwnProperty('allowedAttendeeTypes') ||
213 details.hasOwnProperty('accessLevel')
214 ) {
215 console.warn(
216 'updateCalendarAsync was called with one or more read-only properties, which will not be updated'
217 );
218 }
219 } else {
220 if (
221 details.hasOwnProperty('source') ||
222 details.hasOwnProperty('type') ||
223 details.hasOwnProperty('entityType') ||
224 details.hasOwnProperty('allowsModifications') ||
225 details.hasOwnProperty('allowedAvailabilities')
226 ) {
227 console.warn(
228 'updateCalendarAsync was called with one or more read-only properties, which will not be updated'
229 );
230 }
231 }
232
233 const newDetails = { ...details, id, color };
234 return ExpoCalendar.saveCalendarAsync(newDetails);
235}
236
237export async function deleteCalendarAsync(id: string): Promise<void> {
238 if (!ExpoCalendar.deleteCalendarAsync) {
239 throw new UnavailabilityError('Calendar', 'deleteCalendarAsync');
240 }
241 if (!id) {
242 throw new Error(
243 'deleteCalendarAsync must be called with an id (string) of the target calendar'
244 );
245 }
246 return ExpoCalendar.deleteCalendarAsync(id);
247}
248
249export async function getEventsAsync(
250 calendarIds: string[],
251 startDate: Date,
252 endDate: Date
253): Promise<Event[]> {
254 if (!ExpoCalendar.getEventsAsync) {
255 throw new UnavailabilityError('Calendar', 'getEventsAsync');
256 }
257 if (!startDate) {
258 throw new Error('getEventsAsync must be called with a startDate (date) to search for events');
259 }
260 if (!endDate) {
261 throw new Error('getEventsAsync must be called with an endDate (date) to search for events');
262 }
263 if (!calendarIds || !calendarIds.length) {
264 throw new Error(
265 'getEventsAsync must be called with a non-empty array of calendarIds to search'
266 );
267 }
268 return ExpoCalendar.getEventsAsync(
269 stringifyIfDate(startDate),
270 stringifyIfDate(endDate),
271 calendarIds
272 );
273}
274
275export async function getEventAsync(
276 id: string,
277 { futureEvents = false, instanceStartDate }: RecurringEventOptions = {}
278): Promise<Event> {
279 if (!ExpoCalendar.getEventByIdAsync) {
280 throw new UnavailabilityError('Calendar', 'getEventAsync');
281 }
282 if (!id) {
283 throw new Error('getEventAsync must be called with an id (string) of the target event');
284 }
285 if (Platform.OS === 'ios') {
286 return ExpoCalendar.getEventByIdAsync(id, instanceStartDate);
287 } else {
288 return ExpoCalendar.getEventByIdAsync(id);
289 }
290}
291
292export async function createEventAsync(
293 calendarId: string,
294 { id, ...details }: OptionalKeys<Event> = {}
295): Promise<string> {
296 if (!ExpoCalendar.saveEventAsync) {
297 throw new UnavailabilityError('Calendar', 'createEventAsync');
298 }
299 if (!calendarId) {
300 throw new Error('createEventAsync must be called with an id (string) of the target calendar');
301 }
302
303 if (Platform.OS === 'android') {
304 if (!details.startDate) {
305 throw new Error('createEventAsync requires a startDate (Date)');
306 }
307 if (!details.endDate) {
308 throw new Error('createEventAsync requires an endDate (Date)');
309 }
310 }
311
312 const newDetails = {
313 ...details,
314 calendarId,
315 };
316
317 return ExpoCalendar.saveEventAsync(stringifyDateValues(newDetails), {});
318}
319
320export async function updateEventAsync(
321 id: string,
322 details: OptionalKeys<Event> = {},
323 { futureEvents = false, instanceStartDate }: RecurringEventOptions = {}
324): Promise<string> {
325 if (!ExpoCalendar.saveEventAsync) {
326 throw new UnavailabilityError('Calendar', 'updateEventAsync');
327 }
328 if (!id) {
329 throw new Error('updateEventAsync must be called with an id (string) of the target event');
330 }
331
332 if (Platform.OS === 'ios') {
333 if (
334 details.hasOwnProperty('creationDate') ||
335 details.hasOwnProperty('lastModifiedDate') ||
336 details.hasOwnProperty('originalStartDate') ||
337 details.hasOwnProperty('isDetached') ||
338 details.hasOwnProperty('status') ||
339 details.hasOwnProperty('organizer')
340 ) {
341 console.warn(
342 'updateEventAsync was called with one or more read-only properties, which will not be updated'
343 );
344 }
345 }
346
347 const newDetails = { ...details, id, instanceStartDate };
348 return ExpoCalendar.saveEventAsync(stringifyDateValues(newDetails), { futureEvents });
349}
350
351export async function deleteEventAsync(
352 id: string,
353 { futureEvents = false, instanceStartDate }: RecurringEventOptions = {}
354): Promise<void> {
355 if (!ExpoCalendar.deleteEventAsync) {
356 throw new UnavailabilityError('Calendar', 'deleteEventAsync');
357 }
358 if (!id) {
359 throw new Error('deleteEventAsync must be called with an id (string) of the target event');
360 }
361 return ExpoCalendar.deleteEventAsync({ id, instanceStartDate }, { futureEvents });
362}
363
364export async function getAttendeesForEventAsync(
365 id: string,
366 { futureEvents = false, instanceStartDate }: RecurringEventOptions = {}
367): Promise<Attendee[]> {
368 if (!ExpoCalendar.getAttendeesForEventAsync) {
369 throw new UnavailabilityError('Calendar', 'getAttendeesForEventAsync');
370 }
371 if (!id) {
372 throw new Error(
373 'getAttendeesForEventAsync must be called with an id (string) of the target event'
374 );
375 }
376 // Android only takes an ID, iOS takes an object
377 const params = Platform.OS === 'ios' ? { id, instanceStartDate } : id;
378 return ExpoCalendar.getAttendeesForEventAsync(params);
379}
380
381export async function createAttendeeAsync(
382 eventId: string,
383 details: OptionalKeys<Attendee> = {}
384): Promise<string> {
385 if (!ExpoCalendar.saveAttendeeForEventAsync) {
386 throw new UnavailabilityError('Calendar', 'createAttendeeAsync');
387 }
388 if (!eventId) {
389 throw new Error('createAttendeeAsync must be called with an id (string) of the target event');
390 }
391 if (!details.email) {
392 throw new Error('createAttendeeAsync requires an email (string)');
393 }
394 if (!details.role) {
395 throw new Error('createAttendeeAsync requires a role (string)');
396 }
397 if (!details.type) {
398 throw new Error('createAttendeeAsync requires a type (string)');
399 }
400 if (!details.status) {
401 throw new Error('createAttendeeAsync requires a status (string)');
402 }
403 const newDetails = { ...details, id: undefined };
404 return ExpoCalendar.saveAttendeeForEventAsync(newDetails, eventId);
405} // Android
406
407export async function updateAttendeeAsync(
408 id: string,
409 details: OptionalKeys<Attendee> = {}
410): Promise<string> {
411 if (!ExpoCalendar.saveAttendeeForEventAsync) {
412 throw new UnavailabilityError('Calendar', 'updateAttendeeAsync');
413 }
414 if (!id) {
415 throw new Error('updateAttendeeAsync must be called with an id (string) of the target event');
416 }
417 const newDetails = { ...details, id };
418 return ExpoCalendar.saveAttendeeForEventAsync(newDetails, null);
419} // Android
420
421export async function getDefaultCalendarAsync(): Promise<Calendar> {
422 if (!ExpoCalendar.getDefaultCalendarAsync) {
423 throw new UnavailabilityError('Calendar', 'getDefaultCalendarAsync');
424 }
425 return ExpoCalendar.getDefaultCalendarAsync();
426} // iOS
427
428export async function deleteAttendeeAsync(id: string): Promise<void> {
429 if (!ExpoCalendar.deleteAttendeeAsync) {
430 throw new UnavailabilityError('Calendar', 'deleteAttendeeAsync');
431 }
432 if (!id) {
433 throw new Error('deleteAttendeeAsync must be called with an id (string) of the target event');
434 }
435 return ExpoCalendar.deleteAttendeeAsync(id);
436} // Android
437
438export async function getRemindersAsync(
439 calendarIds: (string | null)[],
440 status: string | null,
441 startDate: Date,
442 endDate: Date
443): Promise<Reminder[]> {
444 if (!ExpoCalendar.getRemindersAsync) {
445 throw new UnavailabilityError('Calendar', 'getRemindersAsync');
446 }
447 if (status && !startDate) {
448 throw new Error(
449 'getRemindersAsync must be called with a startDate (date) to search for reminders'
450 );
451 }
452 if (status && !endDate) {
453 throw new Error(
454 'getRemindersAsync must be called with an endDate (date) to search for reminders'
455 );
456 }
457 if (!calendarIds || !calendarIds.length) {
458 throw new Error(
459 'getRemindersAsync must be called with a non-empty array of calendarIds to search'
460 );
461 }
462 return ExpoCalendar.getRemindersAsync(
463 stringifyIfDate(startDate) || null,
464 stringifyIfDate(endDate) || null,
465 calendarIds,
466 status || null
467 );
468} // iOS
469
470export async function getReminderAsync(id: string): Promise<Reminder> {
471 if (!ExpoCalendar.getReminderByIdAsync) {
472 throw new UnavailabilityError('Calendar', 'getReminderAsync');
473 }
474 if (!id) {
475 throw new Error('getReminderAsync must be called with an id (string) of the target reminder');
476 }
477 return ExpoCalendar.getReminderByIdAsync(id);
478} // iOS
479
480export async function createReminderAsync(
481 calendarId: string | null,
482 { id, ...details }: Reminder = {}
483): Promise<string> {
484 if (!ExpoCalendar.saveReminderAsync) {
485 throw new UnavailabilityError('Calendar', 'createReminderAsync');
486 }
487
488 const newDetails = {
489 ...details,
490 calendarId: calendarId === null ? undefined : calendarId,
491 };
492 return ExpoCalendar.saveReminderAsync(stringifyDateValues(newDetails));
493} // iOS
494
495export async function updateReminderAsync(id: string, details: Reminder = {}): Promise<string> {
496 if (!ExpoCalendar.saveReminderAsync) {
497 throw new UnavailabilityError('Calendar', 'updateReminderAsync');
498 }
499 if (!id) {
500 throw new Error(
501 'updateReminderAsync must be called with an id (string) of the target reminder'
502 );
503 }
504
505 if (details.hasOwnProperty('creationDate') || details.hasOwnProperty('lastModifiedDate')) {
506 console.warn(
507 'updateReminderAsync was called with one or more read-only properties, which will not be updated'
508 );
509 }
510
511 const newDetails = { ...details, id };
512 return ExpoCalendar.saveReminderAsync(stringifyDateValues(newDetails));
513} // iOS
514
515export async function deleteReminderAsync(id: string): Promise<void> {
516 if (!ExpoCalendar.deleteReminderAsync) {
517 throw new UnavailabilityError('Calendar', 'deleteReminderAsync');
518 }
519 if (!id) {
520 throw new Error(
521 'deleteReminderAsync must be called with an id (string) of the target reminder'
522 );
523 }
524 return ExpoCalendar.deleteReminderAsync(id);
525} // iOS
526
527export async function getSourcesAsync(): Promise<Source[]> {
528 if (!ExpoCalendar.getSourcesAsync) {
529 throw new UnavailabilityError('Calendar', 'getSourcesAsync');
530 }
531 return ExpoCalendar.getSourcesAsync();
532} // iOS
533
534export async function getSourceAsync(id: string): Promise<Source> {
535 if (!ExpoCalendar.getSourceByIdAsync) {
536 throw new UnavailabilityError('Calendar', 'getSourceAsync');
537 }
538 if (!id) {
539 throw new Error('getSourceAsync must be called with an id (string) of the target source');
540 }
541 return ExpoCalendar.getSourceByIdAsync(id);
542} // iOS
543
544export function openEventInCalendar(id: string): void {
545 if (!ExpoCalendar.openEventInCalendar) {
546 console.warn(`openEventInCalendar is not available on platform: ${Platform.OS}`);
547 return;
548 }
549 if (!id) {
550 throw new Error('openEventInCalendar must be called with an id (string) of the target event');
551 }
552 return ExpoCalendar.openEventInCalendar(parseInt(id, 10));
553} // Android
554
555/**
556 * @deprecated Use `requestCalendarPermissionsAsync()` instead
557 */
558export async function requestPermissionsAsync(): Promise<PermissionResponse> {
559 console.warn(
560 'requestPermissionsAsync is deprecated. Use requestCalendarPermissionsAsync instead.'
561 );
562 return requestCalendarPermissionsAsync();
563}
564
565export async function getCalendarPermissionsAsync(): Promise<PermissionResponse> {
566 if (!ExpoCalendar.getCalendarPermissionsAsync) {
567 throw new UnavailabilityError('Calendar', 'getCalendarPermissionsAsync');
568 }
569 return ExpoCalendar.getCalendarPermissionsAsync();
570}
571
572export async function getRemindersPermissionsAsync(): Promise<PermissionResponse> {
573 if (!ExpoCalendar.getRemindersPermissionsAsync) {
574 throw new UnavailabilityError('Calendar', 'getRemindersPermissionsAsync');
575 }
576 return ExpoCalendar.getRemindersPermissionsAsync();
577}
578
579export async function requestCalendarPermissionsAsync(): Promise<PermissionResponse> {
580 if (!ExpoCalendar.requestCalendarPermissionsAsync) {
581 throw new UnavailabilityError('Calendar', 'requestCalendarPermissionsAsync');
582 }
583 return await ExpoCalendar.requestCalendarPermissionsAsync();
584}
585
586export async function requestRemindersPermissionsAsync(): Promise<PermissionResponse> {
587 if (!ExpoCalendar.requestRemindersPermissionsAsync) {
588 throw new UnavailabilityError('Calendar', 'requestRemindersPermissionsAsync');
589 }
590 return await ExpoCalendar.requestRemindersPermissionsAsync();
591}
592
593export const EntityTypes = {
594 EVENT: 'event',
595 REMINDER: 'reminder',
596};
597
598export const Frequency = {
599 DAILY: 'daily',
600 WEEKLY: 'weekly',
601 MONTHLY: 'monthly',
602 YEARLY: 'yearly',
603};
604
605export const Availability = {
606 NOT_SUPPORTED: 'notSupported', // iOS
607 BUSY: 'busy',
608 FREE: 'free',
609 TENTATIVE: 'tentative',
610 UNAVAILABLE: 'unavailable', // iOS
611};
612
613export const CalendarType = {
614 LOCAL: 'local',
615 CALDAV: 'caldav',
616 EXCHANGE: 'exchange',
617 SUBSCRIBED: 'subscribed',
618 BIRTHDAYS: 'birthdays',
619 UNKNOWN: 'unknown',
620}; // iOS
621
622export const EventStatus = {
623 NONE: 'none',
624 CONFIRMED: 'confirmed',
625 TENTATIVE: 'tentative',
626 CANCELED: 'canceled',
627};
628
629export const SourceType = {
630 LOCAL: 'local',
631 EXCHANGE: 'exchange',
632 CALDAV: 'caldav',
633 MOBILEME: 'mobileme',
634 SUBSCRIBED: 'subscribed',
635 BIRTHDAYS: 'birthdays',
636};
637
638export const AttendeeRole = {
639 UNKNOWN: 'unknown', // iOS
640 REQUIRED: 'required', // iOS
641 OPTIONAL: 'optional', // iOS
642 CHAIR: 'chair', // iOS
643 NON_PARTICIPANT: 'nonParticipant', // iOS
644 ATTENDEE: 'attendee', // Android
645 ORGANIZER: 'organizer', // Android
646 PERFORMER: 'performer', // Android
647 SPEAKER: 'speaker', // Android
648 NONE: 'none', // Android
649};
650
651export const AttendeeStatus = {
652 UNKNOWN: 'unknown', // iOS
653 PENDING: 'pending', // iOS
654 ACCEPTED: 'accepted',
655 DECLINED: 'declined',
656 TENTATIVE: 'tentative',
657 DELEGATED: 'delegated', // iOS
658 COMPLETED: 'completed', // iOS
659 IN_PROCESS: 'inProcess', // iOS
660 INVITED: 'invited', // Android
661 NONE: 'none', // Android
662};
663
664export const AttendeeType = {
665 UNKNOWN: 'unknown', // iOS
666 PERSON: 'person', // iOS
667 ROOM: 'room', // iOS
668 GROUP: 'group', // iOS
669 RESOURCE: 'resource',
670 OPTIONAL: 'optional', // Android
671 REQUIRED: 'required', // Android
672 NONE: 'none', // Android
673};
674
675export const AlarmMethod = {
676 ALARM: 'alarm',
677 ALERT: 'alert',
678 EMAIL: 'email',
679 SMS: 'sms',
680 DEFAULT: 'default',
681};
682
683export const EventAccessLevel = {
684 CONFIDENTIAL: 'confidential',
685 PRIVATE: 'private',
686 PUBLIC: 'public',
687 DEFAULT: 'default',
688};
689
690export const CalendarAccessLevel = {
691 CONTRIBUTOR: 'contributor',
692 EDITOR: 'editor',
693 FREEBUSY: 'freebusy',
694 OVERRIDE: 'override',
695 OWNER: 'owner',
696 READ: 'read',
697 RESPOND: 'respond',
698 ROOT: 'root',
699 NONE: 'none',
700};
701
702export const ReminderStatus = {
703 COMPLETED: 'completed',
704 INCOMPLETE: 'incomplete',
705};
706
707function stringifyIfDate(date: any): any {
708 return date instanceof Date ? date.toISOString() : date;
709}
710
711function stringifyDateValues(obj: object): object {
712 return Object.keys(obj).reduce((acc, key) => {
713 const value = obj[key];
714 if (value != null && typeof value === 'object' && !(value instanceof Date)) {
715 if (Array.isArray(value)) {
716 return { ...acc, [key]: value.map(stringifyDateValues) };
717 }
718 return { ...acc, [key]: stringifyDateValues(value) };
719 }
720 acc[key] = stringifyIfDate(value);
721 return acc;
722 }, {});
723}