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 |
Installation
Section titled “Installation”npm install @symbiote-native/calendarScaffolding 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));Show permission state
Section titled “Show permission state”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>;}<script setup lang="ts">import { useCalendarPermissions } from '@symbiote-native/calendar/vue';
const [status, requestPermission] = useCalendarPermissions();</script>
<template> <text v-if="status?.granted">Calendar allowed</text> <button v-else title="Allow calendar" @press="requestPermission()" /></template>import { Component, inject } from '@angular/core';import { SYMBIOTE_ELEMENTS } from '@symbiote-native/angular';import { CalendarPermissionsService } from '@symbiote-native/calendar/angular';
@Component({ standalone: true, imports: [SYMBIOTE_ELEMENTS], template: ` @if (calendar()?.granted) { <text>Calendar allowed</text> } @else { <button title="Allow calendar" (press)="request()" /> } `,})export class CalendarGate { private readonly service = inject(CalendarPermissionsService); readonly calendar = this.service.connect();
request(): void { void this.service.request(); }}connect() returns a signal of the current status; get() and request() are imperative.
<script lang="ts"> import { useCalendarPermissions } from '@symbiote-native/calendar/svelte';
const calendar = useCalendarPermissions();</script>
{#if calendar.status?.granted} <text>Calendar allowed</text>{:else} <button title="Allow calendar" onPress={() => calendar.requestPermission()} />{/if}Svelte returns { status, requestPermission, getPermission } instead of a tuple, matching this
repo’s rune idiom.
import { useCalendarPermissions } from '@symbiote-native/calendar/solid';
export function CalendarGate() { const [status, requestPermission] = useCalendarPermissions();
return status()?.granted ? ( <text>Calendar allowed</text> ) : ( <button title="Allow calendar" onPress={() => requestPermission()} /> );}The legacy function API
Section titled “The legacy function API”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),});Module-level functions
Section titled “Module-level functions”| 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 |
Classes
Section titled “Classes”| 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.
Platform availability
Section titled “Platform availability”| 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.
Permission hooks
Section titled “Permission hooks”| 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
requestPermissionsAsyncis the deprecated alias upstream ships. It warns and callsrequestCalendarPermissionsAsync. - 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.
Common questions
Section titled “Common questions”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.
How the wrapper works
Section titled “How the wrapper works”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).