Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.turbomodule.core.interfaces.TurboModule;

import java.text.ParseException;
import java.text.SimpleDateFormat;
Expand All @@ -37,8 +38,11 @@
import java.util.TimeZone;

@ReactModule(name = CalendarEventsNativeModule.NAME)
public class CalendarEventsNativeModule extends ReactContextBaseJavaModule {
public static final String NAME = "CalendarEventsNative";
public class CalendarEventsNativeModule extends ReactContextBaseJavaModule implements TurboModule {
// Must match the name JS requests via TurboModuleRegistry.getEnforcing(...)
// and the iOS RCT_EXPORT_MODULE(...) name, otherwise the TurboModule is not
// found on Android under the new architecture.
public static final String NAME = "RNCalendarEventsNativeSpec";
private static final SimpleDateFormat ISO_8601_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);

static {
Expand All @@ -55,6 +59,13 @@ public String getName() {
return NAME;
}

// Declared in the JS spec (NativeCalendarEventsNativeSpec.ts); required so the
// codegen method map matches the native implementation.
@ReactMethod
public void debugModuleMethods(Promise promise) {
promise.resolve("Methods logged to console");
}

// Permission methods
@ReactMethod
public void requestPermissions(boolean writeOnly, Promise promise) {
Expand Down Expand Up @@ -122,9 +133,11 @@ public void fetchAllCalendars(Promise promise) {
}

@ReactMethod
public void findOrCreateCalendar(ReadableMap calendarMap, Promise promise) {
String title = calendarMap.hasKey("title") ? calendarMap.getString("title") : "Calendar";

public void findOrCreateCalendar(String title, @Nullable String color, @Nullable String entityType, @Nullable String source, Promise promise) {
if (title == null) {
title = "Calendar";
}

// First, try to find existing calendar
ContentResolver cr = getReactApplicationContext().getContentResolver();
String[] projection = new String[] { Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME };
Expand Down Expand Up @@ -163,10 +176,9 @@ public void findOrCreateCalendar(ReadableMap calendarMap, Promise promise) {
values.put(Calendars.VISIBLE, 1);
values.put(Calendars.SYNC_EVENTS, 1);

if (calendarMap.hasKey("color")) {
String colorHex = calendarMap.getString("color");
int color = (int) Long.parseLong(colorHex.replace("#", ""), 16);
values.put(Calendars.CALENDAR_COLOR, color);
if (color != null) {
int colorInt = (int) Long.parseLong(color.replace("#", ""), 16);
values.put(Calendars.CALENDAR_COLOR, colorInt);
}

Uri.Builder builder = Calendars.CONTENT_URI.buildUpon();
Expand All @@ -186,8 +198,8 @@ public void findOrCreateCalendar(ReadableMap calendarMap, Promise promise) {
result.putBoolean("isPrimary", false);
result.putBoolean("allowsModifications", true);

if (calendarMap.hasKey("color")) {
result.putString("color", calendarMap.getString("color"));
if (color != null) {
result.putString("color", color);
}

promise.resolve(result);
Expand Down
123 changes: 49 additions & 74 deletions ios/CalendarEventsNative.mm
Original file line number Diff line number Diff line change
Expand Up @@ -291,51 +291,28 @@ - (NSDictionary *)calendarToDict:(EKCalendar *)calendar {
});
}

RCT_EXPORT_METHOD(saveEvent:(NSString *)title
startDate:(NSString *)startDate
endDate:(NSString *)endDate
location:(NSString *)location
notes:(NSString *)notes
calendarId:(NSString *)calendarId
RCT_EXPORT_METHOD(saveEvent:(NSDictionary *)details
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.eventStore) {
reject(@"event_store_unavailable", @"Event store not available", nil);
return;
}

EKEvent *event = [EKEvent eventWithEventStore:self.eventStore];
if (!event) {
reject(@"event_creation_failed", @"Failed to create event object", nil);
return;
}

// Simple property setting - no complex C++ structs!
event.title = title ?: @"Untitled Event";
event.startDate = [self dateFromISO8601String:startDate];
event.endDate = [self dateFromISO8601String:endDate];

if (location && location.length > 0) {
event.location = location;
}

if (notes && notes.length > 0) {
event.notes = notes;
}

if (calendarId && calendarId.length > 0) {
EKCalendar *calendar = [self.eventStore calendarWithIdentifier:calendarId];
if (calendar) {
event.calendar = calendar;
}
} else {
event.calendar = self.eventStore.defaultCalendarForNewEvents;
}


// Apply the full event object: title, dates, location, notes, url, allDay,
// calendar, availability, alarms, recurrence, timeZone.
[self applyEventProperties:details toEvent:event];

NSError *error;
BOOL success = [self.eventStore saveEvent:event span:EKSpanThisEvent commit:YES error:&error];

if (success) {
resolve(event.eventIdentifier);
} else {
Expand All @@ -345,53 +322,29 @@ - (NSDictionary *)calendarToDict:(EKCalendar *)calendar {
}

RCT_EXPORT_METHOD(updateEvent:(NSString *)eventId
title:(NSString *)title
startDate:(NSString *)startDate
endDate:(NSString *)endDate
location:(NSString *)location
notes:(NSString *)notes
calendarId:(NSString *)calendarId
details:(NSDictionary *)details
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.eventStore) {
reject(@"event_store_unavailable", @"Event store not available", nil);
return;
}

EKEvent *event = [self.eventStore eventWithIdentifier:eventId];

if (!event) {
reject(@"event_not_found", @"Event not found", nil);
return;
}

// Update event properties directly
if (title && title.length > 0) {
event.title = title;
}
if (startDate && startDate.length > 0) {
event.startDate = [self dateFromISO8601String:startDate];
}
if (endDate && endDate.length > 0) {
event.endDate = [self dateFromISO8601String:endDate];
}
if (location && location.length > 0) {
event.location = location;
}
if (notes && notes.length > 0) {
event.notes = notes;
}
if (calendarId && calendarId.length > 0) {
EKCalendar *calendar = [self.eventStore calendarWithIdentifier:calendarId];
if (calendar) {
event.calendar = calendar;
}
}


// Only the keys present in `details` are applied, so a partial update
// leaves the other properties (and existing alarms/recurrence) untouched.
[self applyEventProperties:details toEvent:event];

NSError *error;
BOOL success = [self.eventStore saveEvent:event span:EKSpanThisEvent commit:YES error:&error];

if (success) {
resolve(event.eventIdentifier);
} else {
Expand Down Expand Up @@ -454,21 +407,43 @@ - (NSDictionary *)calendarToDict:(EKCalendar *)calendar {
#pragma mark - Helper Methods

- (void)applyEventProperties:(NSDictionary *)eventDict toEvent:(EKEvent *)event {
event.title = eventDict[@"title"];
event.startDate = [self dateFromISO8601String:eventDict[@"startDate"]];
event.endDate = [self dateFromISO8601String:eventDict[@"endDate"]];
event.location = eventDict[@"location"];
event.notes = eventDict[@"notes"];
event.URL = eventDict[@"url"] ? [NSURL URLWithString:eventDict[@"url"]] : nil;
event.allDay = [eventDict[@"allDay"] boolValue];

// Set calendar
// Only apply keys that are present, so a partial updateEvent() leaves the
// remaining properties untouched instead of clearing them.
if (eventDict[@"title"]) {
event.title = eventDict[@"title"];
}
if (eventDict[@"startDate"]) {
event.startDate = [self dateFromISO8601String:eventDict[@"startDate"]];
}
if (eventDict[@"endDate"]) {
event.endDate = [self dateFromISO8601String:eventDict[@"endDate"]];
}
if (eventDict[@"location"]) {
event.location = eventDict[@"location"];
}
if (eventDict[@"notes"]) {
event.notes = eventDict[@"notes"];
}
if (eventDict[@"url"]) {
event.URL = [NSURL URLWithString:eventDict[@"url"]];
}
if (eventDict[@"allDay"]) {
event.allDay = [eventDict[@"allDay"] boolValue];
}
if (eventDict[@"timeZone"]) {
NSTimeZone *timeZone = [NSTimeZone timeZoneWithName:eventDict[@"timeZone"]];
if (timeZone) {
event.timeZone = timeZone;
}
}

// Set calendar when provided; only fall back to the default for a new event.
if (eventDict[@"calendar"]) {
EKCalendar *calendar = [self.eventStore calendarWithIdentifier:eventDict[@"calendar"]];
if (calendar) {
event.calendar = calendar;
}
} else {
} else if (!event.calendar) {
event.calendar = self.eventStore.defaultCalendarForNewEvents;
}

Expand Down
23 changes: 6 additions & 17 deletions src/NativeCalendarEventsNativeSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,23 +111,12 @@ export interface Spec extends TurboModule {
allDay?: boolean;
calendar?: string;
} | null>;
saveEvent(
title: string,
startDate: string,
endDate: string,
location: string,
notes: string,
calendarId: string
): Promise<string>;
updateEvent(
eventId: string,
title: string,
startDate: string,
endDate: string,
location: string,
notes: string,
calendarId: string
): Promise<string>;
// `details` is a full event object (title, startDate, endDate, location, notes,
// url, allDay, calendar, availability, alarms, recurrence, timeZone). Typed as
// `Object` so codegen maps it to NSDictionary/ReadableMap (a structured type
// would emit a C++ struct that conflicts with the native NSDictionary impl).
saveEvent(details: Object): Promise<string>;
updateEvent(eventId: string, details: Object): Promise<string>;
removeEvent(eventId: string): Promise<boolean>;
openEventInCalendar?(eventId: string): Promise<void>;
}
Expand Down
83 changes: 49 additions & 34 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,46 +198,61 @@ class CalendarEvents {
}

/**
* Save an event
* Convert a (partial) event into the plain object the native module expects.
* Date instances become ISO-8601 strings, and only the keys that are present
* are forwarded so that updateEvent() behaves as a partial update.
*/
private toNativeDetails(event: Partial<CalendarEvent>): Object {
const details: { [key: string]: any } = { ...event };

if (event.startDate !== undefined) {
details.startDate =
typeof event.startDate === 'string'
? event.startDate
: event.startDate.toISOString();
}
if (event.endDate !== undefined) {
details.endDate =
typeof event.endDate === 'string'
? event.endDate
: event.endDate.toISOString();
}
if (event.alarms) {
details.alarms = event.alarms.map((alarm) => ({
...alarm,
date:
alarm.date instanceof Date ? alarm.date.toISOString() : alarm.date,
}));
}
if (event.recurrence?.endDate) {
details.recurrence = {
...event.recurrence,
endDate:
event.recurrence.endDate instanceof Date
? event.recurrence.endDate.toISOString()
: event.recurrence.endDate,
};
}

return details;
}

/**
* Save an event. Honors title, start/end dates, location, notes, url, allDay,
* calendar, availability, alarms, recurrence and timeZone.
*/
async saveEvent(event: CalendarEvent): Promise<string> {
const startDate = typeof event.startDate === 'string'
? event.startDate
: event.startDate.toISOString();
const endDate = typeof event.endDate === 'string'
? event.endDate
: event.endDate.toISOString();

return CalendarEventsNative.saveEvent(
event.title,
startDate,
endDate,
event.location || '',
event.notes || '',
event.calendar || ''
);
return CalendarEventsNative.saveEvent(this.toNativeDetails(event));
}

/**
* Update an event
* Update an existing event. Only the fields provided are changed.
*/
async updateEvent(eventId: string, event: Partial<CalendarEvent>): Promise<string> {
const startDate = event.startDate
? (typeof event.startDate === 'string' ? event.startDate : event.startDate.toISOString())
: '';
const endDate = event.endDate
? (typeof event.endDate === 'string' ? event.endDate : event.endDate.toISOString())
: '';

return CalendarEventsNative.updateEvent(
eventId,
event.title || '',
startDate,
endDate,
event.location || '',
event.notes || '',
event.calendar || ''
);
async updateEvent(
eventId: string,
event: Partial<CalendarEvent>
): Promise<string> {
return CalendarEventsNative.updateEvent(eventId, this.toNativeDetails(event));
}

/**
Expand Down