Skip to content

Calendar

Add a meeting to the user’s calendar, list what is on this week, set a reminder, or hand the user the system’s own event dialog. @symbiote-native/calendar wraps expo-calendar so every SymbioteNative adapter can reach the device calendar, not just React.

Both of upstream’s API surfaces are ported, matching its own layout: the modern class API (ExpoCalendar, ExpoCalendarEvent, ExpoCalendarReminder, ExpoCalendarAttendee; the default entry) and the legacy function-based API (the /legacy subpath). The classes and functions are shared by every adapter. The two permission hooks, useCalendarPermissions and useRemindersPermissions, are ported to all five in each framework’s own idiom.

OS platform Support
iOS live
Android live
Framework adapter Support
React live
Vue live
Angular live
Svelte live
Solid live
Terminal window
npm install @symbiote-native/calendar

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --calendar (or add --calendar in an existing app) installs and wires this for you - see @symbiote-native/cli.

expo-calendar and expo-modules-core come along as regular dependencies, pinned to exact versions. Never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

The package adds these to your native projects on install (nothing overwrites a value you set):

Platform Added Why
iOS NSCalendarsUsageDescription, NSCalendarsFullAccessUsageDescription Calendar permission prompt text. Reword to fit
iOS NSRemindersUsageDescription, NSRemindersFullAccessUsageDescription Reminders permission prompt text. Reword to fit
Android READ_CALENDAR, WRITE_CALENDAR Reading and editing calendars

Ask for permission, then create an event. The classes work the same on every adapter:

import { ExpoCalendar, requestCalendarPermissions } from '@symbiote-native/calendar';
const { granted } = await requestCalendarPermissions();
if (granted) {
const calendar = await ExpoCalendar.get(calendarId);
const event = await calendar.createEvent({
title: 'Standup',
startDate: new Date(),
endDate: new Date(Date.now() + 30 * 60_000),
});
await event.update({ notes: 'Daily sync' });
}

List events across calendars for a week:

import { getCalendars, listEvents } from '@symbiote-native/calendar';
const calendars = await getCalendars();
const events = await listEvents(calendars, new Date(), new Date(Date.now() + 7 * 24 * 3600_000));

To render the permission state, use the adapter’s own binding. Each is shown for calendars; useRemindersPermissions has the same shape.

import { useCalendarPermissions } from '@symbiote-native/calendar/react';
export default function CalendarGate() {
const [status, requestPermission] = useCalendarPermissions();
if (!status?.granted) {
return <button title="Allow calendar" onPress={() => requestPermission()} />;
}
return <text>Calendar allowed</text>;
}

The pre-class API is kept at /legacy for code that already uses it:

import { createEventAsync, requestCalendarPermissionsAsync } from '@symbiote-native/calendar/legacy';
await requestCalendarPermissionsAsync();
const eventId = await createEventAsync(calendarId, {
title: 'Standup',
startDate: new Date(),
endDate: new Date(Date.now() + 30 * 60_000),
});
Signature Description
getCalendars(entityType?): Promise<ExpoCalendar[]> Lists calendars, optionally only those for events or reminders
createCalendar(details?): Promise<ExpoCalendar> Creates a calendar
listEvents(calendars, startDate, endDate): Promise<ExpoCalendarEvent[]> Lists events across several calendars (ids or objects) in a date range
getDefaultCalendarSync(): ExpoCalendar iOS only. The default calendar. Throws UnavailabilityError on Android
presentPicker(): Promise<ExpoCalendar | null> iOS only. Opens the system calendar chooser
getSourcesSync(): ISource[] iOS only. Lists the sources calendars belong to
getCalendarPermissions() / requestCalendarPermissions(writeOnly?) Read or prompt for calendar access
getRemindersPermissions() / requestRemindersPermissions() iOS only. Read or prompt for reminders access
Class Members
ExpoCalendar ExpoCalendar.get(id), then createEvent, listEvents, addEventWithForm, update, delete; on iOS also createReminder, listReminders
ExpoCalendarEvent ExpoCalendarEvent.get(id), then update, delete, getOccurrenceSync, getAttendees, openInCalendar, editInCalendar; on Android also createAttendee
ExpoCalendarReminder iOS only. ExpoCalendarReminder.get(id), then read and update a reminder
ExpoCalendarAttendee Android only. Read and update an event attendee

Dates are Date objects; the wrapper converts them for the native call.

Member iOS Android
ExpoCalendar create, update, delete, createEvent, listEvents, addEventWithForm yes yes
getDefaultCalendarSync, presentPicker, getSourcesSync yes no
calendar.createReminder, listReminders yes no
event.createAttendee no yes
event.getOccurrenceSync, getAttendees, update, delete yes yes
ExpoCalendarReminder (whole class) yes no (a no-op class)
ExpoCalendarAttendee (whole class) no (a no-op class) yes
Reminders permissions functions yes no

An iOS-only or Android-only member throws UnavailabilityError on the other platform, or is absent from the native object. Check before calling if the call site must run on both.

Adapter useCalendarPermissions / useRemindersPermissions
React [status, requestPermission, getPermission]
Vue [status, requestPermission, getPermission], with status a ref
Solid [status, requestPermission, getPermission], with status an accessor
Svelte { status, requestPermission, getPermission }
Angular CalendarPermissionsService / RemindersPermissionsService
  • Request permission first. Reads and writes fail without it. Reminders have their own permission on iOS and do not exist on Android.
  • Check availability before calling a platform-only member. The table above shows what throws where.
  • The legacy requestPermissionsAsync is the deprecated alias upstream ships. It warns and calls requestCalendarPermissionsAsync.
  • Nothing from the public surface is left out. Both the modern and the legacy surface are ported.
  • Calendars can only be verified on a device or simulator. The headless tests fake the native module.

getCalendars() or createEvent fails or returns nothing. Request calendar permission first with requestCalendarPermissions() and check granted. On Android that means READ_CALENDAR and WRITE_CALENDAR, which this package adds for you.

The system event dialog does not appear on iOS and there is no error. On iOS the native code checks permission before showing the system dialog, so without it nothing opens. Ask for permission before calling addEventWithForm or presenting the picker.

How do I get the default calendar? getDefaultCalendarSync() exists on iOS only; Android has no single system default. On Android, list calendars with getCalendars() and pick one (often the one the user owns), or let the user choose.

Reminders do not work on Android. Reminders are an iOS feature in this API. ExpoCalendarReminder and the reminders permission functions are iOS only.

I only need to add an event, not read the calendar. On iOS 17 and later you can request write-only access (requestCalendarPermissions(true)). The linker cannot swap the Info.plist string for you; see the write-only note near Install.

Sources: Expo docs: Calendar, expo/expo#36001 system calendar dialog requires permission on iOS, expo/expo#4991 getCalendarsAsync error on Android, expo/expo#48186 getDefaultCalendarSync and getCalendars requiring full access.

expo-calendar’s JS is hand-ported into this package: core/ for the modern class API, legacy/ for the function API. Every class extends its native shared object and overrides only what upstream overrides (date stringification, a platform guard, upgrading a returned native instance in place, since Android’s Kotlin throws on direct construction). ./react, ./vue, ./svelte and ./solid alias core/ plus their own permission hooks. The native code is never vendored: expo-modules-autolinking resolves it from node_modules (see the native setup guide).