diff --git a/android/src/main/java/com/calendarevents/CalendarEventsNativeModule.java b/android/src/main/java/com/calendarevents/CalendarEventsNativeModule.java index bf1801f..151ce77 100644 --- a/android/src/main/java/com/calendarevents/CalendarEventsNativeModule.java +++ b/android/src/main/java/com/calendarevents/CalendarEventsNativeModule.java @@ -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; @@ -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 { @@ -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) { @@ -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 }; @@ -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(); @@ -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); diff --git a/ios/CalendarEventsNative.mm b/ios/CalendarEventsNative.mm index 19dd127..91f66f7 100644 --- a/ios/CalendarEventsNative.mm +++ b/ios/CalendarEventsNative.mm @@ -291,12 +291,7 @@ - (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(), ^{ @@ -304,38 +299,20 @@ - (NSDictionary *)calendarToDict:(EKCalendar *)calendar { 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 { @@ -345,12 +322,7 @@ - (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(), ^{ @@ -358,40 +330,21 @@ - (NSDictionary *)calendarToDict:(EKCalendar *)calendar { 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 { @@ -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; } diff --git a/src/NativeCalendarEventsNativeSpec.ts b/src/NativeCalendarEventsNativeSpec.ts index d052f45..9fb36f0 100644 --- a/src/NativeCalendarEventsNativeSpec.ts +++ b/src/NativeCalendarEventsNativeSpec.ts @@ -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; - updateEvent( - eventId: string, - title: string, - startDate: string, - endDate: string, - location: string, - notes: string, - calendarId: string - ): Promise; + // `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; + updateEvent(eventId: string, details: Object): Promise; removeEvent(eventId: string): Promise; openEventInCalendar?(eventId: string): Promise; } diff --git a/src/index.tsx b/src/index.tsx index 4bc25cb..b625103 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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): 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 { - 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): Promise { - 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 + ): Promise { + return CalendarEventsNative.updateEvent(eventId, this.toNativeDetails(event)); } /**