From 6ccd9ea049e50d12eda8687953d8c095670604ad Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Thu, 4 Jun 2026 00:27:03 +0200 Subject: [PATCH 1/9] implemented edit feature --- .../connect/tech/eventide/CalendarApi.g.kt | 263 +++++++- .../connect/tech/eventide/CalendarImplem.kt | 174 +++++ .../connect/tech/eventide/CalendarTests.kt | 58 ++ .../sncf/connect/tech/eventide/EventTests.kt | 70 ++ example/ios/EventideTests/CalendarTests.swift | 35 + example/ios/EventideTests/EventTests.swift | 64 ++ .../Mocks/MockEasyEventStore.swift | 278 ++++---- example/ios/Flutter/AppFrameworkInfo.plist | 2 - example/ios/Runner.xcodeproj/project.pbxproj | 2 + .../ios/Flutter/AppFrameworkInfo.plist | 2 - .../lib/calendar/logic/calendar_cubit.dart | 63 ++ .../calendar/ui/components/calendar_form.dart | 8 +- .../calendar/ui/components/custom_drawer.dart | 76 ++- .../calendar/ui/components/event_form.dart | 26 +- .../calendar/ui/views/calendar_screen.dart | 4 +- .../logic/event_details_cubit.dart | 22 + .../ui/views/event_details_screen.dart | 54 ++ .../Sources/eventide/CalendarApi.g.swift | 217 ++++-- .../Sources/eventide/CalendarImplem.swift | 88 ++- .../EasyEventStore/EasyEventStore.swift | 95 +++ .../EasyEventStoreProtocol.swift | 4 + lib/src/calendar_api.g.dart | 626 +++++++++--------- lib/src/eventide.dart | 69 ++ lib/src/eventide_platform_interface.dart | 14 + pigeons/calendar_api.dart | 19 + 25 files changed, 1740 insertions(+), 593 deletions(-) diff --git a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarApi.g.kt b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarApi.g.kt index b8986e81..82345b02 100644 --- a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarApi.g.kt +++ b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarApi.g.kt @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -34,36 +34,150 @@ private object CalendarApiPigeonUtils { ) } } + fun doubleEquals(a: Double, b: Double): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN()) + } + + fun floatEquals(a: Float, b: Float): Boolean { + // Normalize -0.0 to 0.0 and handle NaN equality. + return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN()) + } + + fun doubleHash(d: Double): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (d == 0.0) 0.0 else d + val bits = java.lang.Double.doubleToLongBits(normalized) + return (bits xor (bits ushr 32)).toInt() + } + + fun floatHash(f: Float): Int { + // Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes. + val normalized = if (f == 0.0f) 0.0f else f + return java.lang.Float.floatToIntBits(normalized) + } + fun deepEquals(a: Any?, b: Any?): Boolean { + if (a === b) { + return true + } + if (a == null || b == null) { + return false + } if (a is ByteArray && b is ByteArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is IntArray && b is IntArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is LongArray && b is LongArray) { - return a.contentEquals(b) + return a.contentEquals(b) } if (a is DoubleArray && b is DoubleArray) { - return a.contentEquals(b) + if (a.size != b.size) return false + for (i in a.indices) { + if (!doubleEquals(a[i], b[i])) return false + } + return true + } + if (a is FloatArray && b is FloatArray) { + if (a.size != b.size) return false + for (i in a.indices) { + if (!floatEquals(a[i], b[i])) return false + } + return true } if (a is Array<*> && b is Array<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + for (i in a.indices) { + if (!deepEquals(a[i], b[i])) return false + } + return true } if (a is List<*> && b is List<*>) { - return a.size == b.size && - a.indices.all{ deepEquals(a[it], b[it]) } + if (a.size != b.size) return false + val iterA = a.iterator() + val iterB = b.iterator() + while (iterA.hasNext() && iterB.hasNext()) { + if (!deepEquals(iterA.next(), iterB.next())) return false + } + return true } if (a is Map<*, *> && b is Map<*, *>) { - return a.size == b.size && a.all { - (b as Map).contains(it.key) && - deepEquals(it.value, b[it.key]) + if (a.size != b.size) return false + for (entry in a) { + val key = entry.key + var found = false + for (bEntry in b) { + if (deepEquals(key, bEntry.key)) { + if (deepEquals(entry.value, bEntry.value)) { + found = true + break + } else { + return false + } + } + } + if (!found) return false } + return true + } + if (a is Double && b is Double) { + return doubleEquals(a, b) + } + if (a is Float && b is Float) { + return floatEquals(a, b) } return a == b } - + + fun deepHash(value: Any?): Int { + return when (value) { + null -> 0 + is ByteArray -> value.contentHashCode() + is IntArray -> value.contentHashCode() + is LongArray -> value.contentHashCode() + is DoubleArray -> { + var result = 1 + for (item in value) { + result = 31 * result + doubleHash(item) + } + result + } + is FloatArray -> { + var result = 1 + for (item in value) { + result = 31 * result + floatHash(item) + } + result + } + is Array<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is List<*> -> { + var result = 1 + for (item in value) { + result = 31 * result + deepHash(item) + } + result + } + is Map<*, *> -> { + var result = 0 + for (entry in value) { + result += ((deepHash(entry.key) * 31) xor deepHash(entry.value)) + } + result + } + is Double -> doubleHash(value) + is Float -> floatHash(value) + else -> value.hashCode() + } + } + } /** @@ -76,7 +190,7 @@ class FlutterError ( val code: String, override val message: String? = null, val details: Any? = null -) : Throwable() +) : RuntimeException() /** Generated class from Pigeon that represents data sent in messages. */ data class Calendar ( @@ -107,15 +221,25 @@ data class Calendar ( ) } override fun equals(other: Any?): Boolean { - if (other !is Calendar) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return CalendarApiPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as Calendar + return CalendarApiPigeonUtils.deepEquals(this.id, other.id) && CalendarApiPigeonUtils.deepEquals(this.title, other.title) && CalendarApiPigeonUtils.deepEquals(this.color, other.color) && CalendarApiPigeonUtils.deepEquals(this.isWritable, other.isWritable) && CalendarApiPigeonUtils.deepEquals(this.account, other.account) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.id) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.title) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.color) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.isWritable) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.account) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -165,15 +289,31 @@ data class Event ( ) } override fun equals(other: Any?): Boolean { - if (other !is Event) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return CalendarApiPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as Event + return CalendarApiPigeonUtils.deepEquals(this.id, other.id) && CalendarApiPigeonUtils.deepEquals(this.calendarId, other.calendarId) && CalendarApiPigeonUtils.deepEquals(this.title, other.title) && CalendarApiPigeonUtils.deepEquals(this.isAllDay, other.isAllDay) && CalendarApiPigeonUtils.deepEquals(this.startDate, other.startDate) && CalendarApiPigeonUtils.deepEquals(this.endDate, other.endDate) && CalendarApiPigeonUtils.deepEquals(this.reminders, other.reminders) && CalendarApiPigeonUtils.deepEquals(this.attendees, other.attendees) && CalendarApiPigeonUtils.deepEquals(this.description, other.description) && CalendarApiPigeonUtils.deepEquals(this.url, other.url) && CalendarApiPigeonUtils.deepEquals(this.location, other.location) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.id) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.calendarId) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.title) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.isAllDay) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.startDate) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.endDate) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.reminders) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.attendees) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.description) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.url) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.location) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -199,15 +339,23 @@ data class Account ( ) } override fun equals(other: Any?): Boolean { - if (other !is Account) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return CalendarApiPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as Account + return CalendarApiPigeonUtils.deepEquals(this.id, other.id) && CalendarApiPigeonUtils.deepEquals(this.name, other.name) && CalendarApiPigeonUtils.deepEquals(this.type, other.type) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.id) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.name) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.type) + return result + } } /** Generated class from Pigeon that represents data sent in messages. */ @@ -239,15 +387,25 @@ data class Attendee ( ) } override fun equals(other: Any?): Boolean { - if (other !is Attendee) { + if (other == null || other.javaClass != javaClass) { return false } if (this === other) { return true } - return CalendarApiPigeonUtils.deepEquals(toList(), other.toList()) } + val other = other as Attendee + return CalendarApiPigeonUtils.deepEquals(this.name, other.name) && CalendarApiPigeonUtils.deepEquals(this.email, other.email) && CalendarApiPigeonUtils.deepEquals(this.type, other.type) && CalendarApiPigeonUtils.deepEquals(this.role, other.role) && CalendarApiPigeonUtils.deepEquals(this.status, other.status) + } - override fun hashCode(): Int = toList().hashCode() + override fun hashCode(): Int { + var result = javaClass.hashCode() + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.name) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.email) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.type) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.role) + result = 31 * result + CalendarApiPigeonUtils.deepHash(this.status) + return result + } } private open class CalendarApiPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { @@ -302,10 +460,12 @@ private open class CalendarApiPigeonCodec : StandardMessageCodec() { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface CalendarApi { fun createCalendar(title: String, color: Long, account: Account?, callback: (Result) -> Unit) + fun updateCalendar(calendarId: String, title: String, color: Long, callback: (Result) -> Unit) fun retrieveCalendars(onlyWritableCalendars: Boolean, account: Account?, callback: (Result>) -> Unit) fun retrieveAccounts(callback: (Result>) -> Unit) fun deleteCalendar(calendarId: String, callback: (Result) -> Unit) fun createEvent(calendarId: String, title: String, startDate: Long, endDate: Long, isAllDay: Boolean, description: String?, url: String?, location: String?, reminders: List?, callback: (Result) -> Unit) + fun updateEvent(eventId: String, calendarId: String, title: String, startDate: Long, endDate: Long, isAllDay: Boolean, description: String?, url: String?, location: String?, reminders: List?, callback: (Result) -> Unit) fun createEventInDefaultCalendar(title: String, startDate: Long, endDate: Long, isAllDay: Boolean, description: String?, url: String?, location: String?, reminders: List?, callback: (Result) -> Unit) fun createEventThroughNativePlatform(title: String?, startDate: Long?, endDate: Long?, isAllDay: Boolean?, description: String?, url: String?, location: String?, reminders: List?, callback: (Result) -> Unit) fun retrieveEvents(calendarId: String, startDate: Long, endDate: Long, callback: (Result>) -> Unit) @@ -346,6 +506,28 @@ interface CalendarApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.eventide.CalendarApi.updateCalendar$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val calendarIdArg = args[0] as String + val titleArg = args[1] as String + val colorArg = args[2] as Long + api.updateCalendar(calendarIdArg, titleArg, colorArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(CalendarApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(CalendarApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.eventide.CalendarApi.retrieveCalendars$separatedMessageChannelSuffix", codec) if (api != null) { @@ -432,6 +614,35 @@ interface CalendarApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.eventide.CalendarApi.updateEvent$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val eventIdArg = args[0] as String + val calendarIdArg = args[1] as String + val titleArg = args[2] as String + val startDateArg = args[3] as Long + val endDateArg = args[4] as Long + val isAllDayArg = args[5] as Boolean + val descriptionArg = args[6] as String? + val urlArg = args[7] as String? + val locationArg = args[8] as String? + val remindersArg = args[9] as List? + api.updateEvent(eventIdArg, calendarIdArg, titleArg, startDateArg, endDateArg, isAllDayArg, descriptionArg, urlArg, locationArg, remindersArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(CalendarApiPigeonUtils.wrapError(error)) + } else { + val data = result.getOrNull() + reply.reply(CalendarApiPigeonUtils.wrapResult(data)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } run { val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.eventide.CalendarApi.createEventInDefaultCalendar$separatedMessageChannelSuffix", codec) if (api != null) { diff --git a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt index dd61a2df..df8f3b02 100644 --- a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt +++ b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt @@ -350,6 +350,77 @@ class CalendarImplem( } } + override fun updateCalendar( + calendarId: String, + title: String, + color: Long, + callback: (Result) -> Unit + ) { + permissionHandler.requestWritePermission { granted -> + if (!granted) { + callback( + Result.failure( + FlutterError( + code = "ACCESS_REFUSED", + message = "Calendar access has been refused or has not been given yet", + ) + ) + ) + return@requestWritePermission + } + + CoroutineScope(Dispatchers.IO).launch { + try { + if (isCalendarWritable(calendarId)) { + val values = ContentValues().apply { + put(CalendarContract.Calendars.NAME, title) + put(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, title) + put(CalendarContract.Calendars.CALENDAR_COLOR, color) + } + + val selection = CalendarContract.Calendars._ID + " = ?" + val selectionArgs = arrayOf(calendarId) + + val updated = contentResolver.update(calendarContentUri, values, selection, selectionArgs) + if (updated > 0) { + callback(Result.success(Unit)) + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_FOUND", + message = "Failed to update calendar" + ) + ) + ) + } + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_EDITABLE", + message = "Calendar is not writable" + ) + ) + ) + } + } catch (e: FlutterError) { + callback(Result.failure(e)) + } catch (e: Exception) { + callback( + Result.failure( + FlutterError( + code = "GENERIC_ERROR", + message = e.message, + details = e.cause + ) + ) + ) + } + } + } + } + override fun createEvent( calendarId: String, title: String, @@ -474,6 +545,109 @@ class CalendarImplem( } } + override fun updateEvent( + eventId: String, + calendarId: String, + title: String, + startDate: Long, + endDate: Long, + isAllDay: Boolean, + description: String?, + url: String?, + location: String?, + reminders: List?, + callback: (Result) -> Unit + ) { + permissionHandler.requestWritePermission { granted -> + if (!granted) { + callback( + Result.failure( + FlutterError( + code = "ACCESS_REFUSED", + message = "Calendar access has been refused or has not been given yet", + ) + ) + ) + return@requestWritePermission + } + + CoroutineScope(Dispatchers.IO).launch { + try { + if (isCalendarWritable(calendarId)) { + val descriptionUrlHelper = DescriptionUrlHelper() + val mergedDescription = descriptionUrlHelper.mergeDescriptionAndUrl(description, url) + + val eventValues = ContentValues().apply { + put(CalendarContract.Events.CALENDAR_ID, calendarId) + put(CalendarContract.Events.TITLE, title) + put(CalendarContract.Events.DESCRIPTION, mergedDescription) + put(CalendarContract.Events.EVENT_LOCATION, location) + put(CalendarContract.Events.DTSTART, startDate) + put(CalendarContract.Events.DTEND, endDate) + put(CalendarContract.Events.EVENT_TIMEZONE, "UTC") + put(CalendarContract.Events.ALL_DAY, if (isAllDay) 1 else 0) + } + + val selection = CalendarContract.Events._ID + " = ?" + val selectionArgs = arrayOf(eventId) + + val updated = contentResolver.update(eventContentUri, eventValues, selection, selectionArgs) + + if (reminders != null) { + val reminderSelection = CalendarContract.Reminders.EVENT_ID + " = ?" + contentResolver.delete(remindersContentUri, reminderSelection, arrayOf(eventId)) + + reminders.forEach { reminder -> + val reminderValues = ContentValues().apply { + put(CalendarContract.Reminders.EVENT_ID, eventId) + put(CalendarContract.Reminders.MINUTES, reminder) + put(CalendarContract.Reminders.METHOD, CalendarContract.Reminders.METHOD_ALERT) + } + contentResolver.insert(remindersContentUri, reminderValues) + } + } + + if (updated > 0) { + callback(Result.success(Unit)) + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_FOUND", + message = "Failed to update event" + ) + ) + ) + } + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_EDITABLE", + message = "Calendar is not writable" + ) + ) + ) + } + + } catch (e: FlutterError) { + callback(Result.failure(e)) + + } catch (e: Exception) { + callback( + Result.failure( + FlutterError( + code = "GENERIC_ERROR", + message = e.message, + details = e.cause + ) + ) + ) + } + } + } + } + override fun createEventInDefaultCalendar( title: String, startDate: Long, diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt index 4490ae15..f605a365 100644 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt @@ -506,4 +506,62 @@ class CalendarTests { assertTrue(result!!.isFailure) assertEquals("NOT_FOUND", (result.exceptionOrNull() as FlutterError).code) } + + @Test + fun updateCalendar_withGrantedPermission_updatesCalendarSuccessfully() = runTest { + mockPermissionGranted(permissionHandler) + mockWritableCalendar() + + every { contentResolver.update(calendarContentUri, any(), any(), any()) } returns 1 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.updateCalendar("1", "New Title", 0xFF0000) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + verify { + contentResolver.update(calendarContentUri, any(), any(), any()) + } + } + + @Test + fun updateCalendar_withNotWritableCalendar_returnsNotEditableError() = runTest { + mockPermissionGranted(permissionHandler) + mockNotWritableCalendar() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.updateCalendar("1", "New Title", 0xFF0000) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_EDITABLE", (result.exceptionOrNull() as FlutterError).code) + } + + @Test + fun updateCalendar_withNotFoundCalendar_returnsNotFoundError() = runTest { + mockPermissionGranted(permissionHandler) + mockCalendarNotFound() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.updateCalendar("1", "New Title", 0xFF0000) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isFailure) + assertEquals("NOT_FOUND", (result.exceptionOrNull() as FlutterError).code) + } } diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt index fae19370..e8cddcdc 100644 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt @@ -697,4 +697,74 @@ class EventTests { assertTrue(result!!.isSuccess) } + + @Test + fun updateEvent_withGrantedPermission_updatesEventSuccessfully() = runTest { + mockPermissionGranted(permissionHandler) + mockWritableCalendar() + + every { contentResolver.update(eventContentUri, any(), any(), any()) } returns 1 + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.updateEvent( + "1", + "1", + "New Title", + 0L, + 1000L, + false, + "Description", + "url", + "location", + null + ) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + verify { + contentResolver.update(eventContentUri, any(), any(), any()) + } + } + + @Test + fun updateEvent_withReminders_updatesEventAndRemindersSuccessfully() = runTest { + mockPermissionGranted(permissionHandler) + mockWritableCalendar() + + every { contentResolver.update(eventContentUri, any(), any(), any()) } returns 1 + every { contentResolver.delete(remindersContentUri, any(), any()) } returns 1 + every { contentResolver.insert(remindersContentUri, any()) } returns mockk() + + var result: Result? = null + val latch = CountDownLatch(1) + calendarImplem.updateEvent( + "1", + "1", + "New Title", + 0L, + 1000L, + false, + "Description", + "url", + "location", + listOf(10L, 20L) + ) { + result = it + latch.countDown() + } + + latch.await() + + assertTrue(result!!.isSuccess) + verify { + contentResolver.update(eventContentUri, any(), any(), any()) + contentResolver.delete(remindersContentUri, any(), any()) + contentResolver.insert(remindersContentUri, any()) + } + } } diff --git a/example/ios/EventideTests/CalendarTests.swift b/example/ios/EventideTests/CalendarTests.swift index 125a6f62..7ccb8947 100644 --- a/example/ios/EventideTests/CalendarTests.swift +++ b/example/ios/EventideTests/CalendarTests.swift @@ -539,4 +539,39 @@ final class CalendarTests: XCTestCase { waitForExpectations(timeout: timeout) } + + func testUpdateCalendar_permissionGranted() { + let expectation = expectation(description: "Calendar has been updated") + + let mockEasyEventStore = MockEasyEventStore( + calendars: [ + MockCalendar( + id: "1", + title: "Old Title", + color: UIColor.red, + isWritable: true, + account: Account(id: "local", name: "local", type: "local"), + events: [] + ) + ] + ) + + calendarImplem = CalendarImplem( + easyEventStore: mockEasyEventStore, + permissionHandler: PermissionGranted() + ) + + calendarImplem.updateCalendar(withId: "1", title: "New Title", color: 0x00FF00) { result in + switch (result) { + case .success: + XCTAssert(mockEasyEventStore.calendars[0].title == "New Title") + XCTAssert(mockEasyEventStore.calendars[0].color.toInt64() == 0x00FF00) + expectation.fulfill() + case .failure: + XCTFail("Calendar should have been updated") + } + } + + waitForExpectations(timeout: timeout) + } } diff --git a/example/ios/EventideTests/EventTests.swift b/example/ios/EventideTests/EventTests.swift index 82f326fa..a9687936 100644 --- a/example/ios/EventideTests/EventTests.swift +++ b/example/ios/EventideTests/EventTests.swift @@ -1153,4 +1153,68 @@ final class EventTests: XCTestCase { waitForExpectations(timeout: timeout) } + + func testUpdateEvent_permissionGranted() { + let expectation = expectation(description: "Event has been updated") + + let startDate = Date().millisecondsSince1970 + let endDate = Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970 + + let mockCalendar = MockCalendar( + id: "1", + title: "title", + color: UIColor.red, + isWritable: true, + account: Account(id: "local", name: "local", type: "local"), + events: [ + MockEvent( + id: "event1", + calendarId: "1", + title: "Old Title", + startDate: Date(from: startDate), + endDate: Date(from: endDate), + isAllDay: false, + description: "Old Desc", + url: "Old URL", + location: "Old Loc", + reminders: [] + ) + ] + ) + + let mockEasyEventStore = MockEasyEventStore(calendars: [mockCalendar]) + + calendarImplem = CalendarImplem( + easyEventStore: mockEasyEventStore, + permissionHandler: PermissionGranted() + ) + + calendarImplem.updateEvent( + withId: "event1", + calendarId: "1", + title: "New Title", + startDate: startDate + 1000, + endDate: endDate + 1000, + isAllDay: true, + description: "New Desc", + url: "New URL", + location: "New Loc", + reminders: [10] + ) { result in + switch (result) { + case .success: + let updatedEvent = mockCalendar.events[0] + XCTAssert(updatedEvent.title == "New Title") + XCTAssert(updatedEvent.startDate.millisecondsSince1970 == startDate + 1000) + XCTAssert(updatedEvent.isAllDay == true) + XCTAssert(updatedEvent.description == "New Desc") + XCTAssert(updatedEvent.reminders == [-10.0]) // In MockEasyEventStore it's converted + expectation.fulfill() + case .failure: + XCTFail("Event should have been updated") + } + } + + waitForExpectations(timeout: timeout) + } } diff --git a/example/ios/EventideTests/Mocks/MockEasyEventStore.swift b/example/ios/EventideTests/Mocks/MockEasyEventStore.swift index 88cb26a9..cdec6265 100644 --- a/example/ios/EventideTests/Mocks/MockEasyEventStore.swift +++ b/example/ios/EventideTests/Mocks/MockEasyEventStore.swift @@ -5,28 +5,23 @@ // Created by CHOUPAULT Alexis on 23/01/2025. // +import EventKit +import Foundation import UIKit @testable import eventide class MockEasyEventStore: EasyEventStoreProtocol { - var calendars: [MockCalendar] - - init(calendars: [MockCalendar] = []) { - self.calendars = calendars - } - + var calendars: [MockCalendar] = [] + func createCalendar(title: String, color: UIColor, account: Account?) throws -> eventide.Calendar { let calendar = MockCalendar( - id: "id", + id: UUID().uuidString, title: title, color: color, isWritable: true, - account: account ?? Account(id: "local", name: "local", type: "local"), - events: [] + account: account ?? Account(id: "local", name: "Local", type: "Local") ) - calendars.append(calendar) - return calendar.toCalendar() } @@ -42,7 +37,11 @@ class MockEasyEventStore: EasyEventStoreProtocol { } .map { $0.toCalendar() } } - + + func retrieveAccounts() -> [Account] { + return calendars.map { $0.account } + } + func deleteCalendar(calendarId: String) throws { guard let index = calendars.firstIndex(where: { $0.id == calendarId }) else { throw PigeonError( @@ -51,7 +50,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { details: "The provided calendar.id is certainly incorrect" ) } - + guard calendars[index].isWritable else { throw PigeonError( code: "NOT_EDITABLE", @@ -59,14 +58,31 @@ class MockEasyEventStore: EasyEventStoreProtocol { details: "Calendar does not allow content modifications" ) } - + calendars.remove(at: index) } - - func retrieveAccounts() -> [Account] { - return calendars.map { $0.account } + + func updateCalendar(calendarId: String, title: String, color: UIColor) throws { + guard let index = calendars.firstIndex(where: { $0.id == calendarId }) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Calendar not found", + details: "The provided calendar.id is certainly incorrect" + ) + } + + guard calendars[index].isWritable else { + throw PigeonError( + code: "NOT_EDITABLE", + message: "Calendar not editable", + details: "Calendar does not allow content modifications" + ) + } + + calendars[index].title = title + calendars[index].color = color } - + func createEvent(calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event { guard let mockCalendar = calendars.first(where: { $0.id == calendarId }) else { throw PigeonError( @@ -77,39 +93,75 @@ class MockEasyEventStore: EasyEventStoreProtocol { } let mockEvent = MockEvent( - id: String(mockCalendar.events.count), + id: UUID().uuidString, + calendarId: calendarId, title: title, startDate: startDate, endDate: endDate, - calendarId: mockCalendar.id, isAllDay: isAllDay, description: description, url: url, - reminders: timeIntervals + location: location, + reminders: timeIntervals ?? [] ) mockCalendar.events.append(mockEvent) - return mockEvent.toEvent() } - + + func updateEvent(eventId: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws { + guard let mockEvent = findEvent(eventId: eventId) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Event not found", + details: "The provided event.id is certainly incorrect" + ) + } + + // Remove from old calendar if changed + if mockEvent.calendarId != calendarId { + guard let oldCalendarIndex = calendars.firstIndex(where: { $0.id == mockEvent.calendarId }) else { + throw PigeonError(code: "GENERIC_ERROR", message: "Old calendar not found") + } + calendars[oldCalendarIndex].events.removeAll { $0.id == eventId } + + guard let newCalendarIndex = calendars.firstIndex(where: { $0.id == calendarId }) else { + throw PigeonError(code: "NOT_FOUND", message: "New calendar not found") + } + calendars[newCalendarIndex].events.append(mockEvent) + } + + mockEvent.calendarId = calendarId + mockEvent.title = title + mockEvent.startDate = startDate + mockEvent.endDate = endDate + mockEvent.isAllDay = isAllDay + mockEvent.description = description + mockEvent.url = url + mockEvent.location = location + if let timeIntervals = timeIntervals { + mockEvent.reminders = timeIntervals + } + } + func createEvent(title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws { let mockEvent = MockEvent( - id: String(calendars.first!.events.count), + id: UUID().uuidString, + calendarId: calendars.first!.id, title: title, startDate: startDate, endDate: endDate, - calendarId: calendars.first!.id, isAllDay: isAllDay, description: description, url: url, - reminders: timeIntervals + location: location, + reminders: timeIntervals ?? [] ) calendars.first!.events.append(mockEvent) } - func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { + func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { completion(.success(())) } @@ -123,7 +175,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { } return mockCalendar.events - .filter { startDate.compare($0.startDate) == .orderedAscending && $0.endDate.compare(endDate) == .orderedAscending } + .filter { ($0.startDate >= startDate && $0.startDate <= endDate) || ($0.endDate >= startDate && $0.endDate <= endDate) } .map { $0.toEvent() } } @@ -156,12 +208,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - if (mockEvent.reminders == nil) { - mockEvent.reminders = [timeInterval] - } else { - mockEvent.reminders!.append(timeInterval) - } - + mockEvent.reminders.append(timeInterval) return mockEvent.toEvent() } @@ -174,62 +221,38 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - guard let index = mockEvent.reminders?.firstIndex(where: { -$0 == timeInterval }) else { - throw PigeonError( - code: "NOT_FOUND", - message: "Reminder not found", - details: nil - ) - } - - mockEvent.reminders?.remove(at: index) + mockEvent.reminders.removeAll { $0 == timeInterval } return mockEvent.toEvent() } - - func retrieveAttendees(eventId: String) throws -> [Attendee] { - guard let mockEvent = findEvent(eventId: eventId) else { - throw PigeonError( - code: "NOT_FOUND", - message: "Event not found", - details: "The provided event.id is certainly incorrect" - ) - } - - return mockEvent.attendees?.map { $0.toAttendee() } ?? [] - } - + private func findEvent(eventId: String) -> MockEvent? { for calendar in calendars { - for event in calendar.events { - if event.id == eventId { - return event - } + if let event = calendar.events.first(where: { $0.id == eventId }) { + return event } } - return nil } } class MockCalendar { let id: String - let title: String - let color: UIColor + var title: String + var color: UIColor let isWritable: Bool let account: Account - var events: [MockEvent] - - init(id: String, title: String, color: UIColor, isWritable: Bool, account: Account, events: [MockEvent]) { + var events: [MockEvent] = [] + + init(id: String, title: String, color: UIColor, isWritable: Bool, account: Account) { self.id = id self.title = title self.color = color self.isWritable = isWritable self.account = account - self.events = events } - - fileprivate func toCalendar() -> eventide.Calendar { - eventide.Calendar( + + func toCalendar() -> eventide.Calendar { + return eventide.Calendar( id: id, title: title, color: color.toInt64(), @@ -241,119 +264,42 @@ class MockCalendar { class MockEvent { let id: String - let title: String - let startDate: Date - let endDate: Date - let calendarId: String - let isAllDay: Bool - let description: String? - let url: String? - let location: String? - var reminders: [TimeInterval]? - let attendees: [MockAttendee]? + var calendarId: String + var title: String + var startDate: Date + var endDate: Date + var isAllDay: Bool + var description: String? + var url: String? + var location: String? + var reminders: [TimeInterval] - init(id: String, title: String, startDate: Date, endDate: Date, calendarId: String, isAllDay: Bool, description: String?, url: String?, location: String? = nil, reminders: [TimeInterval]? = nil, attendees: [MockAttendee]? = nil) { + init(id: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, reminders: [TimeInterval]) { self.id = id + self.calendarId = calendarId self.title = title self.startDate = startDate self.endDate = endDate - self.calendarId = calendarId self.isAllDay = isAllDay self.description = description self.url = url self.location = location - self.reminders = reminders?.map({ $0 }) - self.attendees = attendees + self.reminders = reminders } - - fileprivate func toEvent() -> Event { - Event( + + func toEvent() -> Event { + return Event( id: id, calendarId: calendarId, title: title, isAllDay: isAllDay, startDate: startDate.millisecondsSince1970, endDate: endDate.millisecondsSince1970, - reminders: reminders?.map({ Int64($0) }) ?? [], - attendees: attendees?.map { $0.toAttendee() } ?? [], + reminders: reminders.map { Int64($0) }, + attendees: [], description: description, - url: url - ) - } -} - -class MockAttendee { - let name: String - let email: String - let type: Int64 - let role: Int64 - let status: Int64 - - init(name: String, email: String, type: Int64, role: Int64, status: Int64) { - self.name = name - self.email = email - self.type = type - self.role = role - self.status = status - } - - fileprivate func toAttendee() -> Attendee { - return Attendee( - name: name, - email: email, - type: type, - role: role, - status: status + url: url, + location: location ) } } - -// MARK: - Specialized Mocks for Native Platform Testing - -/// Mock that simulates user canceling the native event creation -class MockEasyEventStoreCanceled: MockEasyEventStore { - override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { - // Simulate user cancellation - completion(.failure(PigeonError( - code: "USER_CANCELED", - message: "User canceled event creation", - details: nil - ))) - } -} - -/// Mock that simulates presentation error -class MockEasyEventStorePresentationError: MockEasyEventStore { - override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { - // Simulate presentation error - completion(.failure(PigeonError( - code: "PRESENTATION_ERROR", - message: "Unable to present event creation view", - details: nil - ))) - } -} - -/// Mock that simulates event deletion during creation -class MockEasyEventStoreEventDeleted: MockEasyEventStore { - override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { - // Simulate event deletion - completion(.failure(PigeonError( - code: "EVENT_DELETED", - message: "Event was deleted", - details: nil - ))) - } -} - -/// Mock that simulates unknown action -class MockEasyEventStoreUnknownAction: MockEasyEventStore { - override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { - // Simulate unknown action - completion(.failure(PigeonError( - code: "GENERIC_ERROR", - message: "Unknown action from event edit controller", - details: nil - ))) - } -} diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 7c569640..391a902b 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 12.0 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 0d71b5d1..253f0bd9 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ EA8AABCC2D8D889600A1D69B /* CalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTests.swift; sourceTree = ""; }; EA8AABD02D8D891A00A1D69B /* EventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTests.swift; sourceTree = ""; }; EAECB9432D395CA3000FAA80 /* UtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilsTests.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -101,6 +102,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, diff --git a/example/more-complex/full-permission/ios/Flutter/AppFrameworkInfo.plist b/example/more-complex/full-permission/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf76..391a902b 100644 --- a/example/more-complex/full-permission/ios/Flutter/AppFrameworkInfo.plist +++ b/example/more-complex/full-permission/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart b/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart index c51d516d..4b5b6652 100644 --- a/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart +++ b/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart @@ -161,4 +161,67 @@ final class CalendarCubit extends Cubit { emit(Value.success(data.copyWith(visibleCalendarIds: newVisibleIds))); } } + + Future updateCalendar({ + required ETCalendar calendar, + required String title, + required Color color, + }) async { + if (state case Value(:final data?)) { + await state.fetchFrom(() async { + final updatedCalendar = await _eventide.updateCalendar( + calendar, + title: title, + color: color, + ); + + final updatedCalendars = >{}; + for (final entry in data.calendars.entries) { + if (entry.key.id == calendar.id) { + updatedCalendars[updatedCalendar] = entry.value; + } else { + updatedCalendars[entry.key] = entry.value; + } + } + + return data.copyWith(calendars: updatedCalendars); + }).forEach(emit); + } + } + + Future updateEvent({ + required ETCalendar calendar, + required ETEvent event, + required String title, + required String description, + required bool isAllDay, + required TZDateTime startDate, + required TZDateTime endDate, + }) async { + if (state case Value(:final data?)) { + await state.fetchFrom(() async { + final updatedEvent = await _eventide.updateEvent( + event, + title: title, + description: description, + isAllDay: isAllDay, + startDate: startDate.millisecondsSinceEpoch, + endDate: endDate.millisecondsSinceEpoch, + ); + + final updatedCalendars = Map>.from( + data.calendars.map((k, v) => MapEntry(k, v.toList())), + ); + final events = updatedCalendars[calendar]; + if (events != null) { + final index = events.indexWhere((e) => e.id == event.id); + if (index != -1) { + events[index] = updatedEvent; + } + } + + return data.copyWith(calendars: updatedCalendars); + }).forEach(emit); + } + } } diff --git a/example/more-complex/full-permission/lib/calendar/ui/components/calendar_form.dart b/example/more-complex/full-permission/lib/calendar/ui/components/calendar_form.dart index d341614a..f77341d7 100644 --- a/example/more-complex/full-permission/lib/calendar/ui/components/calendar_form.dart +++ b/example/more-complex/full-permission/lib/calendar/ui/components/calendar_form.dart @@ -8,10 +8,12 @@ typedef OnCalendarFormSubmit = void Function(String title, Color color, ETAccoun class CalendarForm extends StatefulWidget { final OnCalendarFormSubmit onSubmit; final Iterable availableAccounts; + final ETCalendar? initialCalendar; const CalendarForm({ required this.onSubmit, this.availableAccounts = const [], + this.initialCalendar, super.key, }); @@ -28,8 +30,8 @@ class _CalendarFormState extends State { void initState() { super.initState(); - _titleController = TextEditingController(); - selectedColor = Colors.red; + _titleController = TextEditingController(text: widget.initialCalendar?.title ?? ''); + selectedColor = widget.initialCalendar?.color ?? Colors.red; // Sélectionner le premier compte par défaut s'il y en a if (widget.availableAccounts.isNotEmpty) { @@ -203,7 +205,7 @@ class _CalendarFormState extends State { backgroundColor: selectedColor, foregroundColor: Colors.white, ), - child: const Text('Create Calendar'), + child: Text(widget.initialCalendar != null ? 'Update Calendar' : 'Create Calendar'), ), ], ), diff --git a/example/more-complex/full-permission/lib/calendar/ui/components/custom_drawer.dart b/example/more-complex/full-permission/lib/calendar/ui/components/custom_drawer.dart index 3c6f72af..932dec91 100644 --- a/example/more-complex/full-permission/lib/calendar/ui/components/custom_drawer.dart +++ b/example/more-complex/full-permission/lib/calendar/ui/components/custom_drawer.dart @@ -142,13 +142,75 @@ class CustomDrawer extends StatelessWidget { onChanged: (bool? value) { BlocProvider.of(context).toggleCalendarVisibility(calendar.id); }, - secondary: Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: calendar.color, - shape: BoxShape.circle, - ), + secondary: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: calendar.color, + shape: BoxShape.circle, + ), + ), + if (calendar.isWritable) + IconButton( + icon: const Icon(Icons.edit, size: 18), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + onPressed: () { + final calendarCubit = BlocProvider.of(context); + + showDialog( + context: context, + builder: (dialogContext) { + return AlertDialog( + title: const Text('Edit Calendar'), + content: CalendarForm( + availableAccounts: const [], + initialCalendar: calendar, + onSubmit: (title, color, account) async { + try { + await calendarCubit.updateCalendar( + calendar: calendar, + title: title, + color: color, + ); + + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Calendar "$title" updated successfully!'), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } + + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating calendar: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + ); + }, + ); + }, + ), + ], ), title: Text( calendar.title, diff --git a/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart b/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart index 14aa88e1..fe81f2bc 100644 --- a/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart +++ b/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart @@ -15,11 +15,15 @@ final class EventForm extends StatefulWidget { final List calendars; final OnEventFormSubmit onSubmit; final DateTime? initialDate; + final ETEvent? initialEvent; + final ETCalendar? initialCalendar; const EventForm({ required this.calendars, required this.onSubmit, this.initialDate, + this.initialEvent, + this.initialCalendar, super.key, }); @@ -38,10 +42,15 @@ final class _EventFormState extends State { @override void initState() { super.initState(); - _titleController = TextEditingController(); - _descriptionController = TextEditingController(); + _titleController = TextEditingController(text: widget.initialEvent?.title ?? ''); + _descriptionController = TextEditingController(text: widget.initialEvent?.description ?? ''); + _selectedCalendar = widget.initialCalendar; - if (widget.initialDate != null) { + if (widget.initialEvent != null) { + _selectedStartDate = widget.initialEvent!.startDate; + _selectedEndDate = widget.initialEvent!.endDate; + isAllDay = widget.initialEvent!.isAllDay; + } else if (widget.initialDate != null) { _selectedStartDate = widget.initialDate!; _selectedEndDate = widget.initialDate!.add(const Duration(hours: 1)); } else { @@ -122,10 +131,13 @@ final class _EventFormState extends State { child: ElevatedButton( onPressed: () async { final lastDate = _selectedEndDate; + final firstDate = widget.initialEvent != null + ? DateTime(2000) + : DateTime.now(); final pickedDate = await showDatePicker( context: context, initialDate: _selectedStartDate, - firstDate: DateTime.now(), + firstDate: firstDate, lastDate: lastDate, ); @@ -173,7 +185,7 @@ final class _EventFormState extends State { final firstDate = _selectedStartDate; final pickedDate = await showDatePicker( context: context, - initialDate: _selectedEndDate, + initialDate: _selectedEndDate.isBefore(firstDate) ? firstDate : _selectedEndDate, firstDate: firstDate, lastDate: firstDate.add(const Duration(days: 365)), ); @@ -226,9 +238,9 @@ final class _EventFormState extends State { ); } : null, - child: const Text('Create event'), + child: Text(widget.initialEvent != null ? 'Update event' : 'Create event'), ), - if (!isAllDay) ...[ + if (!isAllDay && widget.initialEvent == null) ...[ const SizedBox(height: 16), ElevatedButton( onPressed: widget.calendars.isEmpty || _selectedCalendar != null diff --git a/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart b/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart index bd6b8928..76bd7e6b 100644 --- a/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart +++ b/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart @@ -79,6 +79,7 @@ final class _CalendarScreenState extends State with SingleTicker event: event, title: event.title, date: event.startDate, + endDate: event.endDate, color: calendar.color, startTime: event.startDate, endTime: event.endDate, @@ -132,6 +133,7 @@ final class _CalendarScreenState extends State with SingleTicker if (event is ETEvent) { final relatedCalendar = data.calendars.keys.singleWhere((calendar) => calendar.id == event.calendarId); + final calendarCubit = BlocProvider.of(context); Navigator.of(context).push( MaterialPageRoute( @@ -140,7 +142,7 @@ final class _CalendarScreenState extends State with SingleTicker isCalendarWritable: relatedCalendar.isWritable, ), ), - ); + ).then((_) => calendarCubit.loadFullContent()); } } }, diff --git a/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart b/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart index 8f41189e..c7151a73 100644 --- a/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart +++ b/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart @@ -1,5 +1,6 @@ import 'package:eventide/eventide.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:timezone/timezone.dart'; import 'package:value_state/value_state.dart'; final class EventDetailsCubit extends Cubit> { @@ -46,4 +47,25 @@ final class EventDetailsCubit extends Cubit> { }).forEach(emit); } } + + Future updateEvent({ + required String title, + required String description, + required bool isAllDay, + required TZDateTime startDate, + required TZDateTime endDate, + }) async { + if (state case Value(:final data?)) { + await state.fetchFrom(() async { + return await _calendarPlugin.updateEvent( + data, + title: title, + description: description, + isAllDay: isAllDay, + startDate: startDate.millisecondsSinceEpoch, + endDate: endDate.millisecondsSinceEpoch, + ); + }).forEach(emit); + } + } } diff --git a/example/more-complex/full-permission/lib/event_details/ui/views/event_details_screen.dart b/example/more-complex/full-permission/lib/event_details/ui/views/event_details_screen.dart index 5f2af8a8..83f45e5d 100644 --- a/example/more-complex/full-permission/lib/event_details/ui/views/event_details_screen.dart +++ b/example/more-complex/full-permission/lib/event_details/ui/views/event_details_screen.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:math'; import 'package:eventide/eventide.dart'; +import 'package:eventide_example_full_permission/calendar/ui/components/event_form.dart'; import 'package:eventide_example_full_permission/event_details/logic/event_details_cubit.dart'; import 'package:eventide_example_full_permission/event_details/ui/components/attendee_form.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,59 @@ final class EventDetailsScreen extends StatelessWidget { child: Scaffold( appBar: AppBar( title: Text(event.title), + actions: [ + if (isCalendarWritable) + Builder( + builder: (context) => IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + final cubit = context.read(); + final currentEvent = (context.read().state).data ?? event; + + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Edit event'), + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: EventForm( + calendars: const [], + initialEvent: currentEvent, + onSubmit: (_, title, description, isAllDay, startDate, endDate) async { + try { + await cubit.updateEvent( + title: title, + description: description, + isAllDay: isAllDay, + startDate: startDate, + endDate: endDate, + ); + + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } + } catch (e) { + if (dialogContext.mounted) { + Navigator.of(dialogContext).pop(); + } + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error updating event: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + }, + ), + ), + ), + ); + }, + ), + ), + ], ), body: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), diff --git a/ios/eventide/Sources/eventide/CalendarApi.g.swift b/ios/eventide/Sources/eventide/CalendarApi.g.swift index a2ab5664..43d49905 100644 --- a/ios/eventide/Sources/eventide/CalendarApi.g.swift +++ b/ios/eventide/Sources/eventide/CalendarApi.g.swift @@ -1,4 +1,4 @@ -// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon import Foundation @@ -50,7 +50,7 @@ private func wrapError(_ error: Any) -> [Any?] { } return [ "\(error)", - "\(type(of: error))", + "\(Swift.type(of: error))", "Stacktrace: \(Thread.callStackSymbols)", ] } @@ -64,6 +64,19 @@ private func nilOrValue(_ value: Any?) -> T? { return value as! T? } +private func doubleEqualsCalendarApi(_ lhs: Double, _ rhs: Double) -> Bool { + return (lhs.isNaN && rhs.isNaN) || lhs == rhs +} + +private func doubleHashCalendarApi(_ value: Double, _ hasher: inout Hasher) { + if value.isNaN { + hasher.combine(0x7FF8000000000000) + } else { + // Normalize -0.0 to 0.0 + hasher.combine(value == 0 ? 0 : value) + } +} + func deepEqualsCalendarApi(_ lhs: Any?, _ rhs: Any?) -> Bool { let cleanLhs = nilOrValue(lhs) as Any? let cleanRhs = nilOrValue(rhs) as Any? @@ -74,59 +87,92 @@ func deepEqualsCalendarApi(_ lhs: Any?, _ rhs: Any?) -> Bool { case (nil, _), (_, nil): return false - case is (Void, Void): + case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs: return true - case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable): - return cleanLhsHashable == cleanRhsHashable + case is (Void, Void): + return true - case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]): - guard cleanLhsArray.count == cleanRhsArray.count else { return false } - for (index, element) in cleanLhsArray.enumerated() { - if !deepEqualsCalendarApi(element, cleanRhsArray[index]) { + case (let lhsArray, let rhsArray) as ([Any?], [Any?]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !deepEqualsCalendarApi(element, rhsArray[index]) { return false } } return true - case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): - guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false } - for (key, cleanLhsValue) in cleanLhsDictionary { - guard cleanRhsDictionary.index(forKey: key) != nil else { return false } - if !deepEqualsCalendarApi(cleanLhsValue, cleanRhsDictionary[key]!) { + case (let lhsArray, let rhsArray) as ([Double], [Double]): + guard lhsArray.count == rhsArray.count else { return false } + for (index, element) in lhsArray.enumerated() { + if !doubleEqualsCalendarApi(element, rhsArray[index]) { return false } } return true + case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]): + guard lhsDictionary.count == rhsDictionary.count else { return false } + for (lhsKey, lhsValue) in lhsDictionary { + var found = false + for (rhsKey, rhsValue) in rhsDictionary { + if deepEqualsCalendarApi(lhsKey, rhsKey) { + if deepEqualsCalendarApi(lhsValue, rhsValue) { + found = true + break + } else { + return false + } + } + } + if !found { return false } + } + return true + + case (let lhs as Double, let rhs as Double): + return doubleEqualsCalendarApi(lhs, rhs) + + case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable): + return lhsHashable == rhsHashable + default: - // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue. return false } } func deepHashCalendarApi(value: Any?, hasher: inout Hasher) { - if let valueList = value as? [AnyHashable] { - for item in valueList { deepHashCalendarApi(value: item, hasher: &hasher) } - return - } - - if let valueDict = value as? [AnyHashable: AnyHashable] { - for key in valueDict.keys { - hasher.combine(key) - deepHashCalendarApi(value: valueDict[key]!, hasher: &hasher) + let cleanValue = nilOrValue(value) as Any? + if let cleanValue = cleanValue { + if let doubleValue = cleanValue as? Double { + doubleHashCalendarApi(doubleValue, &hasher) + } else if let valueList = cleanValue as? [Any?] { + for item in valueList { + deepHashCalendarApi(value: item, hasher: &hasher) + } + } else if let valueList = cleanValue as? [Double] { + for item in valueList { + doubleHashCalendarApi(item, &hasher) + } + } else if let valueDict = cleanValue as? [AnyHashable: Any?] { + var result = 0 + for (key, value) in valueDict { + var entryKeyHasher = Hasher() + deepHashCalendarApi(value: key, hasher: &entryKeyHasher) + var entryValueHasher = Hasher() + deepHashCalendarApi(value: value, hasher: &entryValueHasher) + result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize()) + } + hasher.combine(result) + } else if let hashableValue = cleanValue as? AnyHashable { + hasher.combine(hashableValue) + } else { + hasher.combine(String(describing: cleanValue)) } - return - } - - if let hashableValue = value as? AnyHashable { - hasher.combine(hashableValue.hashValue) + } else { + hasher.combine(0) } - - return hasher.combine(String(describing: value)) } - /// Generated class from Pigeon that represents data sent in messages. struct Calendar: Hashable { @@ -163,9 +209,19 @@ struct Calendar: Hashable { ] } static func == (lhs: Calendar, rhs: Calendar) -> Bool { - return deepEqualsCalendarApi(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsCalendarApi(lhs.id, rhs.id) && deepEqualsCalendarApi(lhs.title, rhs.title) && deepEqualsCalendarApi(lhs.color, rhs.color) && deepEqualsCalendarApi(lhs.isWritable, rhs.isWritable) && deepEqualsCalendarApi(lhs.account, rhs.account) + } + func hash(into hasher: inout Hasher) { - deepHashCalendarApi(value: toList(), hasher: &hasher) + hasher.combine("Calendar") + deepHashCalendarApi(value: id, hasher: &hasher) + deepHashCalendarApi(value: title, hasher: &hasher) + deepHashCalendarApi(value: color, hasher: &hasher) + deepHashCalendarApi(value: isWritable, hasher: &hasher) + deepHashCalendarApi(value: account, hasher: &hasher) } } @@ -228,9 +284,25 @@ struct Event: Hashable { ] } static func == (lhs: Event, rhs: Event) -> Bool { - return deepEqualsCalendarApi(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsCalendarApi(lhs.id, rhs.id) && deepEqualsCalendarApi(lhs.calendarId, rhs.calendarId) && deepEqualsCalendarApi(lhs.title, rhs.title) && deepEqualsCalendarApi(lhs.isAllDay, rhs.isAllDay) && deepEqualsCalendarApi(lhs.startDate, rhs.startDate) && deepEqualsCalendarApi(lhs.endDate, rhs.endDate) && deepEqualsCalendarApi(lhs.reminders, rhs.reminders) && deepEqualsCalendarApi(lhs.attendees, rhs.attendees) && deepEqualsCalendarApi(lhs.description, rhs.description) && deepEqualsCalendarApi(lhs.url, rhs.url) && deepEqualsCalendarApi(lhs.location, rhs.location) + } + func hash(into hasher: inout Hasher) { - deepHashCalendarApi(value: toList(), hasher: &hasher) + hasher.combine("Event") + deepHashCalendarApi(value: id, hasher: &hasher) + deepHashCalendarApi(value: calendarId, hasher: &hasher) + deepHashCalendarApi(value: title, hasher: &hasher) + deepHashCalendarApi(value: isAllDay, hasher: &hasher) + deepHashCalendarApi(value: startDate, hasher: &hasher) + deepHashCalendarApi(value: endDate, hasher: &hasher) + deepHashCalendarApi(value: reminders, hasher: &hasher) + deepHashCalendarApi(value: attendees, hasher: &hasher) + deepHashCalendarApi(value: description, hasher: &hasher) + deepHashCalendarApi(value: url, hasher: &hasher) + deepHashCalendarApi(value: location, hasher: &hasher) } } @@ -261,9 +333,17 @@ struct Account: Hashable { ] } static func == (lhs: Account, rhs: Account) -> Bool { - return deepEqualsCalendarApi(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsCalendarApi(lhs.id, rhs.id) && deepEqualsCalendarApi(lhs.name, rhs.name) && deepEqualsCalendarApi(lhs.type, rhs.type) + } + func hash(into hasher: inout Hasher) { - deepHashCalendarApi(value: toList(), hasher: &hasher) + hasher.combine("Account") + deepHashCalendarApi(value: id, hasher: &hasher) + deepHashCalendarApi(value: name, hasher: &hasher) + deepHashCalendarApi(value: type, hasher: &hasher) } } @@ -302,9 +382,19 @@ struct Attendee: Hashable { ] } static func == (lhs: Attendee, rhs: Attendee) -> Bool { - return deepEqualsCalendarApi(lhs.toList(), rhs.toList()) } + if Swift.type(of: lhs) != Swift.type(of: rhs) { + return false + } + return deepEqualsCalendarApi(lhs.name, rhs.name) && deepEqualsCalendarApi(lhs.email, rhs.email) && deepEqualsCalendarApi(lhs.type, rhs.type) && deepEqualsCalendarApi(lhs.role, rhs.role) && deepEqualsCalendarApi(lhs.status, rhs.status) + } + func hash(into hasher: inout Hasher) { - deepHashCalendarApi(value: toList(), hasher: &hasher) + hasher.combine("Attendee") + deepHashCalendarApi(value: name, hasher: &hasher) + deepHashCalendarApi(value: email, hasher: &hasher) + deepHashCalendarApi(value: type, hasher: &hasher) + deepHashCalendarApi(value: role, hasher: &hasher) + deepHashCalendarApi(value: status, hasher: &hasher) } } @@ -363,10 +453,12 @@ class CalendarApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable { /// Generated protocol from Pigeon that represents a handler of messages from Flutter. protocol CalendarApi { func createCalendar(title: String, color: Int64, in account: Account?, completion: @escaping (Result) -> Void) + func updateCalendar(withId calendarId: String, title: String, color: Int64, completion: @escaping (Result) -> Void) func retrieveCalendars(onlyWritable onlyWritableCalendars: Bool, from account: Account?, completion: @escaping (Result<[Calendar], Error>) -> Void) func retrieveAccounts(completion: @escaping (Result<[Account], Error>) -> Void) func deleteCalendar(_ calendarId: String, completion: @escaping (Result) -> Void) func createEvent(calendarId: String, title: String, startDate: Int64, endDate: Int64, isAllDay: Bool, description: String?, url: String?, location: String?, reminders: [Int64]?, completion: @escaping (Result) -> Void) + func updateEvent(withId eventId: String, calendarId: String, title: String, startDate: Int64, endDate: Int64, isAllDay: Bool, description: String?, url: String?, location: String?, reminders: [Int64]?, completion: @escaping (Result) -> Void) func createEventInDefaultCalendar(title: String, startDate: Int64, endDate: Int64, isAllDay: Bool, description: String?, url: String?, location: String?, reminders: [Int64]?, completion: @escaping (Result) -> Void) func createEventThroughNativePlatform(title: String?, startDate: Int64?, endDate: Int64?, isAllDay: Bool?, description: String?, url: String?, location: String?, reminders: [Int64]?, completion: @escaping (Result) -> Void) func retrieveEvents(calendarId: String, startDate: Int64, endDate: Int64, completion: @escaping (Result<[Event], Error>) -> Void) @@ -402,6 +494,25 @@ class CalendarApiSetup { } else { createCalendarChannel.setMessageHandler(nil) } + let updateCalendarChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.eventide.CalendarApi.updateCalendar\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + updateCalendarChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let calendarIdArg = args[0] as! String + let titleArg = args[1] as! String + let colorArg = args[2] as! Int64 + api.updateCalendar(withId: calendarIdArg, title: titleArg, color: colorArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + updateCalendarChannel.setMessageHandler(nil) + } let retrieveCalendarsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.eventide.CalendarApi.retrieveCalendars\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { retrieveCalendarsChannel.setMessageHandler { message, reply in @@ -477,6 +588,32 @@ class CalendarApiSetup { } else { createEventChannel.setMessageHandler(nil) } + let updateEventChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.eventide.CalendarApi.updateEvent\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + updateEventChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let eventIdArg = args[0] as! String + let calendarIdArg = args[1] as! String + let titleArg = args[2] as! String + let startDateArg = args[3] as! Int64 + let endDateArg = args[4] as! Int64 + let isAllDayArg = args[5] as! Bool + let descriptionArg: String? = nilOrValue(args[6]) + let urlArg: String? = nilOrValue(args[7]) + let locationArg: String? = nilOrValue(args[8]) + let remindersArg: [Int64]? = nilOrValue(args[9]) + api.updateEvent(withId: eventIdArg, calendarId: calendarIdArg, title: titleArg, startDate: startDateArg, endDate: endDateArg, isAllDay: isAllDayArg, description: descriptionArg, url: urlArg, location: locationArg, reminders: remindersArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + updateEventChannel.setMessageHandler(nil) + } let createEventInDefaultCalendarChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.eventide.CalendarApi.createEventInDefaultCalendar\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) if let api = api { createEventInDefaultCalendarChannel.setMessageHandler { message, reply in diff --git a/ios/eventide/Sources/eventide/CalendarImplem.swift b/ios/eventide/Sources/eventide/CalendarImplem.swift index 952054f0..fea19ca8 100644 --- a/ios/eventide/Sources/eventide/CalendarImplem.swift +++ b/ios/eventide/Sources/eventide/CalendarImplem.swift @@ -41,16 +41,15 @@ class CalendarImplem: CalendarApi { } onPermissionError: { error in completion(.failure(error)) } - } func retrieveCalendars( - onlyWritable: Bool, + onlyWritable onlyWritableCalendars: Bool, from account: Account?, completion: @escaping (Result<[Calendar], Error>) -> Void ) { permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in - let calendars = easyEventStore.retrieveCalendars(onlyWritable: onlyWritable, from: account) + let calendars = easyEventStore.retrieveCalendars(onlyWritable: onlyWritableCalendars, from: account) completion(.success(calendars)) } onPermissionRefused: { @@ -80,7 +79,28 @@ class CalendarImplem: CalendarApi { } } - func deleteCalendar(_ calendarId: String, completion: @escaping (Result) -> Void) { + func updateCalendar(withId calendarId: String, title: String, color: Int64, completion: @escaping (Result) -> Void) { + permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in + do { + let calendar = try easyEventStore.updateCalendar(calendarId: calendarId, title: title, color: UIColor(int64: color)) + completion(.success(calendar)) + + } catch { + completion(.failure(error)) + } + + } onPermissionRefused: { + completion(.failure(PigeonError( + code: "ACCESS_REFUSED", + message: "Calendar access has been refused or has not been given yet", + details: nil + ))) + } onPermissionError: { error in + completion(.failure(error)) + } + } + + func deleteCalendar(_ calendarId: String, completion: @escaping (Result) -> Void) { permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in do { try easyEventStore.deleteCalendar(calendarId: calendarId) @@ -151,7 +171,7 @@ class CalendarImplem: CalendarApi { url: String?, location: String?, reminders: [Int64]?, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { permissionHandler.checkCalendarAccessThenExecute(.writeOnly) { [self] in do { @@ -192,7 +212,7 @@ class CalendarImplem: CalendarApi { url: String?, location: String?, reminders: [Int64]?, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { easyEventStore.presentEventCreationViewController( title: title, @@ -212,7 +232,7 @@ class CalendarImplem: CalendarApi { calendarId: String, startDate: Int64, endDate: Int64, - completion: @escaping (Result<[Event], any Error>) -> Void + completion: @escaping (Result<[Event], Error>) -> Void ) { permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in do { @@ -238,7 +258,51 @@ class CalendarImplem: CalendarApi { } } - func deleteEvent(withId eventId: String, completion: @escaping (Result) -> Void) { + func updateEvent( + withId eventId: String, + calendarId: String, + title: String, + startDate: Int64, + endDate: Int64, + isAllDay: Bool, + description: String?, + url: String?, + location: String?, + reminders: [Int64]?, + completion: @escaping (Result) -> Void + ) { + permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in + do { + let event = try easyEventStore.updateEvent( + eventId: eventId, + calendarId: calendarId, + title: title, + startDate: Date(from: startDate), + endDate: Date(from: endDate), + isAllDay: isAllDay, + description: description, + url: url, + location: location, + timeIntervals: reminders?.compactMap { TimeInterval(-$0) } + ) + completion(.success(event)) + + } catch { + completion(.failure(error)) + } + + } onPermissionRefused: { + completion(.failure(PigeonError( + code: "ACCESS_REFUSED", + message: "Calendar access has been refused or has not been given yet", + details: nil + ))) + } onPermissionError: { error in + completion(.failure(error)) + } + } + + func deleteEvent(withId eventId: String, completion: @escaping (Result) -> Void) { permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in do { try easyEventStore.deleteEvent(eventId: eventId) @@ -259,7 +323,7 @@ class CalendarImplem: CalendarApi { } } - func createReminder(_ reminder: Int64, forEventId eventId: String, completion: @escaping (Result) -> Void) { + func createReminder(_ reminder: Int64, forEventId eventId: String, completion: @escaping (Result) -> Void) { permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in do { let modifiedEvent = try easyEventStore.createReminder(timeInterval: TimeInterval(-reminder), eventId: eventId) @@ -281,7 +345,7 @@ class CalendarImplem: CalendarApi { } - func deleteReminder(_ reminder: Int64, withEventId eventId: String, completion: @escaping (Result) -> Void) { + func deleteReminder(_ reminder: Int64, withEventId eventId: String, completion: @escaping (Result) -> Void) { permissionHandler.checkCalendarAccessThenExecute(.fullAccess) { [self] in do { let modifiedEvent = try easyEventStore.deleteReminder(timeInterval: TimeInterval(-reminder), eventId: eventId) @@ -308,7 +372,7 @@ class CalendarImplem: CalendarApi { email: String, role: Int64, type: Int64, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { /// EventKit cannot add participants to an event nor change participant information. /// https://developer.apple.com/documentation/eventkit/ekparticipant#overview @@ -324,7 +388,7 @@ class CalendarImplem: CalendarApi { func deleteAttendee( eventId: String, email: String, - completion: @escaping (Result) -> Void + completion: @escaping (Result) -> Void ) { /// EventKit cannot add participants to an event nor change participant information. /// https://developer.apple.com/documentation/eventkit/ekparticipant#overview diff --git a/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift b/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift index 93cc47c6..02c3ab02 100644 --- a/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift +++ b/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift @@ -77,6 +77,40 @@ final class EasyEventStore: EasyEventStoreProtocol { } } + func updateCalendar(calendarId: String, title: String, color: UIColor) throws -> Calendar { + guard let ekCalendar = eventStore.calendar(withIdentifier: calendarId) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Calendar not found", + details: "The provided calendar.id is certainly incorrect" + ) + } + + guard ekCalendar.allowsContentModifications else { + throw PigeonError( + code: "NOT_EDITABLE", + message: "Calendar not editable", + details: "Calendar does not allow content modifications" + ) + } + + ekCalendar.title = title + ekCalendar.cgColor = color.cgColor + + do { + try eventStore.saveCalendar(ekCalendar, commit: true) + return ekCalendar.toCalendar() + + } catch { + eventStore.reset() + throw PigeonError( + code: "GENERIC_ERROR", + message: "Error while updating calendar", + details: error.localizedDescription + ) + } + } + func deleteCalendar(calendarId: String) throws { guard let calendar = eventStore.calendar(withIdentifier: calendarId) else { throw PigeonError( @@ -237,6 +271,67 @@ final class EasyEventStore: EasyEventStoreProtocol { return eventStore.events(matching: predicate).map { $0.toEvent() } } + func updateEvent( + eventId: String, + calendarId: String, + title: String, + startDate: Date, + endDate: Date, + isAllDay: Bool, + description: String?, + url: String?, + location: String?, + timeIntervals: [TimeInterval]? + ) throws -> Event { + guard let ekEvent = eventStore.event(withIdentifier: eventId) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Event not found", + details: "The provided event.id is certainly incorrect" + ) + } + + guard let ekCalendar = eventStore.calendar(withIdentifier: calendarId) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Calendar not found", + details: "The provided calendar.id is certainly incorrect" + ) + } + + ekEvent.calendar = ekCalendar + ekEvent.title = title + ekEvent.notes = description + ekEvent.startDate = startDate + ekEvent.endDate = endDate + ekEvent.timeZone = TimeZone(identifier: "UTC") + ekEvent.isAllDay = isAllDay + ekEvent.location = location + + if let urlString = url { + ekEvent.url = URL(string: urlString) + } else { + ekEvent.url = nil + } + + if let timeIntervals = timeIntervals { + ekEvent.alarms = timeIntervals.map({ EKAlarm(relativeOffset: $0) }) + } + + do { + try eventStore.save(ekEvent, span: EKSpan.thisEvent, commit: true) + return ekEvent.toEvent() + + } catch { + eventStore.reset() + throw PigeonError( + code: "GENERIC_ERROR", + message: "Event not updated", + details: error.localizedDescription + ) + } + } + func deleteEvent(eventId: String) throws { guard let event = eventStore.event(withIdentifier: eventId) else { throw PigeonError( diff --git a/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStoreProtocol.swift b/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStoreProtocol.swift index f7603b63..52de066f 100644 --- a/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStoreProtocol.swift +++ b/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStoreProtocol.swift @@ -15,6 +15,8 @@ protocol EasyEventStoreProtocol { func retrieveAccounts() -> [Account] + func updateCalendar(calendarId: String, title: String, color: UIColor) throws -> Calendar + func deleteCalendar(calendarId: String) throws -> Void func createEvent(calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event @@ -34,6 +36,8 @@ protocol EasyEventStoreProtocol { ) func retrieveEvents(calendarId: String, startDate: Date, endDate: Date) throws -> [Event] + + func updateEvent(eventId: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event func deleteEvent(eventId: String) throws -> Void diff --git a/lib/src/calendar_api.g.dart b/lib/src/calendar_api.g.dart index 30a94d35..34d7a210 100644 --- a/lib/src/calendar_api.g.dart +++ b/lib/src/calendar_api.g.dart @@ -1,34 +1,102 @@ -// Autogenerated from Pigeon (v26.1.7), do not edit directly. +// Autogenerated from Pigeon (v26.3.4), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers +// ignore_for_file: unused_import, unused_shown_name +// ignore_for_file: type=lint import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; +import 'dart:typed_data' show Float64List, Int32List, Int64List; -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; - -PlatformException _createConnectionError(String channelName) { - return PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel: "$channelName".', - ); +import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; + +Object? _extractReplyValueOrThrow( + List? replyList, + String channelName, { + required bool isNullValid, +}) { + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } + return replyList.firstOrNull; } bool _deepEquals(Object? a, Object? b) { + if (identical(a, b)) { + return true; + } + if (a is double && b is double) { + if (a.isNaN && b.isNaN) { + return true; + } + return a == b; + } if (a is List && b is List) { - return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + return a.length == b.length && + a.indexed + .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { - return a.length == b.length && - a.entries.every( - (MapEntry entry) => - (b as Map).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]), - ); + if (a.length != b.length) { + return false; + } + for (final MapEntry entryA in a.entries) { + bool found = false; + for (final MapEntry entryB in b.entries) { + if (_deepEquals(entryA.key, entryB.key)) { + if (_deepEquals(entryA.value, entryB.value)) { + found = true; + break; + } else { + return false; + } + } + } + if (!found) { + return false; + } + } + return true; } return a == b; } +int _deepHash(Object? value) { + if (value is List) { + return Object.hashAll(value.map(_deepHash)); + } + if (value is Map) { + int result = 0; + for (final MapEntry entry in value.entries) { + result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value); + } + return result; + } + if (value is double && value.isNaN) { + // Normalize NaN to a consistent hash. + return 0x7FF8000000000000.hashCode; + } + if (value is double && value == 0.0) { + // Normalize -0.0 to 0.0 so they have the same hash code. + return 0.0.hashCode; + } + return value.hashCode; +} + + class Calendar { Calendar({ required this.id, @@ -49,12 +117,17 @@ class Calendar { Account account; List _toList() { - return [id, title, color, isWritable, account]; + return [ + id, + title, + color, + isWritable, + account, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static Calendar decode(Object result) { result as List; @@ -76,12 +149,12 @@ class Calendar { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(id, other.id) && _deepEquals(title, other.title) && _deepEquals(color, other.color) && _deepEquals(isWritable, other.isWritable) && _deepEquals(account, other.account); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class Event { @@ -138,8 +211,7 @@ class Event { } Object encode() { - return _toList(); - } + return _toList(); } static Event decode(Object result) { result as List; @@ -150,8 +222,8 @@ class Event { isAllDay: result[3]! as bool, startDate: result[4]! as int, endDate: result[5]! as int, - reminders: (result[6] as List?)!.cast(), - attendees: (result[7] as List?)!.cast(), + reminders: (result[6]! as List).cast(), + attendees: (result[7]! as List).cast(), description: result[8] as String?, url: result[9] as String?, location: result[10] as String?, @@ -167,16 +239,20 @@ class Event { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(id, other.id) && _deepEquals(calendarId, other.calendarId) && _deepEquals(title, other.title) && _deepEquals(isAllDay, other.isAllDay) && _deepEquals(startDate, other.startDate) && _deepEquals(endDate, other.endDate) && _deepEquals(reminders, other.reminders) && _deepEquals(attendees, other.attendees) && _deepEquals(description, other.description) && _deepEquals(url, other.url) && _deepEquals(location, other.location); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class Account { - Account({required this.id, required this.name, required this.type}); + Account({ + required this.id, + required this.name, + required this.type, + }); String id; @@ -185,16 +261,23 @@ class Account { String type; List _toList() { - return [id, name, type]; + return [ + id, + name, + type, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static Account decode(Object result) { result as List; - return Account(id: result[0]! as String, name: result[1]! as String, type: result[2]! as String); + return Account( + id: result[0]! as String, + name: result[1]! as String, + type: result[2]! as String, + ); } @override @@ -206,16 +289,22 @@ class Account { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(id, other.id) && _deepEquals(name, other.name) && _deepEquals(type, other.type); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } class Attendee { - Attendee({required this.name, required this.email, required this.type, required this.role, required this.status}); + Attendee({ + required this.name, + required this.email, + required this.type, + required this.role, + required this.status, + }); String name; @@ -228,12 +317,17 @@ class Attendee { int status; List _toList() { - return [name, email, type, role, status]; + return [ + name, + email, + type, + role, + status, + ]; } Object encode() { - return _toList(); - } + return _toList(); } static Attendee decode(Object result) { result as List; @@ -255,14 +349,15 @@ class Attendee { if (identical(this, other)) { return true; } - return _deepEquals(encode(), other.encode()); + return _deepEquals(name, other.name) && _deepEquals(email, other.email) && _deepEquals(type, other.type) && _deepEquals(role, other.role) && _deepEquals(status, other.status); } @override // ignore: avoid_equals_and_hash_code_on_mutable_classes - int get hashCode => Object.hashAll(_toList()); + int get hashCode => _deepHash([runtimeType, ..._toList()]); } + class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -270,16 +365,16 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is Calendar) { + } else if (value is Calendar) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is Event) { + } else if (value is Event) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is Account) { + } else if (value is Account) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is Attendee) { + } else if (value is Attendee) { buffer.putUint8(132); writeValue(buffer, value.encode()); } else { @@ -309,17 +404,16 @@ class CalendarApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. CalendarApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future createCalendar({required String title, required int color, required Account? account}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.createCalendar$pigeonVar_messageChannelSuffix'; + Future createCalendar({required String title, required int color, required Account? account, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -327,27 +421,37 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, color, account]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Calendar?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Calendar; + } + + Future updateCalendar({required String calendarId, required String title, required int color, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.updateCalendar$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([calendarId, title, color]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Calendar; } Future> retrieveCalendars({required bool onlyWritableCalendars, required Account? account}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.retrieveCalendars$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.retrieveCalendars$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -355,27 +459,18 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([onlyWritableCalendars, account]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as List?)!.cast(); - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return (pigeonVar_replyValue! as List).cast(); } Future> retrieveAccounts() async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.retrieveAccounts$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.retrieveAccounts$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -383,27 +478,18 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send(null); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as List?)!.cast(); - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return (pigeonVar_replyValue! as List).cast(); } Future deleteCalendar({required String calendarId}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.deleteCalendar$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.deleteCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -411,151 +497,91 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([calendarId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } - Future createEvent({ - required String calendarId, - required String title, - required int startDate, - required int endDate, - required bool isAllDay, - required String? description, - required String? url, - required String? location, - required List? reminders, - }) async { + Future createEvent({required String calendarId, required String title, required int startDate, required int endDate, required bool isAllDay, required String? description, required String? url, required String? location, required List? reminders, }) async { final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createEvent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - calendarId, - title, - startDate, - endDate, - isAllDay, - description, - url, - location, - reminders, - ]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([calendarId, title, startDate, endDate, isAllDay, description, url, location, reminders]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Event?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Event; } - Future createEventInDefaultCalendar({ - required String title, - required int startDate, - required int endDate, - required bool isAllDay, - required String? description, - required String? url, - required String? location, - required List? reminders, - }) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.createEventInDefaultCalendar$pigeonVar_messageChannelSuffix'; + Future updateEvent({required String eventId, required String calendarId, required String title, required int startDate, required int endDate, required bool isAllDay, required String? description, required String? url, required String? location, required List? reminders, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.updateEvent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - title, - startDate, - endDate, - isAllDay, - description, - url, - location, - reminders, - ]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([eventId, calendarId, title, startDate, endDate, isAllDay, description, url, location, reminders]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Event; } - Future createEventThroughNativePlatform({ - String? title, - int? startDate, - int? endDate, - bool? isAllDay, - String? description, - String? url, - String? location, - List? reminders, - }) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.createEventThroughNativePlatform$pigeonVar_messageChannelSuffix'; + Future createEventInDefaultCalendar({required String title, required int startDate, required int endDate, required bool isAllDay, required String? description, required String? url, required String? location, required List? reminders, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createEventInDefaultCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([ - title, - startDate, - endDate, - isAllDay, - description, - url, - location, - reminders, - ]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, startDate, endDate, isAllDay, description, url, location, reminders]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } - Future> retrieveEvents({required String calendarId, required int startDate, required int endDate}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.retrieveEvents$pigeonVar_messageChannelSuffix'; + Future createEventThroughNativePlatform({String? title, int? startDate, int? endDate, bool? isAllDay, String? description, String? url, String? location, List? reminders, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createEventThroughNativePlatform$pigeonVar_messageChannelSuffix'; + final pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, startDate, endDate, isAllDay, description, url, location, reminders]); + final pigeonVar_replyList = await pigeonVar_sendFuture as List?; + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; + } + + Future> retrieveEvents({required String calendarId, required int startDate, required int endDate, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.retrieveEvents$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -563,22 +589,14 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([calendarId, startDate, endDate]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as List?)!.cast(); - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return (pigeonVar_replyValue! as List).cast(); } Future deleteEvent({required String eventId}) async { @@ -590,22 +608,17 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([eventId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else { - return; - } + + _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: true, + ) + ; } Future createReminder({required int reminder, required String eventId}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.createReminder$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createReminder$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -613,27 +626,18 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([reminder, eventId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Event?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Event; } Future deleteReminder({required int reminder, required String eventId}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.deleteReminder$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.deleteReminder$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -641,33 +645,18 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([reminder, eventId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Event?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Event; } - Future createAttendee({ - required String eventId, - required String name, - required String email, - required int role, - required int type, - }) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.createAttendee$pigeonVar_messageChannelSuffix'; + Future createAttendee({required String eventId, required String name, required String email, required int role, required int type, }) async { + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createAttendee$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -675,27 +664,18 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([eventId, name, email, role, type]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Event?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Event; } Future deleteAttendee({required String eventId, required String email}) async { - final pigeonVar_channelName = - 'dev.flutter.pigeon.eventide.CalendarApi.deleteAttendee$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.deleteAttendee$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -703,21 +683,13 @@ class CalendarApi { ); final Future pigeonVar_sendFuture = pigeonVar_channel.send([eventId, email]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - if (pigeonVar_replyList == null) { - throw _createConnectionError(pigeonVar_channelName); - } else if (pigeonVar_replyList.length > 1) { - throw PlatformException( - code: pigeonVar_replyList[0]! as String, - message: pigeonVar_replyList[1] as String?, - details: pigeonVar_replyList[2], - ); - } else if (pigeonVar_replyList[0] == null) { - throw PlatformException( - code: 'null-error', - message: 'Host platform returned null value for non-null return value.', - ); - } else { - return (pigeonVar_replyList[0] as Event?)!; - } + + final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ) + ; + return pigeonVar_replyValue! as Event; } } diff --git a/lib/src/eventide.dart b/lib/src/eventide.dart index 4bd6c34b..d26db0bd 100644 --- a/lib/src/eventide.dart +++ b/lib/src/eventide.dart @@ -88,6 +88,32 @@ class Eventide extends EventidePlatform { } } + /// Updates the given [calendar] with optional new values for [title] and [color]. + /// Only the provided parameters will be updated, the others will remain unchanged. + /// + /// Returns the updated [ETCalendar]. + /// + /// Throws a [ETPermissionException] if the user refuses to grant calendar permissions. + /// + /// Throws a [ETNotFoundException] if the calendar with the given [calendar.id] is not found. + /// + /// Throws a [ETNotEditableException] if the calendar is not editable. + /// + /// Throws a [ETGenericException] if any other error occurs during calendar update. + @override + Future updateCalendar(ETCalendar calendar, {String? title, Color? color}) async { + try { + final updatedCalendar = await _calendarApi.updateCalendar( + calendarId: calendar.id, + title: title ?? calendar.title, + color: color?.toValue() ?? calendar.color.toValue(), + ); + return updatedCalendar.toETCalendar(); + } on PlatformException catch (e) { + throw e.toETException(); + } + } + /// Deletes the calendar with the given [calendarId]. /// /// Throws a [ETPermissionException] if the user refuses to grant calendar permissions. @@ -260,6 +286,49 @@ class Eventide extends EventidePlatform { } } + /// Updates the given [event] with optional new values for [calendarId], [title], [startDate], [endDate], [isAllDay], [description], [url], [location], and a list of [reminders] duration. + /// Only the provided parameters will be updated, the others will remain unchanged. + /// + /// Returns the updated [ETEvent]. + /// + /// Throws a [ETPermissionException] if the user refuses to grant calendar permissions. + /// + /// Throws a [ETNotFoundException] if the event with the given [event.id] is not found or if the new calendar with the given [calendarId] is not found. + /// + /// Throws a [ETNotEditableException] if the calendar is not editable. + /// + /// Throws a [ETGenericException] if any other error occurs during event update. + @override + Future updateEvent(ETEvent event, { + String? calendarId, + String? title, + int? startDate, + int? endDate, + bool? isAllDay, + String? description, + String? url, + String? location, + List? reminders, + }) async { + try { + final updatedEvent = await _calendarApi.updateEvent( + eventId: event.id, + calendarId: calendarId ?? event.calendarId, + title: title ?? event.title, + startDate: startDate ?? event.startDate.toUtc().millisecondsSinceEpoch, + endDate: endDate ?? event.endDate.toUtc().millisecondsSinceEpoch, + isAllDay: isAllDay ?? event.isAllDay, + description: description ?? event.description, + url: url ?? event.url, + location: location ?? event.location, + reminders: reminders ?? event.reminders.map((e) => e.toNativeDuration()).toList(), + ); + return updatedEvent.toETEvent(); + } on PlatformException catch (e) { + throw e.toETException(); + } + } + /// Deletes the event with the given [eventId] from the calendar with the given [calendarId]. /// /// Throws a [ETPermissionException] if the user refuses to grant calendar permissions. diff --git a/lib/src/eventide_platform_interface.dart b/lib/src/eventide_platform_interface.dart index eba7b253..9d43e6c3 100644 --- a/lib/src/eventide_platform_interface.dart +++ b/lib/src/eventide_platform_interface.dart @@ -26,6 +26,8 @@ abstract class EventidePlatform extends PlatformInterface { Future> retrieveCalendars({bool onlyWritableCalendars = true, ETAccount? account}); Future> retrieveAccounts(); + + Future updateCalendar(ETCalendar calendar, {String? title, Color? color}); Future deleteCalendar({required String calendarId}); @@ -64,6 +66,18 @@ abstract class EventidePlatform extends PlatformInterface { }); Future> retrieveEvents({required String calendarId, DateTime? startDate, DateTime? endDate}); + + Future updateEvent(ETEvent event, { + String? calendarId, + String? title, + int? startDate, + int? endDate, + bool? isAllDay, + String? description, + String? url, + String? location, + List? reminders, + }); Future deleteEvent({required String eventId}); diff --git a/pigeons/calendar_api.dart b/pigeons/calendar_api.dart index 80b8a2df..e7ccf09d 100644 --- a/pigeons/calendar_api.dart +++ b/pigeons/calendar_api.dart @@ -18,6 +18,10 @@ abstract class CalendarApi { @SwiftFunction('createCalendar(title:color:in:)') Calendar createCalendar({required String title, required int color, required Account? account}); + @async + @SwiftFunction('updateCalendar(withId:title:color:)') + Calendar updateCalendar({required String calendarId, required String title, required int color}); + @async @SwiftFunction('retrieveCalendars(onlyWritable:from:)') List retrieveCalendars({required bool onlyWritableCalendars, required Account? account}); @@ -42,6 +46,21 @@ abstract class CalendarApi { required List? reminders, }); + @async + @SwiftFunction('updateEvent(withId:calendarId:title:startDate:endDate:isAllDay:description:url:location:reminders:)') + Event updateEvent({ + required String eventId, + required String calendarId, + required String title, + required int startDate, + required int endDate, + required bool isAllDay, + required String? description, + required String? url, + required String? location, + required List? reminders, + }); + @async void createEventInDefaultCalendar({ required String title, From a51860cf2b7b420fe325671b6484d1828072360b Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Fri, 5 Jun 2026 16:43:57 +0200 Subject: [PATCH 2/9] fix updateCalendar signature --- .../connect/tech/eventide/CalendarImplem.kt | 75 ++++++++++++++++++- .../connect/tech/eventide/CalendarTests.kt | 6 +- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt index df8f3b02..98d71d4a 100644 --- a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt +++ b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt @@ -354,7 +354,7 @@ class CalendarImplem( calendarId: String, title: String, color: Long, - callback: (Result) -> Unit + callback: (Result) -> Unit ) { permissionHandler.requestWritePermission { granted -> if (!granted) { @@ -383,7 +383,7 @@ class CalendarImplem( val updated = contentResolver.update(calendarContentUri, values, selection, selectionArgs) if (updated > 0) { - callback(Result.success(Unit)) + retrieveCalendar(calendarId, callback) } else { callback( Result.failure( @@ -1059,6 +1059,77 @@ class CalendarImplem( } // ------------------- Private methods ------------------- + private fun retrieveCalendar(calendarId: String, callback: (Result) -> Unit) { + try { + val projection = arrayOf( + CalendarContract.Calendars._ID, + CalendarContract.Calendars.CALENDAR_DISPLAY_NAME, + CalendarContract.Calendars.CALENDAR_COLOR, + CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL, + CalendarContract.Calendars.ACCOUNT_NAME, + CalendarContract.Calendars.ACCOUNT_TYPE + ) + val selection = CalendarContract.Calendars._ID + " = ?" + val selectionArgs = arrayOf(calendarId) + + val cursor = contentResolver.query(calendarContentUri, projection, selection, selectionArgs, null) + cursor?.use { + if (it.moveToNext()) { + val id = it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars._ID)) + val displayName = it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_DISPLAY_NAME)) + val color = it.getLong(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_COLOR)) + val accessLevel = it.getInt(it.getColumnIndexOrThrow(CalendarContract.Calendars.CALENDAR_ACCESS_LEVEL)) + val accountName = it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_NAME)) + val accountType = it.getString(it.getColumnIndexOrThrow(CalendarContract.Calendars.ACCOUNT_TYPE)) + val displayAccountName = getSystemAccountLabel(accountType) ?: accountName + + val isWritable = accessLevel >= CalendarContract.Calendars.CAL_ACCESS_CONTRIBUTOR + callback( + Result.success( + Calendar( + id = id, + title = displayName, + color = color, + isWritable = isWritable, + account = Account( + id = accountName, + name = displayAccountName, + type = accountType + ) + ) + ) + ) + } else { + callback( + Result.failure( + FlutterError( + code = "NOT_FOUND", + message = "Failed to retrieve calendar" + ) + ) + ) + } + } ?: callback( + Result.failure( + FlutterError( + code = "GENERIC_ERROR", + message = "An error occurred" + ) + ) + ) + } catch (e: Exception) { + callback( + Result.failure( + FlutterError( + code = "GENERIC_ERROR", + message = e.message, + details = e.cause + ) + ) + ) + } + } + private fun isCalendarWritable( calendarId: String, ): Boolean { diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt index f605a365..6e4234ce 100644 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/CalendarTests.kt @@ -514,7 +514,7 @@ class CalendarTests { every { contentResolver.update(calendarContentUri, any(), any(), any()) } returns 1 - var result: Result? = null + var result: Result? = null val latch = CountDownLatch(1) calendarImplem.updateCalendar("1", "New Title", 0xFF0000) { result = it @@ -534,7 +534,7 @@ class CalendarTests { mockPermissionGranted(permissionHandler) mockNotWritableCalendar() - var result: Result? = null + var result: Result? = null val latch = CountDownLatch(1) calendarImplem.updateCalendar("1", "New Title", 0xFF0000) { result = it @@ -552,7 +552,7 @@ class CalendarTests { mockPermissionGranted(permissionHandler) mockCalendarNotFound() - var result: Result? = null + var result: Result? = null val latch = CountDownLatch(1) calendarImplem.updateCalendar("1", "New Title", 0xFF0000) { result = it From 1c3cf63184b40dc234169ee4da4c6026b8f12df3 Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Fri, 5 Jun 2026 16:45:16 +0200 Subject: [PATCH 3/9] fix updateEvent signature --- .../main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt | 4 ++-- .../src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt index 98d71d4a..9d134c2d 100644 --- a/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt +++ b/android/src/main/kotlin/sncf/connect/tech/eventide/CalendarImplem.kt @@ -556,7 +556,7 @@ class CalendarImplem( url: String?, location: String?, reminders: List?, - callback: (Result) -> Unit + callback: (Result) -> Unit ) { permissionHandler.requestWritePermission { granted -> if (!granted) { @@ -608,7 +608,7 @@ class CalendarImplem( } if (updated > 0) { - callback(Result.success(Unit)) + retrieveEvent(eventId, callback) } else { callback( Result.failure( diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt index e8cddcdc..d935487d 100644 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt @@ -705,7 +705,7 @@ class EventTests { every { contentResolver.update(eventContentUri, any(), any(), any()) } returns 1 - var result: Result? = null + var result: Result? = null val latch = CountDownLatch(1) calendarImplem.updateEvent( "1", @@ -740,7 +740,7 @@ class EventTests { every { contentResolver.delete(remindersContentUri, any(), any()) } returns 1 every { contentResolver.insert(remindersContentUri, any()) } returns mockk() - var result: Result? = null + var result: Result? = null val latch = CountDownLatch(1) calendarImplem.updateEvent( "1", From 31c77d6a08394d2e5a5a890e991fd14c7e21583a Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Fri, 5 Jun 2026 16:46:39 +0200 Subject: [PATCH 4/9] fix ios tests --- example/ios/EventideTests/Mocks/MockEasyEventStore.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/example/ios/EventideTests/Mocks/MockEasyEventStore.swift b/example/ios/EventideTests/Mocks/MockEasyEventStore.swift index cdec6265..b81056fe 100644 --- a/example/ios/EventideTests/Mocks/MockEasyEventStore.swift +++ b/example/ios/EventideTests/Mocks/MockEasyEventStore.swift @@ -62,7 +62,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { calendars.remove(at: index) } - func updateCalendar(calendarId: String, title: String, color: UIColor) throws { + func updateCalendar(calendarId: String, title: String, color: UIColor) throws -> eventide.Calendar { guard let index = calendars.firstIndex(where: { $0.id == calendarId }) else { throw PigeonError( code: "NOT_FOUND", @@ -81,6 +81,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { calendars[index].title = title calendars[index].color = color + return calendars[index].toCalendar() } func createEvent(calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event { @@ -109,7 +110,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { return mockEvent.toEvent() } - func updateEvent(eventId: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws { + func updateEvent(eventId: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event { guard let mockEvent = findEvent(eventId: eventId) else { throw PigeonError( code: "NOT_FOUND", @@ -142,6 +143,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { if let timeIntervals = timeIntervals { mockEvent.reminders = timeIntervals } + return mockEvent.toEvent() } func createEvent(title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws { From 07367eb2efd1390cc1122db3b4c709e491222ce9 Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Tue, 9 Jun 2026 09:49:48 +0200 Subject: [PATCH 5/9] sort imports --- lib/src/calendar_api.g.dart | 384 +++++++++++++---------- lib/src/eventide.dart | 39 +-- lib/src/eventide_platform_interface.dart | 7 +- 3 files changed, 239 insertions(+), 191 deletions(-) diff --git a/lib/src/calendar_api.g.dart b/lib/src/calendar_api.g.dart index 34d7a210..3fb1468b 100644 --- a/lib/src/calendar_api.g.dart +++ b/lib/src/calendar_api.g.dart @@ -7,24 +7,17 @@ import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List; import 'package:flutter/services.dart'; + import 'package:meta/meta.dart' show immutable, protected, visibleForTesting; -Object? _extractReplyValueOrThrow( - List? replyList, - String channelName, { - required bool isNullValid, -}) { +Object? _extractReplyValueOrThrow(List? replyList, String channelName, {required bool isNullValid}) { if (replyList == null) { throw PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel: "$channelName".', ); } else if (replyList.length > 1) { - throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], - ); + throw PlatformException(code: replyList[0]! as String, message: replyList[1] as String?, details: replyList[2]); } else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) { throw PlatformException( code: 'null-error', @@ -45,9 +38,7 @@ bool _deepEquals(Object? a, Object? b) { return a == b; } if (a is List && b is List) { - return a.length == b.length && - a.indexed - .every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); + return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1])); } if (a is Map && b is Map) { if (a.length != b.length) { @@ -96,7 +87,6 @@ int _deepHash(Object? value) { return value.hashCode; } - class Calendar { Calendar({ required this.id, @@ -117,17 +107,12 @@ class Calendar { Account account; List _toList() { - return [ - id, - title, - color, - isWritable, - account, - ]; + return [id, title, color, isWritable, account]; } Object encode() { - return _toList(); } + return _toList(); + } static Calendar decode(Object result) { result as List; @@ -149,7 +134,11 @@ class Calendar { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(title, other.title) && _deepEquals(color, other.color) && _deepEquals(isWritable, other.isWritable) && _deepEquals(account, other.account); + return _deepEquals(id, other.id) && + _deepEquals(title, other.title) && + _deepEquals(color, other.color) && + _deepEquals(isWritable, other.isWritable) && + _deepEquals(account, other.account); } @override @@ -211,7 +200,8 @@ class Event { } Object encode() { - return _toList(); } + return _toList(); + } static Event decode(Object result) { result as List; @@ -239,7 +229,17 @@ class Event { if (identical(this, other)) { return true; } - return _deepEquals(id, other.id) && _deepEquals(calendarId, other.calendarId) && _deepEquals(title, other.title) && _deepEquals(isAllDay, other.isAllDay) && _deepEquals(startDate, other.startDate) && _deepEquals(endDate, other.endDate) && _deepEquals(reminders, other.reminders) && _deepEquals(attendees, other.attendees) && _deepEquals(description, other.description) && _deepEquals(url, other.url) && _deepEquals(location, other.location); + return _deepEquals(id, other.id) && + _deepEquals(calendarId, other.calendarId) && + _deepEquals(title, other.title) && + _deepEquals(isAllDay, other.isAllDay) && + _deepEquals(startDate, other.startDate) && + _deepEquals(endDate, other.endDate) && + _deepEquals(reminders, other.reminders) && + _deepEquals(attendees, other.attendees) && + _deepEquals(description, other.description) && + _deepEquals(url, other.url) && + _deepEquals(location, other.location); } @override @@ -248,11 +248,7 @@ class Event { } class Account { - Account({ - required this.id, - required this.name, - required this.type, - }); + Account({required this.id, required this.name, required this.type}); String id; @@ -261,23 +257,16 @@ class Account { String type; List _toList() { - return [ - id, - name, - type, - ]; + return [id, name, type]; } Object encode() { - return _toList(); } + return _toList(); + } static Account decode(Object result) { result as List; - return Account( - id: result[0]! as String, - name: result[1]! as String, - type: result[2]! as String, - ); + return Account(id: result[0]! as String, name: result[1]! as String, type: result[2]! as String); } @override @@ -298,13 +287,7 @@ class Account { } class Attendee { - Attendee({ - required this.name, - required this.email, - required this.type, - required this.role, - required this.status, - }); + Attendee({required this.name, required this.email, required this.type, required this.role, required this.status}); String name; @@ -317,17 +300,12 @@ class Attendee { int status; List _toList() { - return [ - name, - email, - type, - role, - status, - ]; + return [name, email, type, role, status]; } Object encode() { - return _toList(); } + return _toList(); + } static Attendee decode(Object result) { result as List; @@ -349,7 +327,11 @@ class Attendee { if (identical(this, other)) { return true; } - return _deepEquals(name, other.name) && _deepEquals(email, other.email) && _deepEquals(type, other.type) && _deepEquals(role, other.role) && _deepEquals(status, other.status); + return _deepEquals(name, other.name) && + _deepEquals(email, other.email) && + _deepEquals(type, other.type) && + _deepEquals(role, other.role) && + _deepEquals(status, other.status); } @override @@ -357,7 +339,6 @@ class Attendee { int get hashCode => _deepHash([runtimeType, ..._toList()]); } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -365,16 +346,16 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is Calendar) { + } else if (value is Calendar) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is Event) { + } else if (value is Event) { buffer.putUint8(130); writeValue(buffer, value.encode()); - } else if (value is Account) { + } else if (value is Account) { buffer.putUint8(131); writeValue(buffer, value.encode()); - } else if (value is Attendee) { + } else if (value is Attendee) { buffer.putUint8(132); writeValue(buffer, value.encode()); } else { @@ -404,16 +385,17 @@ class CalendarApi { /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. CalendarApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) - : pigeonVar_binaryMessenger = binaryMessenger, - pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; final BinaryMessenger? pigeonVar_binaryMessenger; static const MessageCodec pigeonChannelCodec = _PigeonCodec(); final String pigeonVar_messageChannelSuffix; - Future createCalendar({required String title, required int color, required Account? account, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createCalendar$pigeonVar_messageChannelSuffix'; + Future createCalendar({required String title, required int color, required Account? account}) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.createCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -423,16 +405,16 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Calendar; } - Future updateCalendar({required String calendarId, required String title, required int color, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.updateCalendar$pigeonVar_messageChannelSuffix'; + Future updateCalendar({required String calendarId, required String title, required int color}) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.updateCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -442,16 +424,16 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Calendar; } Future> retrieveCalendars({required bool onlyWritableCalendars, required Account? account}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.retrieveCalendars$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.retrieveCalendars$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -461,16 +443,16 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future> retrieveAccounts() async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.retrieveAccounts$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.retrieveAccounts$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -480,16 +462,16 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } Future deleteCalendar({required String calendarId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.deleteCalendar$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.deleteCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -498,90 +480,154 @@ class CalendarApi { final Future pigeonVar_sendFuture = pigeonVar_channel.send([calendarId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } - Future createEvent({required String calendarId, required String title, required int startDate, required int endDate, required bool isAllDay, required String? description, required String? url, required String? location, required List? reminders, }) async { + Future createEvent({ + required String calendarId, + required String title, + required int startDate, + required int endDate, + required bool isAllDay, + required String? description, + required String? url, + required String? location, + required List? reminders, + }) async { final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createEvent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([calendarId, title, startDate, endDate, isAllDay, description, url, location, reminders]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + calendarId, + title, + startDate, + endDate, + isAllDay, + description, + url, + location, + reminders, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Event; } - Future updateEvent({required String eventId, required String calendarId, required String title, required int startDate, required int endDate, required bool isAllDay, required String? description, required String? url, required String? location, required List? reminders, }) async { + Future updateEvent({ + required String eventId, + required String calendarId, + required String title, + required int startDate, + required int endDate, + required bool isAllDay, + required String? description, + required String? url, + required String? location, + required List? reminders, + }) async { final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.updateEvent$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([eventId, calendarId, title, startDate, endDate, isAllDay, description, url, location, reminders]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + eventId, + calendarId, + title, + startDate, + endDate, + isAllDay, + description, + url, + location, + reminders, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Event; } - Future createEventInDefaultCalendar({required String title, required int startDate, required int endDate, required bool isAllDay, required String? description, required String? url, required String? location, required List? reminders, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createEventInDefaultCalendar$pigeonVar_messageChannelSuffix'; + Future createEventInDefaultCalendar({ + required String title, + required int startDate, + required int endDate, + required bool isAllDay, + required String? description, + required String? url, + required String? location, + required List? reminders, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.createEventInDefaultCalendar$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, startDate, endDate, isAllDay, description, url, location, reminders]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + title, + startDate, + endDate, + isAllDay, + description, + url, + location, + reminders, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; - } - - Future createEventThroughNativePlatform({String? title, int? startDate, int? endDate, bool? isAllDay, String? description, String? url, String? location, List? reminders, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createEventThroughNativePlatform$pigeonVar_messageChannelSuffix'; + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); + } + + Future createEventThroughNativePlatform({ + String? title, + int? startDate, + int? endDate, + bool? isAllDay, + String? description, + String? url, + String? location, + List? reminders, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.createEventThroughNativePlatform$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, binaryMessenger: pigeonVar_binaryMessenger, ); - final Future pigeonVar_sendFuture = pigeonVar_channel.send([title, startDate, endDate, isAllDay, description, url, location, reminders]); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([ + title, + startDate, + endDate, + isAllDay, + description, + url, + location, + reminders, + ]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } - Future> retrieveEvents({required String calendarId, required int startDate, required int endDate, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.retrieveEvents$pigeonVar_messageChannelSuffix'; + Future> retrieveEvents({required String calendarId, required int startDate, required int endDate}) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.retrieveEvents$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -591,11 +637,10 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return (pigeonVar_replyValue! as List).cast(); } @@ -609,16 +654,12 @@ class CalendarApi { final Future pigeonVar_sendFuture = pigeonVar_channel.send([eventId]); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; - _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: true, - ) - ; + _extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true); } Future createReminder({required int reminder, required String eventId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createReminder$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.createReminder$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -628,16 +669,16 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Event; } Future deleteReminder({required int reminder, required String eventId}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.deleteReminder$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.deleteReminder$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -647,16 +688,22 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Event; } - Future createAttendee({required String eventId, required String name, required String email, required int role, required int type, }) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.createAttendee$pigeonVar_messageChannelSuffix'; + Future createAttendee({ + required String eventId, + required String name, + required String email, + required int role, + required int type, + }) async { + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.createAttendee$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -666,16 +713,16 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Event; } Future deleteAttendee({required String eventId, required String email}) async { - final pigeonVar_channelName = 'dev.flutter.pigeon.eventide.CalendarApi.deleteAttendee$pigeonVar_messageChannelSuffix'; + final pigeonVar_channelName = + 'dev.flutter.pigeon.eventide.CalendarApi.deleteAttendee$pigeonVar_messageChannelSuffix'; final pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, pigeonChannelCodec, @@ -685,11 +732,10 @@ class CalendarApi { final pigeonVar_replyList = await pigeonVar_sendFuture as List?; final Object? pigeonVar_replyValue = _extractReplyValueOrThrow( - pigeonVar_replyList, - pigeonVar_channelName, - isNullValid: false, - ) - ; + pigeonVar_replyList, + pigeonVar_channelName, + isNullValid: false, + ); return pigeonVar_replyValue! as Event; } } diff --git a/lib/src/eventide.dart b/lib/src/eventide.dart index d26db0bd..8f97e0fd 100644 --- a/lib/src/eventide.dart +++ b/lib/src/eventide.dart @@ -90,15 +90,15 @@ class Eventide extends EventidePlatform { /// Updates the given [calendar] with optional new values for [title] and [color]. /// Only the provided parameters will be updated, the others will remain unchanged. - /// + /// /// Returns the updated [ETCalendar]. - /// + /// /// Throws a [ETPermissionException] if the user refuses to grant calendar permissions. - /// + /// /// Throws a [ETNotFoundException] if the calendar with the given [calendar.id] is not found. - /// + /// /// Throws a [ETNotEditableException] if the calendar is not editable. - /// + /// /// Throws a [ETGenericException] if any other error occurs during calendar update. @override Future updateCalendar(ETCalendar calendar, {String? title, Color? color}) async { @@ -288,26 +288,27 @@ class Eventide extends EventidePlatform { /// Updates the given [event] with optional new values for [calendarId], [title], [startDate], [endDate], [isAllDay], [description], [url], [location], and a list of [reminders] duration. /// Only the provided parameters will be updated, the others will remain unchanged. - /// + /// /// Returns the updated [ETEvent]. - /// + /// /// Throws a [ETPermissionException] if the user refuses to grant calendar permissions. - /// + /// /// Throws a [ETNotFoundException] if the event with the given [event.id] is not found or if the new calendar with the given [calendarId] is not found. - /// + /// /// Throws a [ETNotEditableException] if the calendar is not editable. - /// + /// /// Throws a [ETGenericException] if any other error occurs during event update. @override - Future updateEvent(ETEvent event, { - String? calendarId, - String? title, - int? startDate, - int? endDate, - bool? isAllDay, - String? description, - String? url, - String? location, + Future updateEvent( + ETEvent event, { + String? calendarId, + String? title, + int? startDate, + int? endDate, + bool? isAllDay, + String? description, + String? url, + String? location, List? reminders, }) async { try { diff --git a/lib/src/eventide_platform_interface.dart b/lib/src/eventide_platform_interface.dart index 9d43e6c3..9e8881ad 100644 --- a/lib/src/eventide_platform_interface.dart +++ b/lib/src/eventide_platform_interface.dart @@ -26,7 +26,7 @@ abstract class EventidePlatform extends PlatformInterface { Future> retrieveCalendars({bool onlyWritableCalendars = true, ETAccount? account}); Future> retrieveAccounts(); - + Future updateCalendar(ETCalendar calendar, {String? title, Color? color}); Future deleteCalendar({required String calendarId}); @@ -66,8 +66,9 @@ abstract class EventidePlatform extends PlatformInterface { }); Future> retrieveEvents({required String calendarId, DateTime? startDate, DateTime? endDate}); - - Future updateEvent(ETEvent event, { + + Future updateEvent( + ETEvent event, { String? calendarId, String? title, int? startDate, From 546195808cb4458ac2aac0d35775672dfeb6cda6 Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Tue, 9 Jun 2026 09:51:12 +0200 Subject: [PATCH 6/9] dart format --- .../lib/calendar/ui/components/event_form.dart | 4 +--- .../lib/calendar/ui/views/calendar_screen.dart | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart b/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart index fe81f2bc..9b71dde9 100644 --- a/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart +++ b/example/more-complex/full-permission/lib/calendar/ui/components/event_form.dart @@ -131,9 +131,7 @@ final class _EventFormState extends State { child: ElevatedButton( onPressed: () async { final lastDate = _selectedEndDate; - final firstDate = widget.initialEvent != null - ? DateTime(2000) - : DateTime.now(); + final firstDate = widget.initialEvent != null ? DateTime(2000) : DateTime.now(); final pickedDate = await showDatePicker( context: context, initialDate: _selectedStartDate, diff --git a/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart b/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart index 76bd7e6b..f9e5f64a 100644 --- a/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart +++ b/example/more-complex/full-permission/lib/calendar/ui/views/calendar_screen.dart @@ -135,14 +135,16 @@ final class _CalendarScreenState extends State with SingleTicker data.calendars.keys.singleWhere((calendar) => calendar.id == event.calendarId); final calendarCubit = BlocProvider.of(context); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => EventDetailsScreen( - event: event, - isCalendarWritable: relatedCalendar.isWritable, - ), - ), - ).then((_) => calendarCubit.loadFullContent()); + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => EventDetailsScreen( + event: event, + isCalendarWritable: relatedCalendar.isWritable, + ), + ), + ) + .then((_) => calendarCubit.loadFullContent()); } } }, From 7b25d0e5d8f83de388195bd60111e20aa41b3d34 Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Wed, 10 Jun 2026 11:05:35 +0200 Subject: [PATCH 7/9] fix ios tests --- example/ios/EventideTests/AccountTests.swift | 14 +- example/ios/EventideTests/AttendeeTests.swift | 62 +-- example/ios/EventideTests/CalendarTests.swift | 100 ++-- example/ios/EventideTests/EventTests.swift | 466 +++++++++--------- .../Mocks/MockEasyEventStore.swift | 363 ++++++++------ example/ios/EventideTests/ReminderTests.swift | 346 ++++++------- example/ios/Runner.xcodeproj/project.pbxproj | 4 +- .../EasyEventStore/EasyEventStore.swift | 31 +- 8 files changed, 735 insertions(+), 651 deletions(-) diff --git a/example/ios/EventideTests/AccountTests.swift b/example/ios/EventideTests/AccountTests.swift index 16cd578b..0cea5d19 100644 --- a/example/ios/EventideTests/AccountTests.swift +++ b/example/ios/EventideTests/AccountTests.swift @@ -17,21 +17,19 @@ final class AccountTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: Account(id: "id1", name: "test", type: "local"), - events: [] + account: Account(id: "id1", name: "test", type: "local") ), - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "id2", name: "iCloud", type: "calDAV"), - events: [] + account: Account(id: "id2", name: "iCloud", type: "calDAV") ) ] ) diff --git a/example/ios/EventideTests/AttendeeTests.swift b/example/ios/EventideTests/AttendeeTests.swift index 822cc0b7..2ee78a04 100644 --- a/example/ios/EventideTests/AttendeeTests.swift +++ b/example/ios/EventideTests/AttendeeTests.swift @@ -17,24 +17,26 @@ final class AttendeeTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -107,24 +109,26 @@ final class AttendeeTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) diff --git a/example/ios/EventideTests/CalendarTests.swift b/example/ios/EventideTests/CalendarTests.swift index 7ccb8947..cee6cbc8 100644 --- a/example/ios/EventideTests/CalendarTests.swift +++ b/example/ios/EventideTests/CalendarTests.swift @@ -44,21 +44,19 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ), - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "iCloud", name: "iCloud", type: "calDAV"), - events: [] + account: Account(id: "iCloud", name: "iCloud", type: "calDAV") ) ] ) @@ -89,21 +87,19 @@ final class CalendarTests: XCTestCase { let account2 = Account(id: "iCloud", name: "iCloud", type: "calDAV") let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: account1, - events: [] + account: account1 ), - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: account2, - events: [] + account: account2 ) ] ) @@ -133,21 +129,19 @@ final class CalendarTests: XCTestCase { let account2 = Account(id: "iCloud", name: "iCloud", type: "calDAV") let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: account1, - events: [] + account: account1 ), - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: account2, - events: [] + account: account2 ) ] ) @@ -179,21 +173,19 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ), - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "local", name: "iCloud", type: "calDAV"), - events: [] + account: Account(id: "local", name: "iCloud", type: "calDAV") ) ] ) @@ -246,13 +238,12 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -280,13 +271,12 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -319,13 +309,12 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -414,13 +403,12 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -507,13 +495,12 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -545,13 +532,12 @@ final class CalendarTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "Old Title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -565,7 +551,7 @@ final class CalendarTests: XCTestCase { switch (result) { case .success: XCTAssert(mockEasyEventStore.calendars[0].title == "New Title") - XCTAssert(mockEasyEventStore.calendars[0].color.toInt64() == 0x00FF00) + XCTAssert(mockEasyEventStore.calendars[0].color == 0x00FF00) expectation.fulfill() case .failure: XCTFail("Calendar should have been updated") diff --git a/example/ios/EventideTests/EventTests.swift b/example/ios/EventideTests/EventTests.swift index a9687936..3152ecd7 100644 --- a/example/ios/EventideTests/EventTests.swift +++ b/example/ios/EventideTests/EventTests.swift @@ -20,13 +20,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -70,13 +69,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -121,34 +119,38 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: startDate, - endDate: endDate, - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ), - MockEvent( - id: "2", - title: "title", - startDate: startDate.addingTimeInterval(TimeInterval(50)), - endDate: endDate.addingTimeInterval(TimeInterval(50)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: startDate.millisecondsSince1970, + endDate: endDate.millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" + ), + Event( + id: "2", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: startDate.addingTimeInterval(TimeInterval(50)).millisecondsSince1970, + endDate: endDate.addingTimeInterval(TimeInterval(50)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -167,7 +169,7 @@ final class EventTests: XCTestCase { case .success(let events): XCTAssert(events.count == 1) XCTAssert(events.first!.id == "1") - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.id == "1") + XCTAssert(mockEasyEventStore.events.first!.id == "1") expectation.fulfill() case .failure: XCTFail("Event should have been retrieved") @@ -185,44 +187,46 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: startDate, - endDate: endDate, - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ), - MockEvent( - id: "2", - title: "title", - startDate: startDate.addingTimeInterval(TimeInterval(50)), - endDate: endDate.addingTimeInterval(TimeInterval(50)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [TimeInterval(360), TimeInterval(3600)], - attendees: [ - MockAttendee( - name: "John Doe", - email: "john.doe@example.com", - type: 1, - role: 1, - status: 1 - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: startDate.millisecondsSince1970, + endDate: endDate.millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" + ), + Event( + id: "2", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: startDate.addingTimeInterval(TimeInterval(50)).millisecondsSince1970, + endDate: endDate.addingTimeInterval(TimeInterval(50)).millisecondsSince1970, + reminders: [360, 3600], + attendees: [ + Attendee( + name: "John Doe", + email: "john.doe@example.com", + type: 1, + role: 1, + status: 1 ) - ] + ], + description: "description", + url: "url" ) ] ) @@ -266,24 +270,26 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "2", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: startDate, - endDate: endDate, - calendarId: "2", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "2", + title: "title", + isAllDay: false, + startDate: startDate.millisecondsSince1970, + endDate: endDate.millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -322,13 +328,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -360,24 +365,26 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -390,7 +397,7 @@ final class EventTests: XCTestCase { calendarImplem.deleteEvent(withId: "1") { deleteEventResult in switch (deleteEventResult) { case .success: - XCTAssert(mockEasyEventStore.calendars.first!.events.isEmpty) + XCTAssert(mockEasyEventStore.events.isEmpty) expectation.fulfill() case .failure: XCTFail("Event should have been deleted") @@ -405,24 +412,26 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "2", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "2", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -442,7 +451,7 @@ final class EventTests: XCTestCase { return } XCTAssert(error.code == "NOT_FOUND") - XCTAssert(mockEasyEventStore.calendars.first!.events.count == 1) + XCTAssert(mockEasyEventStore.events.count == 1) expectation.fulfill() } } @@ -455,24 +464,26 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: false, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "2", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "2", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -492,7 +503,7 @@ final class EventTests: XCTestCase { return } XCTAssert(error.code == "NOT_EDITABLE") - XCTAssert(mockEasyEventStore.calendars.first!.events.count == 1) + XCTAssert(mockEasyEventStore.events.count == 1) expectation.fulfill() } } @@ -505,13 +516,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -541,7 +551,7 @@ final class EventTests: XCTestCase { return } XCTAssert(error.code == "ACCESS_REFUSED") - XCTAssert(mockEasyEventStore.calendars.first!.events.isEmpty) + XCTAssert(mockEasyEventStore.events.isEmpty) expectation.fulfill() } } @@ -554,13 +564,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -584,7 +593,7 @@ final class EventTests: XCTestCase { return } XCTAssert(error.code == "ACCESS_REFUSED") - XCTAssert(mockEasyEventStore.calendars.first!.events.isEmpty) + XCTAssert(mockEasyEventStore.events.isEmpty) expectation.fulfill() } } @@ -597,24 +606,26 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -634,7 +645,7 @@ final class EventTests: XCTestCase { return } XCTAssert(error.code == "ACCESS_REFUSED") - XCTAssert(mockEasyEventStore.calendars.first!.events.count == 1) + XCTAssert(mockEasyEventStore.events.count == 1) expectation.fulfill() } } @@ -647,13 +658,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -682,7 +692,7 @@ final class EventTests: XCTestCase { XCTFail("error should be of type PermissionError.PermErr") return } - XCTAssert(mockEasyEventStore.calendars.first!.events.isEmpty) + XCTAssert(mockEasyEventStore.events.isEmpty) expectation.fulfill() } } @@ -695,13 +705,13 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, account: Account(id: "local", name: "local", type: "local"), - events: [] + ) ] ) @@ -724,7 +734,7 @@ final class EventTests: XCTestCase { XCTFail("error should be of type PermissionError.PermErr") return } - XCTAssert(mockEasyEventStore.calendars.first!.events.isEmpty) + XCTAssert(mockEasyEventStore.events.isEmpty) expectation.fulfill() } } @@ -737,24 +747,26 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -773,7 +785,7 @@ final class EventTests: XCTestCase { XCTFail("error should be of type PermissionError.PermErr") return } - XCTAssert(mockEasyEventStore.calendars.first!.events.count == 1) + XCTAssert(mockEasyEventStore.events.count == 1) expectation.fulfill() } } @@ -791,13 +803,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "Default Calendar", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -819,8 +830,8 @@ final class EventTests: XCTestCase { ) { result in switch result { case .success: - XCTAssertEqual(mockEasyEventStore.calendars.first!.events.count, 1) - XCTAssertEqual(mockEasyEventStore.calendars.first!.events.first!.title, "Default Calendar Event") + XCTAssertEqual(mockEasyEventStore.events.count, 1) + XCTAssertEqual(mockEasyEventStore.events.first!.title, "Default Calendar Event") expectation.fulfill() case .failure(let error): XCTFail("Event should have been created: \(error)") @@ -838,13 +849,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "Default Calendar", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -866,8 +876,8 @@ final class EventTests: XCTestCase { ) { result in switch result { case .success: - XCTAssertEqual(mockEasyEventStore.calendars.first!.events.count, 1) - XCTAssertEqual(mockEasyEventStore.calendars.first!.events.first!.isAllDay, true) + XCTAssertEqual(mockEasyEventStore.events.count, 1) + XCTAssertEqual(mockEasyEventStore.events.first!.isAllDay, true) expectation.fulfill() case .failure(let error): XCTFail("Event should have been created: \(error)") @@ -885,13 +895,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "Default Calendar", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -913,8 +922,8 @@ final class EventTests: XCTestCase { ) { result in switch result { case .success: - XCTAssertEqual(mockEasyEventStore.calendars.first!.events.count, 1) - XCTAssertNil(mockEasyEventStore.calendars.first!.events.first!.description) + XCTAssertEqual(mockEasyEventStore.events.count, 1) + XCTAssertNil(mockEasyEventStore.events.first!.description) expectation.fulfill() case .failure(let error): XCTFail("Event should have been created: \(error)") @@ -931,13 +940,12 @@ final class EventTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "Test Calendar", - color: UIColor.blue, + color: UIColor.blue.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [] + account: Account(id: "local", name: "local", type: "local") ) ] ) @@ -1160,29 +1168,29 @@ final class EventTests: XCTestCase { let startDate = Date().millisecondsSince1970 let endDate = Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970 - let mockCalendar = MockCalendar( + let mockCalendar = Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "event1", - calendarId: "1", - title: "Old Title", - startDate: Date(from: startDate), - endDate: Date(from: endDate), - isAllDay: false, - description: "Old Desc", - url: "Old URL", - location: "Old Loc", - reminders: [] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + + let mockEvent = Event( + id: "event1", + calendarId: "1", + title: "Old Title", + isAllDay: false, + startDate: Date(from: startDate).millisecondsSince1970, + endDate: Date(from: endDate).millisecondsSince1970, + reminders: [], + attendees: [], + description: "Old Desc", + url: "Old URL", + location: "Old Loc", ) - let mockEasyEventStore = MockEasyEventStore(calendars: [mockCalendar]) + let mockEasyEventStore = MockEasyEventStore(calendars: [mockCalendar], events: [mockEvent]) calendarImplem = CalendarImplem( easyEventStore: mockEasyEventStore, @@ -1203,12 +1211,12 @@ final class EventTests: XCTestCase { ) { result in switch (result) { case .success: - let updatedEvent = mockCalendar.events[0] + let updatedEvent = mockEasyEventStore.events.first! XCTAssert(updatedEvent.title == "New Title") - XCTAssert(updatedEvent.startDate.millisecondsSince1970 == startDate + 1000) + XCTAssert(updatedEvent.startDate == startDate + 1000) XCTAssert(updatedEvent.isAllDay == true) XCTAssert(updatedEvent.description == "New Desc") - XCTAssert(updatedEvent.reminders == [-10.0]) // In MockEasyEventStore it's converted + XCTAssert(updatedEvent.reminders == [-10]) expectation.fulfill() case .failure: XCTFail("Event should have been updated") diff --git a/example/ios/EventideTests/Mocks/MockEasyEventStore.swift b/example/ios/EventideTests/Mocks/MockEasyEventStore.swift index b81056fe..09936746 100644 --- a/example/ios/EventideTests/Mocks/MockEasyEventStore.swift +++ b/example/ios/EventideTests/Mocks/MockEasyEventStore.swift @@ -5,24 +5,35 @@ // Created by CHOUPAULT Alexis on 23/01/2025. // -import EventKit -import Foundation -import UIKit +import class UIKit.UIColor +import struct Foundation.UUID +import struct Foundation.Date +import struct Foundation.TimeInterval @testable import eventide class MockEasyEventStore: EasyEventStoreProtocol { - var calendars: [MockCalendar] = [] - + var calendars: [Calendar] + var events: [Event] + + init(calendars: [Calendar] = [], events: [Event] = []) { + self.calendars = calendars + self.events = events + } + func createCalendar(title: String, color: UIColor, account: Account?) throws -> eventide.Calendar { - let calendar = MockCalendar( + // TODO: source / account + + let calendar = Calendar( id: UUID().uuidString, title: title, - color: color, + color: color.toInt64(), isWritable: true, - account: account ?? Account(id: "local", name: "Local", type: "Local") + account: account ?? Account(id: "local", name: "local", type: "local") ) + calendars.append(calendar) - return calendar.toCalendar() + + return calendar } func retrieveCalendars( @@ -35,14 +46,13 @@ class MockEasyEventStore: EasyEventStoreProtocol { guard let account = account else { return true } return account.id == calendar.account.id && account.type == calendar.account.type } - .map { $0.toCalendar() } } - + func retrieveAccounts() -> [Account] { return calendars.map { $0.account } } - - func deleteCalendar(calendarId: String) throws { + + func updateCalendar(calendarId: String, title: String, color: UIColor) throws -> eventide.Calendar { guard let index = calendars.firstIndex(where: { $0.id == calendarId }) else { throw PigeonError( code: "NOT_FOUND", @@ -50,7 +60,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { details: "The provided calendar.id is certainly incorrect" ) } - + guard calendars[index].isWritable else { throw PigeonError( code: "NOT_EDITABLE", @@ -58,11 +68,14 @@ class MockEasyEventStore: EasyEventStoreProtocol { details: "Calendar does not allow content modifications" ) } - - calendars.remove(at: index) + + calendars[index].title = title + calendars[index].color = color.toInt64() + + return calendars[index] } - - func updateCalendar(calendarId: String, title: String, color: UIColor) throws -> eventide.Calendar { + + func deleteCalendar(calendarId: String) throws { guard let index = calendars.firstIndex(where: { $0.id == calendarId }) else { throw PigeonError( code: "NOT_FOUND", @@ -70,7 +83,7 @@ class MockEasyEventStore: EasyEventStoreProtocol { details: "The provided calendar.id is certainly incorrect" ) } - + guard calendars[index].isWritable else { throw PigeonError( code: "NOT_EDITABLE", @@ -78,13 +91,23 @@ class MockEasyEventStore: EasyEventStoreProtocol { details: "Calendar does not allow content modifications" ) } - - calendars[index].title = title - calendars[index].color = color - return calendars[index].toCalendar() + + let calendarId = calendars[index].id + calendars.remove(at: index) + events.removeAll { $0.calendarId == calendarId } } - - func createEvent(calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event { + + func createEvent( + calendarId: String, + title: String, + startDate: Date, + endDate: Date, + isAllDay: Bool, + description: String?, + url: String?, + location: String?, + timeIntervals: [TimeInterval]? + ) throws -> Event { guard let mockCalendar = calendars.first(where: { $0.id == calendarId }) else { throw PigeonError( code: "NOT_FOUND", @@ -93,77 +116,50 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - let mockEvent = MockEvent( + let mockEvent = Event( id: UUID().uuidString, - calendarId: calendarId, + calendarId: mockCalendar.id, title: title, - startDate: startDate, - endDate: endDate, isAllDay: isAllDay, + startDate: startDate.millisecondsSince1970, + endDate: endDate.millisecondsSince1970, + reminders: timeIntervals?.map({ Int64($0) }) ?? [], + attendees: [], description: description, - url: url, - location: location, - reminders: timeIntervals ?? [] + url: url ) - mockCalendar.events.append(mockEvent) - return mockEvent.toEvent() + events.append(mockEvent) + + return mockEvent } - - func updateEvent(eventId: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws -> Event { - guard let mockEvent = findEvent(eventId: eventId) else { + + func createEvent(title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws { + guard let firstCalendar = calendars.first else { throw PigeonError( code: "NOT_FOUND", - message: "Event not found", - details: "The provided event.id is certainly incorrect" + message: "Default calendar not found", + details: "No calendar has been found" ) } - - // Remove from old calendar if changed - if mockEvent.calendarId != calendarId { - guard let oldCalendarIndex = calendars.firstIndex(where: { $0.id == mockEvent.calendarId }) else { - throw PigeonError(code: "GENERIC_ERROR", message: "Old calendar not found") - } - calendars[oldCalendarIndex].events.removeAll { $0.id == eventId } - - guard let newCalendarIndex = calendars.firstIndex(where: { $0.id == calendarId }) else { - throw PigeonError(code: "NOT_FOUND", message: "New calendar not found") - } - calendars[newCalendarIndex].events.append(mockEvent) - } - - mockEvent.calendarId = calendarId - mockEvent.title = title - mockEvent.startDate = startDate - mockEvent.endDate = endDate - mockEvent.isAllDay = isAllDay - mockEvent.description = description - mockEvent.url = url - mockEvent.location = location - if let timeIntervals = timeIntervals { - mockEvent.reminders = timeIntervals - } - return mockEvent.toEvent() - } - - func createEvent(title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?) throws { - let mockEvent = MockEvent( + + let mockEvent = Event( id: UUID().uuidString, - calendarId: calendars.first!.id, + calendarId: firstCalendar.id, title: title, - startDate: startDate, - endDate: endDate, isAllDay: isAllDay, + startDate: startDate.millisecondsSince1970, + endDate: endDate.millisecondsSince1970, + reminders: timeIntervals?.map({ Int64($0) }) ?? [], + attendees: [], description: description, - url: url, - location: location, - reminders: timeIntervals ?? [] + url: url ) - calendars.first!.events.append(mockEvent) + events.append(mockEvent) } - func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { + func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { completion(.success(())) } @@ -176,13 +172,77 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - return mockCalendar.events - .filter { ($0.startDate >= startDate && $0.startDate <= endDate) || ($0.endDate >= startDate && $0.endDate <= endDate) } - .map { $0.toEvent() } + return events + .filter { startDate.compare(Date(from: $0.startDate)) == .orderedAscending && Date(from: $0.endDate).compare(endDate) == .orderedAscending } + } + + + func updateEvent( + eventId: String, + calendarId: String, + title: String, + startDate: Date, + endDate: Date, + isAllDay: Bool, + description: String?, + url: String?, + location: String?, + timeIntervals: [TimeInterval]? + ) throws -> eventide.Event { + guard let eventIndex = events.firstIndex(where: { $0.id == eventId }) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Event not found", + details: "The provided event.id is certainly incorrect" + ) + } + + guard let oldCalendarIndex = calendars.firstIndex(where: { $0.id == events[eventIndex].calendarId }), + calendars[oldCalendarIndex].isWritable else { + throw PigeonError( + code: "NOT_EDITABLE", + message: "Event actual calendar is not editable", + details: "Calendar does not allow content modifications" + ) + } + + if calendarId != events[eventIndex].calendarId { + guard let calendarIndex = calendars.firstIndex(where: { $0.id == calendarId }) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Calendar not found", + details: "The provided calendar.id is certainly incorrect" + ) + } + + guard calendars[calendarIndex].isWritable else { + throw PigeonError( + code: "NOT_EDITABLE", + message: "Calendar not editable", + details: "Calendar does not allow content modifications" + ) + } + + events[eventIndex].calendarId = calendarId + } + + events[eventIndex].title = title + events[eventIndex].startDate = startDate.millisecondsSince1970 + events[eventIndex].endDate = endDate.millisecondsSince1970 + events[eventIndex].isAllDay = isAllDay + events[eventIndex].description = description + events[eventIndex].url = url + events[eventIndex].location = location + + if let timeIntervals = timeIntervals { + events[eventIndex].reminders = timeIntervals.map({ Int64($0) }) + } + + return events[eventIndex] } func deleteEvent(eventId: String) throws { - guard let mockEvent = findEvent(eventId: eventId) else { + guard let eventIndex = events.firstIndex(where: { $0.id == eventId }) else { throw PigeonError( code: "NOT_FOUND", message: "Event not found", @@ -190,7 +250,8 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - guard let index = calendars.firstIndex(where: { $0.id == mockEvent.calendarId && $0.isWritable }) else { + guard let calendarIndex = calendars.firstIndex(where: { $0.id == events[eventIndex].calendarId }), + calendars[calendarIndex].isWritable else { throw PigeonError( code: "NOT_EDITABLE", message: "Calendar not editable", @@ -198,11 +259,11 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - calendars[index].events.removeAll { $0.id == eventId } + events.removeAll { $0.id == eventId } } func createReminder(timeInterval: TimeInterval, eventId: String) throws -> Event { - guard let mockEvent = findEvent(eventId: eventId) else { + guard let eventIndex = events.firstIndex(where: { $0.id == eventId }) else { throw PigeonError( code: "NOT_FOUND", message: "Event not found", @@ -210,12 +271,13 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - mockEvent.reminders.append(timeInterval) - return mockEvent.toEvent() + events[eventIndex].reminders.append(Int64(timeInterval)) + + return events[eventIndex] } func deleteReminder(timeInterval: TimeInterval, eventId: String) throws -> Event { - guard let mockEvent = findEvent(eventId: eventId) else { + guard let eventIndex = events.firstIndex(where: { $0.id == eventId }) else { throw PigeonError( code: "NOT_FOUND", message: "Event not found", @@ -223,85 +285,78 @@ class MockEasyEventStore: EasyEventStoreProtocol { ) } - mockEvent.reminders.removeAll { $0 == timeInterval } - return mockEvent.toEvent() + guard let reminderIndex = events[eventIndex].reminders.firstIndex(where: { -$0 == Int64(timeInterval) }) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Reminder not found", + details: nil + ) + } + + events[eventIndex].reminders.remove(at: reminderIndex) + + return events[eventIndex] } - - private func findEvent(eventId: String) -> MockEvent? { - for calendar in calendars { - if let event = calendar.events.first(where: { $0.id == eventId }) { - return event - } + + func retrieveAttendees(eventId: String) throws -> [Attendee] { + guard let eventIndex = events.firstIndex(where: { $0.id == eventId }) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Event not found", + details: "The provided event.id is certainly incorrect" + ) } - return nil + + return events[eventIndex].attendees } } -class MockCalendar { - let id: String - var title: String - var color: UIColor - let isWritable: Bool - let account: Account - var events: [MockEvent] = [] +// MARK: - Specialized Mocks for Native Platform Testing - init(id: String, title: String, color: UIColor, isWritable: Bool, account: Account) { - self.id = id - self.title = title - self.color = color - self.isWritable = isWritable - self.account = account +/// Mock that simulates user canceling the native event creation +class MockEasyEventStoreCanceled: MockEasyEventStore { + override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { + // Simulate user cancellation + completion(.failure(PigeonError( + code: "USER_CANCELED", + message: "User canceled event creation", + details: nil + ))) } +} - func toCalendar() -> eventide.Calendar { - return eventide.Calendar( - id: id, - title: title, - color: color.toInt64(), - isWritable: isWritable, - account: account - ) +/// Mock that simulates presentation error +class MockEasyEventStorePresentationError: MockEasyEventStore { + override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { + // Simulate presentation error + completion(.failure(PigeonError( + code: "PRESENTATION_ERROR", + message: "Unable to present event creation view", + details: nil + ))) } } -class MockEvent { - let id: String - var calendarId: String - var title: String - var startDate: Date - var endDate: Date - var isAllDay: Bool - var description: String? - var url: String? - var location: String? - var reminders: [TimeInterval] - - init(id: String, calendarId: String, title: String, startDate: Date, endDate: Date, isAllDay: Bool, description: String?, url: String?, location: String?, reminders: [TimeInterval]) { - self.id = id - self.calendarId = calendarId - self.title = title - self.startDate = startDate - self.endDate = endDate - self.isAllDay = isAllDay - self.description = description - self.url = url - self.location = location - self.reminders = reminders +/// Mock that simulates event deletion during creation +class MockEasyEventStoreEventDeleted: MockEasyEventStore { + override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { + // Simulate event deletion + completion(.failure(PigeonError( + code: "EVENT_DELETED", + message: "Event was deleted", + details: nil + ))) } +} - func toEvent() -> Event { - return Event( - id: id, - calendarId: calendarId, - title: title, - isAllDay: isAllDay, - startDate: startDate.millisecondsSince1970, - endDate: endDate.millisecondsSince1970, - reminders: reminders.map { Int64($0) }, - attendees: [], - description: description, - url: url, - location: location - ) +/// Mock that simulates unknown action +class MockEasyEventStoreUnknownAction: MockEasyEventStore { + override func presentEventCreationViewController(title: String?, startDate: Date?, endDate: Date?, isAllDay: Bool?, description: String?, url: String?, location: String?, timeIntervals: [TimeInterval]?, completion: @escaping (Result) -> Void) { + // Simulate unknown action + completion(.failure(PigeonError( + code: "GENERIC_ERROR", + message: "Unknown action from event edit controller", + details: nil + ))) } } diff --git a/example/ios/EventideTests/ReminderTests.swift b/example/ios/EventideTests/ReminderTests.swift index dbd15724..cf1839bb 100644 --- a/example/ios/EventideTests/ReminderTests.swift +++ b/example/ios/EventideTests/ReminderTests.swift @@ -19,24 +19,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -51,7 +53,7 @@ final class ReminderTests: XCTestCase { case .success(let event): XCTAssert(event.reminders.count == 1) XCTAssert(event.reminders.first == -reminder) - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.first! == TimeInterval(-reminder)) + XCTAssert(mockEasyEventStore.events.first!.reminders.first! == -reminder) expectation.fulfill() case .failure: XCTFail("Reminder should have been created") @@ -68,25 +70,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [10] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [10], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -101,7 +104,7 @@ final class ReminderTests: XCTestCase { case .success(let event): XCTAssert(event.reminders.count == 2) XCTAssert(event.reminders.last == -reminder) - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.last! == TimeInterval(-reminder)) + XCTAssert(mockEasyEventStore.events.first!.reminders.last! == -reminder) expectation.fulfill() case .failure: XCTFail("Reminder should have been created") @@ -118,24 +121,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "2", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "2", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -155,7 +160,7 @@ final class ReminderTests: XCTestCase { return } XCTAssert(error.code == "NOT_FOUND") - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders == nil) + XCTAssert(mockEasyEventStore.events.first!.reminders.isEmpty) expectation.fulfill() } } @@ -168,25 +173,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [3600] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [3600], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -200,7 +206,7 @@ final class ReminderTests: XCTestCase { switch (createReminderResult) { case .success(let event): XCTAssert(event.reminders.isEmpty) - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.isEmpty) + XCTAssert(mockEasyEventStore.events.first!.reminders.isEmpty) expectation.fulfill() case .failure: XCTFail("Reminder should have been deleted") @@ -215,25 +221,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "2", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [3600] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "2", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [3600], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -253,7 +260,7 @@ final class ReminderTests: XCTestCase { return } XCTAssert(error.code == "NOT_FOUND") - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.count == 1) + XCTAssert(mockEasyEventStore.events.first!.reminders.count == 1) expectation.fulfill() } } @@ -266,25 +273,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [3600] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [3600], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -304,7 +312,7 @@ final class ReminderTests: XCTestCase { return } XCTAssert(error.code == "NOT_FOUND") - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.count == 1) + XCTAssert(mockEasyEventStore.events.first!.reminders.count == 1) expectation.fulfill() } } @@ -319,24 +327,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -356,7 +366,7 @@ final class ReminderTests: XCTestCase { return } XCTAssert(error.code == "ACCESS_REFUSED") - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders == nil) + XCTAssert(mockEasyEventStore.events.first!.reminders.isEmpty) expectation.fulfill() } } @@ -369,25 +379,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [3600] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [3600], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -407,7 +418,7 @@ final class ReminderTests: XCTestCase { return } XCTAssert(error.code == "ACCESS_REFUSED") - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.count == 1) + XCTAssert(mockEasyEventStore.events.first!.reminders.count == 1) expectation.fulfill() } } @@ -422,24 +433,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url" - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -458,7 +471,7 @@ final class ReminderTests: XCTestCase { XCTFail("error should be of type PermissionError.PermErr") return } - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders == nil) + XCTAssert(mockEasyEventStore.events.first!.reminders.isEmpty) expectation.fulfill() } } @@ -471,25 +484,26 @@ final class ReminderTests: XCTestCase { let mockEasyEventStore = MockEasyEventStore( calendars: [ - MockCalendar( + Calendar( id: "1", title: "title", - color: UIColor.red, + color: UIColor.red.toInt64(), isWritable: true, - account: Account(id: "local", name: "local", type: "local"), - events: [ - MockEvent( - id: "1", - title: "title", - startDate: Date(), - endDate: Date().addingTimeInterval(TimeInterval(10)), - calendarId: "1", - isAllDay: false, - description: "description", - url: "url", - reminders: [3600] - ) - ] + account: Account(id: "local", name: "local", type: "local") + ) + ], + events: [ + Event( + id: "1", + calendarId: "1", + title: "title", + isAllDay: false, + startDate: Date().millisecondsSince1970, + endDate: Date().addingTimeInterval(TimeInterval(10)).millisecondsSince1970, + reminders: [3600], + attendees: [], + description: "description", + url: "url" ) ] ) @@ -508,7 +522,7 @@ final class ReminderTests: XCTestCase { XCTFail("error should be of type PermissionError.PermErr") return } - XCTAssert(mockEasyEventStore.calendars.first!.events.first!.reminders!.count == 1) + XCTAssert(mockEasyEventStore.events.first!.reminders.count == 1) expectation.fulfill() } } diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 253f0bd9..3c11eb08 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -53,6 +53,7 @@ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -70,7 +71,6 @@ EA8AABCC2D8D889600A1D69B /* CalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarTests.swift; sourceTree = ""; }; EA8AABD02D8D891A00A1D69B /* EventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTests.swift; sourceTree = ""; }; EAECB9432D395CA3000FAA80 /* UtilsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UtilsTests.swift; sourceTree = ""; }; - 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ diff --git a/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift b/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift index 02c3ab02..dbbe4e9e 100644 --- a/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift +++ b/ios/eventide/Sources/eventide/EasyEventStore/EasyEventStore.swift @@ -290,16 +290,35 @@ final class EasyEventStore: EasyEventStoreProtocol { details: "The provided event.id is certainly incorrect" ) } - - guard let ekCalendar = eventStore.calendar(withIdentifier: calendarId) else { + + guard ekEvent.calendar.allowsContentModifications else { throw PigeonError( - code: "NOT_FOUND", - message: "Calendar not found", - details: "The provided calendar.id is certainly incorrect" + code: "NOT_EDITABLE", + message: "Event actual calendar is not editable", + details: "Calendar does not allow content modifications" ) } + + if calendarId != ekEvent.calendar.calendarIdentifier { + guard let ekCalendar = eventStore.calendar(withIdentifier: calendarId) else { + throw PigeonError( + code: "NOT_FOUND", + message: "Calendar not found", + details: "The provided calendar.id is certainly incorrect" + ) + } + + guard ekCalendar.allowsContentModifications else { + throw PigeonError( + code: "NOT_EDITABLE", + message: "Event new calendar is not editable", + details: "Calendar does not allow content modifications" + ) + } + + ekEvent.calendar = ekCalendar + } - ekEvent.calendar = ekCalendar ekEvent.title = title ekEvent.notes = description ekEvent.startDate = startDate From 4bd1ec168bd707dc933d9768feea2480d2816079 Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Wed, 10 Jun 2026 12:04:41 +0200 Subject: [PATCH 8/9] fix android tests --- .../sncf/connect/tech/eventide/EventTests.kt | 8 ++ .../sncf/connect/tech/eventide/Mocks.kt | 11 +- .../lib/calendar/logic/calendar_cubit.dart | 4 +- .../logic/event_details_cubit.dart | 4 +- lib/src/eventide.dart | 12 +- lib/src/eventide_platform_interface.dart | 6 +- test/calendar_test.dart | 64 ++++++++++ test/event_test.dart | 114 ++++++++++++++++++ 8 files changed, 206 insertions(+), 17 deletions(-) diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt index d935487d..c9af8b97 100644 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/EventTests.kt @@ -704,6 +704,10 @@ class EventTests { mockWritableCalendar() every { contentResolver.update(eventContentUri, any(), any(), any()) } returns 1 + + mockRetrieveEvents(contentResolver, eventContentUri) + mockRetrieveAttendees(contentResolver, attendeesContentUri) + mockRetrieveReminders(contentResolver, remindersContentUri) var result: Result? = null val latch = CountDownLatch(1) @@ -739,6 +743,10 @@ class EventTests { every { contentResolver.update(eventContentUri, any(), any(), any()) } returns 1 every { contentResolver.delete(remindersContentUri, any(), any()) } returns 1 every { contentResolver.insert(remindersContentUri, any()) } returns mockk() + + mockRetrieveEvents(contentResolver, eventContentUri) + mockRetrieveAttendees(contentResolver, attendeesContentUri) + mockRetrieveReminders(contentResolver, remindersContentUri) var result: Result? = null val latch = CountDownLatch(1) diff --git a/android/src/test/kotlin/sncf/connect/tech/eventide/Mocks.kt b/android/src/test/kotlin/sncf/connect/tech/eventide/Mocks.kt index f7484c15..8367dddb 100644 --- a/android/src/test/kotlin/sncf/connect/tech/eventide/Mocks.kt +++ b/android/src/test/kotlin/sncf/connect/tech/eventide/Mocks.kt @@ -19,19 +19,22 @@ class Mocks { every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events._ID) } returns 0 every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.TITLE) } returns 1 every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.DESCRIPTION) } returns 2 - every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.CALENDAR_ID) } returns 3 + every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.EVENT_LOCATION) } returns 3 every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.DTSTART) } returns 4 every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.DTEND) } returns 5 - every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.ALL_DAY) } returns 6 + every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.EVENT_TIMEZONE) } returns 6 + every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.CALENDAR_ID) } returns 7 + every { eventCursor.getColumnIndexOrThrow(CalendarContract.Events.ALL_DAY) } returns 8 // Mock values for each column every { eventCursor.getString(0) } returns "eventId" every { eventCursor.getString(1) } returns "eventTitle" every { eventCursor.getString(2) } returns "eventDescription" - every { eventCursor.getString(3) } returns "calendarId" + every { eventCursor.getString(3) } returns "location" + every { eventCursor.getString(7) } returns "calendarId" every { eventCursor.getLong(4) } returns 1L every { eventCursor.getLong(5) } returns 1L - every { eventCursor.getInt(6) } returns 0 + every { eventCursor.getInt(8) } returns 0 } fun mockRetrieveAttendees(contentResolver: ContentResolver, attendeesContentUri: Uri) { diff --git a/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart b/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart index 4b5b6652..62a34fca 100644 --- a/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart +++ b/example/more-complex/full-permission/lib/calendar/logic/calendar_cubit.dart @@ -205,8 +205,8 @@ final class CalendarCubit extends Cubit { title: title, description: description, isAllDay: isAllDay, - startDate: startDate.millisecondsSinceEpoch, - endDate: endDate.millisecondsSinceEpoch, + startDate: startDate, + endDate: endDate, ); final updatedCalendars = Map>.from( diff --git a/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart b/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart index c7151a73..0edf9016 100644 --- a/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart +++ b/example/more-complex/full-permission/lib/event_details/logic/event_details_cubit.dart @@ -62,8 +62,8 @@ final class EventDetailsCubit extends Cubit> { title: title, description: description, isAllDay: isAllDay, - startDate: startDate.millisecondsSinceEpoch, - endDate: endDate.millisecondsSinceEpoch, + startDate: startDate, + endDate: endDate, ); }).forEach(emit); } diff --git a/lib/src/eventide.dart b/lib/src/eventide.dart index 8f97e0fd..54e89219 100644 --- a/lib/src/eventide.dart +++ b/lib/src/eventide.dart @@ -303,26 +303,26 @@ class Eventide extends EventidePlatform { ETEvent event, { String? calendarId, String? title, - int? startDate, - int? endDate, + DateTime? startDate, + DateTime? endDate, bool? isAllDay, String? description, String? url, String? location, - List? reminders, + Iterable? reminders, }) async { try { final updatedEvent = await _calendarApi.updateEvent( eventId: event.id, calendarId: calendarId ?? event.calendarId, title: title ?? event.title, - startDate: startDate ?? event.startDate.toUtc().millisecondsSinceEpoch, - endDate: endDate ?? event.endDate.toUtc().millisecondsSinceEpoch, + startDate: (startDate ?? event.startDate).toUtc().millisecondsSinceEpoch, + endDate: (endDate ?? event.endDate).toUtc().millisecondsSinceEpoch, isAllDay: isAllDay ?? event.isAllDay, description: description ?? event.description, url: url ?? event.url, location: location ?? event.location, - reminders: reminders ?? event.reminders.map((e) => e.toNativeDuration()).toList(), + reminders: (reminders ?? event.reminders).map((e) => e.toNativeDuration()).toList(), ); return updatedEvent.toETEvent(); } on PlatformException catch (e) { diff --git a/lib/src/eventide_platform_interface.dart b/lib/src/eventide_platform_interface.dart index 9e8881ad..bc310f5a 100644 --- a/lib/src/eventide_platform_interface.dart +++ b/lib/src/eventide_platform_interface.dart @@ -71,13 +71,13 @@ abstract class EventidePlatform extends PlatformInterface { ETEvent event, { String? calendarId, String? title, - int? startDate, - int? endDate, + DateTime? startDate, + DateTime? endDate, bool? isAllDay, String? description, String? url, String? location, - List? reminders, + Iterable? reminders, }); Future deleteEvent({required String eventId}); diff --git a/test/calendar_test.dart b/test/calendar_test.dart index 0a3d7a43..4cd2de9c 100644 --- a/test/calendar_test.dart +++ b/test/calendar_test.dart @@ -114,6 +114,70 @@ void main() { verify(() => mockCalendarApi.retrieveCalendars(onlyWritableCalendars: true, account: null)).called(1); }); + test('updateCalendar calls the API and returns an ETCalendar', () async { + // Given + final calendar = Calendar( + id: '987654321', + title: 'Updated Calendar', + color: Colors.green.toValue(), + isWritable: true, + account: Account(id: "1", name: 'Test Account', type: 'Test Type'), + ); + when( + () => mockCalendarApi.updateCalendar( + calendarId: any(named: 'calendarId'), + title: any(named: 'title'), + color: any(named: 'color'), + ), + ).thenAnswer((_) async => calendar); + + // When + await eventide.updateCalendar(calendar.toETCalendar(), title: 'Updated Calendar', color: Colors.green); + + // Then + verify( + () => mockCalendarApi.updateCalendar( + calendarId: '987654321', + title: 'Updated Calendar', + color: Colors.green.toValue(), + ), + ).called(1); + }); + + test('updateCalendar throws an exception when API fails', () async { + // Given + when( + () => mockCalendarApi.updateCalendar( + calendarId: any(named: 'calendarId'), + title: any(named: 'title'), + color: any(named: 'color'), + ), + ).thenThrow(ETGenericException(message: 'API Error')); + + // When + Future call() => eventide.updateCalendar( + ETCalendar( + id: '987654321', + title: 'Updated Calendar', + color: Colors.green, + isWritable: true, + account: ETAccount(id: "1", name: 'Test Account', type: 'Test Type'), + ), + title: 'Updated Calendar', + color: Colors.green, + ); + + // Then + expect(call, throwsException); + verify( + () => mockCalendarApi.updateCalendar( + calendarId: '987654321', + title: 'Updated Calendar', + color: Colors.green.toValue(), + ), + ).called(1); + }); + test('deleteCalendar calls the API', () async { // Given when(() => mockCalendarApi.deleteCalendar(calendarId: any(named: 'calendarId'))).thenAnswer((_) async => {}); diff --git a/test/event_test.dart b/test/event_test.dart index 47b932cf..6c64947c 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -786,6 +786,120 @@ void main() { expect(etEvents.length, 1); expect(etEvents.first.id, '1'); }); + + test('updateEvent with new start and end dates should update the event correctly', () async { + // Given + final originalEvent = Event( + id: '1', + title: 'Test Event', + isAllDay: false, + startDate: DateTime.now().millisecondsSinceEpoch, + endDate: DateTime.now().add(Duration(days: 1)).millisecondsSinceEpoch, + calendarId: '19', + description: 'Test Description', + url: 'http://test.com', + reminders: [10, 20], + attendees: [], + ); + + final updatedStartDate = DateTime.now().add(Duration(days: 2)); + final updatedEndDate = DateTime.now().add(Duration(days: 3)); + + when( + () => mockCalendarApi.updateEvent( + eventId: any(named: 'eventId'), + title: any(named: 'title'), + isAllDay: any(named: 'isAllDay'), + startDate: any(named: 'startDate'), + endDate: any(named: 'endDate'), + calendarId: any(named: 'calendarId'), + description: any(named: 'description'), + url: any(named: 'url'), + location: any(named: 'location'), + reminders: any(named: 'reminders'), + ), + ).thenAnswer((_) async => originalEvent); + + // When + await eventide.updateEvent( + originalEvent.toETEvent(), + title: 'Updated test Event', + startDate: updatedStartDate, + endDate: updatedEndDate, + ); + + // Then + verify( + () => mockCalendarApi.updateEvent( + eventId: '1', + title: 'Updated test Event', + isAllDay: originalEvent.isAllDay, + startDate: updatedStartDate.millisecondsSinceEpoch, + endDate: updatedEndDate.millisecondsSinceEpoch, + calendarId: '19', + description: originalEvent.description, + url: originalEvent.url, + location: originalEvent.location, + reminders: originalEvent.reminders, + ), + ).called(1); + }); + + test('updateEvent throws an exception when API fails', () async { + // Given + final originalEvent = Event( + id: '1', + title: 'Test Event', + isAllDay: false, + startDate: DateTime.now().millisecondsSinceEpoch, + endDate: DateTime.now().add(Duration(days: 1)).millisecondsSinceEpoch, + calendarId: '19', + description: 'Test Description', + url: 'http://test.com', + reminders: [10, 20], + attendees: [], + ); + + when( + () => mockCalendarApi.updateEvent( + eventId: any(named: 'eventId'), + title: any(named: 'title'), + isAllDay: any(named: 'isAllDay'), + startDate: any(named: 'startDate'), + endDate: any(named: 'endDate'), + calendarId: any(named: 'calendarId'), + description: any(named: 'description'), + url: any(named: 'url'), + location: any(named: 'location'), + reminders: any(named: 'reminders'), + ), + ).thenThrow(ETGenericException(message: 'API Error')); + + // When + Future call() => eventide.updateEvent( + originalEvent.toETEvent(), + title: 'Updated test Event', + startDate: DateTime.now().add(Duration(days: 2)), + endDate: DateTime.now().add(Duration(days: 3)), + ); + + // Then + expect(call, throwsException); + verify( + () => mockCalendarApi.updateEvent( + eventId: any(named: 'eventId'), + title: any(named: 'title'), + isAllDay: any(named: 'isAllDay'), + startDate: any(named: 'startDate'), + endDate: any(named: 'endDate'), + calendarId: any(named: 'calendarId'), + description: any(named: 'description'), + url: any(named: 'url'), + location: any(named: 'location'), + reminders: any(named: 'reminders'), + ), + ).called(1); + }); }); group('ETEventCopy tests', () { From 69593f04ad1d9ed481ed6f85b0eb43c1e780618a Mon Sep 17 00:00:00 2001 From: Alexis Choupault Date: Fri, 12 Jun 2026 12:33:16 +0200 Subject: [PATCH 9/9] improve coverage --- test/calendar_test.dart | 34 +++++++++++++++++--- test/event_test.dart | 70 +++++++++++++++++++++++++++++++++++------ 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/test/calendar_test.dart b/test/calendar_test.dart index 4cd2de9c..a3c41dae 100644 --- a/test/calendar_test.dart +++ b/test/calendar_test.dart @@ -114,12 +114,12 @@ void main() { verify(() => mockCalendarApi.retrieveCalendars(onlyWritableCalendars: true, account: null)).called(1); }); - test('updateCalendar calls the API and returns an ETCalendar', () async { + test('updateCalendar calls the API and returns an ETCalendar with updated values', () async { // Given final calendar = Calendar( id: '987654321', - title: 'Updated Calendar', - color: Colors.green.toValue(), + title: 'Calendar', + color: Colors.red.toValue(), isWritable: true, account: Account(id: "1", name: 'Test Account', type: 'Test Type'), ); @@ -137,13 +137,39 @@ void main() { // Then verify( () => mockCalendarApi.updateCalendar( - calendarId: '987654321', + calendarId: calendar.id, title: 'Updated Calendar', color: Colors.green.toValue(), ), ).called(1); }); + test('updateCalendar calls the API and returns an ETCalendar with no updated values', () async { + // Given + final calendar = Calendar( + id: '987654321', + title: 'Calendar', + color: Colors.red.toValue(), + isWritable: true, + account: Account(id: "1", name: 'Test Account', type: 'Test Type'), + ); + when( + () => mockCalendarApi.updateCalendar( + calendarId: any(named: 'calendarId'), + title: any(named: 'title'), + color: any(named: 'color'), + ), + ).thenAnswer((_) async => calendar); + + // When + await eventide.updateCalendar(calendar.toETCalendar(), title: null, color: null); + + // Then + verify( + () => mockCalendarApi.updateCalendar(calendarId: calendar.id, title: calendar.title, color: calendar.color), + ).called(1); + }); + test('updateCalendar throws an exception when API fails', () async { // Given when( diff --git a/test/event_test.dart b/test/event_test.dart index 6c64947c..7ea1546d 100644 --- a/test/event_test.dart +++ b/test/event_test.dart @@ -787,14 +787,14 @@ void main() { expect(etEvents.first.id, '1'); }); - test('updateEvent with new start and end dates should update the event correctly', () async { + test('updateEvent calls the API and returns an ETEvent with updated values', () async { // Given final originalEvent = Event( id: '1', title: 'Test Event', isAllDay: false, - startDate: DateTime.now().millisecondsSinceEpoch, - endDate: DateTime.now().add(Duration(days: 1)).millisecondsSinceEpoch, + startDate: DateTime(2023, 10, 1).millisecondsSinceEpoch, + endDate: DateTime(2023, 10, 2).millisecondsSinceEpoch, calendarId: '19', description: 'Test Description', url: 'http://test.com', @@ -802,8 +802,8 @@ void main() { attendees: [], ); - final updatedStartDate = DateTime.now().add(Duration(days: 2)); - final updatedEndDate = DateTime.now().add(Duration(days: 3)); + final updatedStartDate = DateTime(2023, 10, 5); + final updatedEndDate = DateTime(2023, 10, 6); when( () => mockCalendarApi.updateEvent( @@ -823,20 +823,72 @@ void main() { // When await eventide.updateEvent( originalEvent.toETEvent(), - title: 'Updated test Event', + title: 'Updated Test Event', startDate: updatedStartDate, endDate: updatedEndDate, + description: 'Updated Description', + url: 'http://updated.com', ); // Then verify( () => mockCalendarApi.updateEvent( eventId: '1', - title: 'Updated test Event', + title: 'Updated Test Event', isAllDay: originalEvent.isAllDay, - startDate: updatedStartDate.millisecondsSinceEpoch, - endDate: updatedEndDate.millisecondsSinceEpoch, + startDate: updatedStartDate.toUtc().millisecondsSinceEpoch, + endDate: updatedEndDate.toUtc().millisecondsSinceEpoch, calendarId: '19', + description: 'Updated Description', + url: 'http://updated.com', + location: originalEvent.location, + reminders: originalEvent.reminders, + ), + ).called(1); + }); + + test('updateEvent calls the API and returns an ETEvent with no updated values', () async { + // Given + final originalEvent = Event( + id: '1', + title: 'Test Event', + isAllDay: false, + startDate: DateTime(2023, 10, 1).millisecondsSinceEpoch, + endDate: DateTime(2023, 10, 2).millisecondsSinceEpoch, + calendarId: '19', + description: 'Test Description', + url: 'http://test.com', + reminders: [10, 20], + attendees: [], + ); + + when( + () => mockCalendarApi.updateEvent( + eventId: any(named: 'eventId'), + title: any(named: 'title'), + isAllDay: any(named: 'isAllDay'), + startDate: any(named: 'startDate'), + endDate: any(named: 'endDate'), + calendarId: any(named: 'calendarId'), + description: any(named: 'description'), + url: any(named: 'url'), + location: any(named: 'location'), + reminders: any(named: 'reminders'), + ), + ).thenAnswer((_) async => originalEvent); + + // When + await eventide.updateEvent(originalEvent.toETEvent()); + + // Then + verify( + () => mockCalendarApi.updateEvent( + eventId: '1', + title: originalEvent.title, + isAllDay: originalEvent.isAllDay, + startDate: originalEvent.startDate, + endDate: originalEvent.endDate, + calendarId: originalEvent.calendarId, description: originalEvent.description, url: originalEvent.url, location: originalEvent.location,