July 30, 2026 — The unofficial Laracon US Day 3. Get your ticket to The Vibes
Plugin Marketplace
srwiez/nativephp-mobile-calendar logo

srwiez/nativephp-mobile-calendar

A NativePHP plugin for mobile calendar integration

MobileCalendar Plugin for NativePHP Mobile#

A comprehensive NativePHP plugin for native calendar integration on iOS (EventKit) and Android (CalendarContract).

Features#

  • Full Calendar Access: List, create, and delete calendars
  • Event Management: Create, read, update, and delete calendar events
  • Recurring Events: Support for RFC 5545 RRULE recurrence rules
  • Alarms/Reminders: Add and remove event alarms
  • Attendees: View attendees (add attendees on Android only)
  • Native Calendar App: Open the native calendar app to add, edit, or view events
  • Change Notifications: Listen for calendar changes in real-time
  • Calendar URI Helpers: Generate Google Calendar, Outlook, Yahoo, Office 365 URLs and ICS files (pure PHP)

Installation#

Copied!
# Install the package
composer require srwiez/nativephp-mobile-calendar
 
# Publish the plugins provider (first time only)
php artisan vendor:publish --tag=nativephp-plugins-provider
 
# Register the plugin
php artisan native:plugin:register srwiez/nativephp-mobile-calendar
 
# Verify registration
php artisan native:plugin:list

Permissions#

The plugin automatically configures the required permissions:

Android (AndroidManifest.xml):

  • android.permission.READ_CALENDAR
  • android.permission.WRITE_CALENDAR

iOS (Info.plist):

  • NSCalendarsUsageDescription
  • NSCalendarsFullAccessUsageDescription (iOS 17+)
  • NSCalendarsWriteOnlyAccessUsageDescription (iOS 17+)

Usage#

Check & Request Permission#

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Facades\MobileCalendar;
use SRWieZ\NativePHP\Mobile\Calendar\Enums\CalendarAccessLevel;
 
// Check current permission status (no user interaction)
$status = MobileCalendar::checkPermission();
// Returns: 'not_determined', 'denied', 'restricted', 'write_only', 'full_access'
 
// Request full access (read + write)
$result = MobileCalendar::requestAccess(CalendarAccessLevel::Full);
 
// Request write-only access (iOS 17+ only)
$result = MobileCalendar::requestAccess(CalendarAccessLevel::Write);
 
if ($result['granted']) {
// Permission granted
}

CalendarAccessLevel per Platform

CalendarAccessLevel iOS Android
Read Not applicable (mapped to Full) Requests READ_CALENDAR only
Write Requests write-only access (iOS 17+), full access on older iOS Requests WRITE_CALENDAR only
Full Requests full access (read + write) Requests both READ_CALENDAR + WRITE_CALENDAR

PermissionStatus per Platform

PermissionStatus iOS Android
NotDetermined Never prompted yet Never prompted yet
Denied User denied access User denied access
Restricted Parental controls / MDM Not used
WriteOnly iOS 17+ write-only grant Not used
FullAccess Full access granted Both permissions granted

List Calendars#

Copied!
// Get all calendars
$calendars = MobileCalendar::calendars()->get();
 
// Get only writable calendars
$calendars = MobileCalendar::calendars()->writable()->get();
 
// Get default calendar
$default = MobileCalendar::getDefaultCalendar();
 
// Calendar properties: id, name, color, accountName, isPrimary, isReadOnly, isDeletable

Create Calendar#

Copied!
$calendar = MobileCalendar::createCalendar(
title: 'My App Calendar',
color: '#FF5733'
);

Delete Calendar#

Copied!
$deleted = MobileCalendar::deleteCalendar($calendarId);

Query Events#

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Facades\MobileCalendar;
 
// Query events with fluent builder
$events = MobileCalendar::events()
->calendarId($calendarId)
->between(now(), now()->addMonth())
->search('meeting')
->limit(50)
->get();
 
// Get a single event
$event = MobileCalendar::getEvent($eventId);

Create Event#

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Data\EventData;
use SRWieZ\NativePHP\Mobile\Calendar\Data\RecurrenceRule;
use SRWieZ\NativePHP\Mobile\Calendar\Data\Alarm;
use SRWieZ\NativePHP\Mobile\Calendar\Enums\DayOfWeek;
 
// Simple event
$event = MobileCalendar::createEvent([
'title' => 'Team Meeting',
'startDate' => now()->addDay()->timestamp,
'endDate' => now()->addDay()->addHour()->timestamp,
'location' => 'Conference Room A',
]);
 
// Using fluent builder
$event = MobileCalendar::createEvent(
EventData::make('Weekly Standup')
->startsAt(now()->next(DayOfWeek::Monday)->setHour(10))
->endsAt(now()->next(DayOfWeek::Monday)->setHour(10, 30))
->at('Virtual - Zoom')
->describedAs('Team weekly sync meeting')
->inCalendar($calendarId)
->repeating(
RecurrenceRule::weekly()
->onDays(DayOfWeek::Monday)
->count(12)
)
->withAlarm(Alarm::minutesBefore(15))
->withAlarm(Alarm::hoursBefore(1))
);

Tracking Created Events#

createEvent() returns the full event object including the native calendar event ID. Store this ID in your database to retrieve or manage the event later:

Copied!
$event = MobileCalendar::createEvent(
EventData::make('Team Meeting')
->from(now()->addDay(), now()->addDay()->addHour())
->inCalendar($calendarId)
);
 
if ($event) {
// Persist the native event ID for future lookups
$user->calendarEvents()->create([
'native_event_id' => $event->id,
'title' => $event->title,
]);
 
// Later, retrieve the event from the native calendar
$nativeEvent = MobileCalendar::getEvent($event->id);
}

Update Event#

Copied!
$updated = MobileCalendar::updateEvent($eventId, [
'title' => 'Updated Title',
'location' => 'New Location',
]);
 
// Or with EventData
$updated = MobileCalendar::updateEvent($eventId,
EventData::make('Updated Meeting')
->startsAt($newStart)
->endsAt($newEnd)
);

Delete Event#

Copied!
// Delete single event
MobileCalendar::deleteEvent($eventId);
 
// Delete all occurrences of a recurring event
MobileCalendar::deleteEvent($eventId, deleteAllOccurrences: true);

Recurring Events#

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Data\RecurrenceRule;
use SRWieZ\NativePHP\Mobile\Calendar\Enums\DayOfWeek;
use SRWieZ\NativePHP\Mobile\Calendar\Enums\RecurrenceFrequency;
 
// Daily for 10 occurrences
$rule = RecurrenceRule::daily()->count(10);
 
// Weekly on Monday and Wednesday until date
$rule = RecurrenceRule::weekly()
->onDays(DayOfWeek::Monday, DayOfWeek::Wednesday)
->until(now()->addMonths(6));
 
// Monthly on the 15th
$rule = RecurrenceRule::monthly()
->onDayOfMonth(15)
->count(12);
 
// Every 2 weeks
$rule = RecurrenceRule::make(RecurrenceFrequency::Weekly)
->interval(2)
->count(26);
 
// Get instances of a recurring event
$instances = MobileCalendar::getEventInstances(
$eventId,
now(),
now()->addMonths(3)
);

Alarms#

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Data\Alarm;
use SRWieZ\NativePHP\Mobile\Calendar\Enums\AlarmMethod;
 
// Add alarm to existing event
MobileCalendar::addAlarm($eventId, Alarm::minutesBefore(30));
MobileCalendar::addAlarm($eventId, Alarm::hoursBefore(1));
MobileCalendar::addAlarm($eventId, Alarm::daysBefore(1));
 
// Alarm with specific method (Android)
MobileCalendar::addAlarm($eventId,
Alarm::minutesBefore(15)->method(AlarmMethod::Email)
);
 
// Remove alarm by index
MobileCalendar::removeAlarm($eventId, alarmIndex: 0);

Attendees#

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Data\Attendee;
use SRWieZ\NativePHP\Mobile\Calendar\Enums\AttendeeType;
 
// Get attendees
$attendees = MobileCalendar::getAttendees($eventId);
 
// Add attendee (Android only - throws UnsupportedPlatformException on iOS)
try {
MobileCalendar::addAttendee($eventId,
Attendee::make('[email protected]')
->name('John Doe')
->type(AttendeeType::Required)
);
} catch (UnsupportedPlatformException $e) {
// Use calendar URI helpers instead for cross-platform support
}

Open Native Calendar App#

Copied!
// Open to add event (uses Intent on Android - no permission needed)
MobileCalendar::openCalendarToAdd(
EventData::make('Quick Event')
->startsAt(now()->addHour())
->endsAt(now()->addHours(2))
);
 
// Open to edit existing event
MobileCalendar::openCalendarToEdit($eventId);
 
// Open calendar to view specific date
MobileCalendar::openCalendarToView(now()->addWeek());
 
// Open calendar to today
MobileCalendar::openCalendarToView();

Listen for Calendar Changes#

Copied!
// Start listening for event changes
MobileCalendar::startChangeListener(trackEvents: true);
 
// Also detect calendar create/delete (e.g. from external apps)
MobileCalendar::startChangeListener(trackEvents: true, trackCalendars: true);
 
// In your Livewire component
use SRWieZ\NativePHP\Mobile\Calendar\Events\CalendarChanged;
use SRWieZ\NativePHP\Mobile\Calendar\Events\CalendarCreated;
use SRWieZ\NativePHP\Mobile\Calendar\Events\CalendarDeleted;
 
#[OnNative(CalendarChanged::class)]
public function handleCalendarChanged(string $changeType, array $eventIds = [])
{
$this->loadEvents();
}
 
#[OnNative(CalendarCreated::class)]
public function handleCalendarCreated(string $calendarId, string $name)
{
$this->loadCalendars();
}
 
#[OnNative(CalendarDeleted::class)]
public function handleCalendarDeleted(string $calendarId)
{
$this->loadCalendars();
}
 
// Stop listening when done
MobileCalendar::stopChangeListener();

Calendar URI Helpers (Pure PHP - No Native Calls)#

Generate URLs to add events to various calendar services:

Copied!
use SRWieZ\NativePHP\Mobile\Calendar\Data\EventData;
 
$event = EventData::make('Conference Talk')
->startsAt(now()->addWeek())
->endsAt(now()->addWeek()->addHours(2))
->at('Convention Center')
->describedAs('My talk on NativePHP');
 
// Google Calendar
$url = MobileCalendar::generateGoogleCalendarUrl($event);
 
// Outlook.com
$url = MobileCalendar::generateOutlookUrl($event);
 
// Office 365
$url = MobileCalendar::generateOffice365Url($event);
 
// Yahoo Calendar
$url = MobileCalendar::generateYahooUrl($event);
 
// Webcal subscription URL
$url = MobileCalendar::generateWebcalUrl('https://example.com/calendar.ics');
 
// Generate ICS file content
$ics = MobileCalendar::generateIcsContent($event);
 
// Generate ICS for multiple events
$ics = MobileCalendar::generateIcsContent([$event1, $event2, $event3]);
 
// Generate data URI for download link
$dataUri = MobileCalendar::generateIcsDataUri($event);
// Use in HTML: <a href="{{ $dataUri }}" download="event.ics">Download</a>

Events#

The plugin dispatches Laravel events for async operations:

Event Description
CalendarAccessGranted Permission was granted
CalendarAccessDenied Permission was denied
CalendarChanged Calendar or events changed (from listener)
CalendarCreated A calendar was created (CRUD or listener detection)
CalendarDeleted A calendar was deleted (CRUD or listener detection)
EventCreated Event was created
EventUpdated Event was updated
EventDeleted Event was deleted
Copied!
use Native\Mobile\Attributes\OnNative;
use SRWieZ\NativePHP\Mobile\Calendar\Events\EventCreated;
 
#[OnNative(EventCreated::class)]
public function handleEventCreated($eventId)
{
// Handle event creation
}

JavaScript Usage#

Copied!
import { mobileCalendar } from '@srwiez/nativephp-mobile-calendar';
 
// Request permission
const { granted, status } = await mobileCalendar.requestAccess('full');
 
// Get calendars
const { calendars } = await mobileCalendar.getCalendars();
 
// Create event
const { success, event } = await mobileCalendar.createEvent({
title: 'Meeting',
startDate: Math.floor(Date.now() / 1000) + 86400,
endDate: Math.floor(Date.now() / 1000) + 90000,
location: 'Office'
});
 
// Open native calendar app
await mobileCalendar.openCalendarToAdd({
title: 'Quick Event',
startDate: Math.floor(Date.now() / 1000) + 3600,
endDate: Math.floor(Date.now() / 1000) + 7200
});

Platform Differences#

Feature iOS Android
Add Attendees Not supported (read-only) Supported (metadata only, no invitations sent)
Intent-based (no permission) N/A openCalendarToAdd
Write-only permission iOS 17+ N/A
Separate read/write permissions No (full-or-nothing) Yes (READ_CALENDAR / WRITE_CALENDAR)
Restricted status Yes (parental controls / MDM) No

Important Notes for Newcomers#

If you've never worked with native calendar APIs before, read this section carefully. Calendar integration has significant platform-level quirks that this plugin abstracts where possible, but cannot fully hide.

Permissions Are Required First#

You must request and receive permission before calling any calendar method. Without permission, methods won't throw errors — they silently return empty results or null values. Always request access on app launch or before your first calendar operation:

Copied!
$result = MobileCalendar::requestAccess(CalendarAccessLevel::Full);
 
if (! $result['granted']) {
// Don't attempt calendar operations — they will return empty/null
return;
}

On iOS, requestAccess() triggers the system permission dialog. On Android, it only checks whether permissions have already been granted by the OS — you must handle the actual permission request through NativePHP's permission system.

Timestamps Are Always Unix Seconds#

All dates in the plugin API use Unix timestamps in seconds (not milliseconds). The plugin handles the conversion to iOS's TimeInterval and Android's millisecond-based ContentProvider internally.

Copied!
// Correct: Carbon timestamps work directly
$event = EventData::make('Meeting')
->from(now()->addDay(), now()->addDay()->addHour());
 
// Also correct: raw Unix timestamp in seconds
$event = MobileCalendar::createEvent([
'title' => 'Meeting',
'startDate' => now()->addDay()->timestamp,
'endDate' => now()->addDay()->addHour()->timestamp,
]);

In JavaScript, remember to divide Date.now() by 1000:

Copied!
const startDate = Math.floor(Date.now() / 1000) + 3600;

Timezone Is Metadata Only#

Setting a timezone on an event (inTimezone()) does not adjust the timestamps. It is stored as display metadata. You are responsible for converting your timestamps to the intended timezone before passing them.

Event IDs Differ Between Platforms#

Both are returned as strings. Never assume a numeric format. Store them as strings in your database.

All-Day Events Ignore Time Components#

When isAllDay is true, the time portion of your start/end timestamps is ignored. The event spans the entire calendar day(s). Don't rely on specific hours for all-day events.

Recurring Events#

  • Use the RecurrenceRule class to build rules safely. Raw RRULE strings must follow RFC 5545 format.
  • getEventInstances() only returns instances within the date range you specify. A too-narrow range returns no results.
  • deleteEvent($id, deleteAllOccurrences: true) on iOS deletes the current and all future occurrences, not past ones. On Android, it deletes the entire event.
  • Not all RFC 5545 features are supported (e.g., BYSECOND, BYHOUR are ignored).

Change Listener Must Be Stopped#

The change listener registers a native observer that persists until explicitly stopped. Failing to call stopChangeListener() will leak memory. Always clean up:

Copied!
// When your component is destroyed or no longer needs updates
MobileCalendar::stopChangeListener();

The trackEvents: true option captures a snapshot of all events (±1 year) in memory. The trackCalendars: true option captures a snapshot of calendar IDs. On devices with thousands of events, event tracking can be expensive. Only enable what you need.

Creating Calendars Has Caveats#

On Android, creating a local calendar requires internal sync adapter flags. The plugin handles this, but the calendar may not appear in all calendar apps. On iOS, a local calendar source must exist (it usually does). If no source is available, creation fails with an error message.

Attendees Are Read-Only on iOS#

iOS does not allow programmatically adding attendees to events — EKParticipant is a read-only class. Calling addAttendee() on iOS returns {"supported": false}. Use the calendar URI helpers or openCalendarToAdd() as cross-platform alternatives for inviting people.

On Android, addAttendee() works but only adds attendee metadata to the event. It does not send email invitations.

Version Support#

Platform Minimum Version
Android 4.0 (API 14)
iOS 17.0

Features requiring higher versions:

  • iOS 17+: Write-only calendar access (CalendarAccessLevel::Write)

Support#

Bugs, questions, and feature requests should be reported at github.com/SRWieZ/nativephp-mobile-packages.