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#
# Install the packagecomposer require srwiez/nativephp-mobile-calendar # Publish the plugins provider (first time only)php artisan vendor:publish --tag=nativephp-plugins-provider # Register the pluginphp artisan native:plugin:register srwiez/nativephp-mobile-calendar # Verify registrationphp artisan native:plugin:list
Permissions#
The plugin automatically configures the required permissions:
Android (AndroidManifest.xml):
android.permission.READ_CALENDARandroid.permission.WRITE_CALENDAR
iOS (Info.plist):
NSCalendarsUsageDescriptionNSCalendarsFullAccessUsageDescription(iOS 17+)NSCalendarsWriteOnlyAccessUsageDescription(iOS 17+)
Usage#
Check & Request Permission#
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#
// 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#
$calendar = MobileCalendar::createCalendar( title: 'My App Calendar', color: '#FF5733');
Delete Calendar#
$deleted = MobileCalendar::deleteCalendar($calendarId);
Query Events#
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#
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:
$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#
$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#
// Delete single eventMobileCalendar::deleteEvent($eventId); // Delete all occurrences of a recurring eventMobileCalendar::deleteEvent($eventId, deleteAllOccurrences: true);
Recurring Events#
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#
use SRWieZ\NativePHP\Mobile\Calendar\Data\Alarm;use SRWieZ\NativePHP\Mobile\Calendar\Enums\AlarmMethod; // Add alarm to existing eventMobileCalendar::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 indexMobileCalendar::removeAlarm($eventId, alarmIndex: 0);
Attendees#
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, ->name('John Doe') ->type(AttendeeType::Required) );} catch (UnsupportedPlatformException $e) { // Use calendar URI helpers instead for cross-platform support}
Open Native Calendar App#
// 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 eventMobileCalendar::openCalendarToEdit($eventId); // Open calendar to view specific dateMobileCalendar::openCalendarToView(now()->addWeek()); // Open calendar to todayMobileCalendar::openCalendarToView();
Listen for Calendar Changes#
// Start listening for event changesMobileCalendar::startChangeListener(trackEvents: true); // Also detect calendar create/delete (e.g. from external apps)MobileCalendar::startChangeListener(trackEvents: true, trackCalendars: true); // In your Livewire componentuse 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 doneMobileCalendar::stopChangeListener();
Calendar URI Helpers (Pure PHP - No Native Calls)#
Generate URLs to add events to various calendar services:
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 |
use Native\Mobile\Attributes\OnNative;use SRWieZ\NativePHP\Mobile\Calendar\Events\EventCreated; #[OnNative(EventCreated::class)]public function handleEventCreated($eventId){ // Handle event creation}
JavaScript Usage#
import { mobileCalendar } from '@srwiez/nativephp-mobile-calendar'; // Request permissionconst { granted, status } = await mobileCalendar.requestAccess('full'); // Get calendarsconst { calendars } = await mobileCalendar.getCalendars(); // Create eventconst { 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 appawait 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:
$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.
// 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:
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#
- iOS: UUIDs like
"[email protected]" - Android: Numeric database row IDs like
"42"
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
RecurrenceRuleclass 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,BYHOURare 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:
// When your component is destroyed or no longer needs updatesMobileCalendar::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.