Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Comments

fix: batch Apple Reminders sync into single silent push#4920

Draft
mdmohsin7 wants to merge 9 commits intomain from
fix/apple-reminders-batch-sync
Draft

fix: batch Apple Reminders sync into single silent push #4920
mdmohsin7 wants to merge 9 commits intomain from
fix/apple-reminders-batch-sync

Conversation

@mdmohsin7
Copy link
Member

@mdmohsin7 mdmohsin7 commented Feb 20, 2026

Summary

  • Batch all action items into a single silent push instead of sending one per item — eliminates iOS silent push throttling that was causing most tasks to be dropped when a conversation had multiple action items
  • Add foreground FCM handling for apple_reminders_sync — previously this message type was unhandled in Flutter's foreground listener, causing sync to silently fail when the app was open
  • Add syncFromFCM native method — allows Flutter to forward the batch payload to native iOS for EKReminder creation when the app is in the foreground

Root Cause

Two issues were causing tasks to not sync to Apple Reminders:

  1. iOS silent push throttling: The backend was sending N individual silent pushes for N action items from a single conversation, all within milliseconds. iOS aggressively throttles content-available pushes, delivering only the first and dropping the rest.

  2. Foreground handling gap: When the app was in the foreground, Flutter's FirebaseMessaging.onMessage intercepted the FCM message but had no handler for apple_reminders_sync, so it was silently discarded. The native didReceiveRemoteNotification is not called when FlutterFire handles the message.

Changes

Backend

  • notifications.py: send_apple_reminders_sync_push now accepts a list of action items and JSON-encodes them into a single push payload
  • task_sync.py: auto_sync_action_items_batch checks the integration once and sends all Apple Reminders items in one push (also eliminates N redundant Firestore lookups)

iOS Native

  • AppDelegate.swift: handleAppleRemindersSync parses a JSON array batch and creates all reminders in one background execution pass, calls markExportedBatch to notify Flutter
  • AppleRemindersService.swift: New syncFromFCM method handles the same batch logic when triggered from the foreground via MethodChannel

Flutter

  • notification_service_fcm.dart: Added apple_reminders_sync case in the foreground FCM listener, forwarding to native via AppleRemindersService.triggerSyncFromFCM
  • apple_reminders_service.dart: Added markExportedBatch handler and triggerSyncFromFCM method that invokes native and marks exported items in the backend

Test plan

  • Create a conversation that generates 3+ action items with Apple Reminders as default integration
  • Verify all items appear in Apple Reminders (not just the first one)
  • Test with app in foreground during conversation processing
  • Test with app in background during conversation processing
  • Verify no duplicate reminders on re-processing
  • Manually create a single action item and verify it syncs

🤖 Generated with Claude Code

mdmohsin7 and others added 6 commits February 20, 2026 18:17
Previously one silent push was sent per action item, causing iOS
to throttle and drop most pushes when a conversation had multiple
tasks. Now all items are JSON-encoded into a single push payload.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactored auto_sync_action_items_batch to check the integration
once and send all Apple Reminders items in one push, eliminating
redundant Firestore lookups and iOS silent push throttling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Parse JSON array of action items from single push and create all
reminders in one background execution pass. Use markExportedBatch
callback to notify Flutter of all exported items at once.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the app is in the foreground, FCM messages are handled by
Flutter, not the native silent push handler. This method allows
Flutter to forward the batch payload to native for processing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Handle markExportedBatch callback from native and add
triggerSyncFromFCM to forward foreground FCM messages to native
for Apple Reminders creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously this message type was unhandled in the Flutter foreground
FCM listener, causing sync to silently fail when the app was open.
Now forwards the payload to native via AppleRemindersService.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses the iOS silent push throttling issue by batching Apple Reminders sync actions into a single push. The changes are well-structured across the backend, native iOS, and Flutter layers. I've identified a significant area of code duplication in the native iOS code between AppDelegate.swift and AppleRemindersService.swift that should be refactored for better maintainability. Additionally, there are opportunities in the Flutter code to improve performance by parallelizing API calls. Overall, this is a solid fix with a few areas for refinement.

Comment on lines 41 to 113
private func syncFromFCM(call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let itemsJson = args["items"] as? String,
let data = itemsJson.data(using: .utf8),
let items = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing or invalid items payload", details: nil))
return
}

guard !items.isEmpty else {
result([String]())
return
}

guard hasRemindersAccess() else {
result(FlutterError(code: "PERMISSION_DENIED", message: "Reminders permission not granted", details: nil))
return
}

guard let calendar = eventStore.defaultCalendarForNewReminders() else {
result(FlutterError(code: "NO_CALENDAR", message: "No default reminders calendar", details: nil))
return
}

var syncedIds = Set(UserDefaults.standard.stringArray(forKey: AppleRemindersService.syncedItemsKey) ?? [])
var exportedIds: [String] = []

for item in items {
guard let actionItemId = item["id"] as? String,
let reminderTitle = item["description"] as? String else {
continue
}

if syncedIds.contains(actionItemId) {
continue
}

let dueDate: Date? = {
if let dueDateStr = item["due_at"] as? String, !dueDateStr.isEmpty {
return AppleRemindersService.iso8601DateFormatter.date(from: dueDateStr)
}
return nil
}()

let reminder = EKReminder(eventStore: eventStore)
reminder.title = reminderTitle
reminder.notes = "From Omi"
reminder.calendar = calendar

if let due = dueDate {
reminder.dueDateComponents = Calendar.current.dateComponents(
[.year, .month, .day, .hour, .minute], from: due
)
}

do {
try eventStore.save(reminder, commit: true)
syncedIds.insert(actionItemId)
exportedIds.append(actionItemId)
} catch {
continue
}
}

// Persist dedup set
var syncedArray = Array(syncedIds)
if syncedArray.count > 100 {
syncedArray = Array(syncedArray.suffix(100))
}
UserDefaults.standard.set(syncedArray, forKey: AppleRemindersService.syncedItemsKey)

result(exportedIds)
}
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is significant code duplication between this syncFromFCM function and the handleAppleRemindersSync function in AppDelegate.swift. Both functions implement the same logic for parsing a batch of action items and creating EKReminder objects.

This duplication makes the code harder to maintain, as any future changes to the sync logic will need to be applied in two places.

To improve this, you should refactor the common logic into a single, shared method. Since AppDelegate already has an instance of AppleRemindersService, you could create a new private method within AppleRemindersService that encapsulates the core batch processing logic.

For example, you could create a function like this in AppleRemindersService:

private func syncReminderBatch(items: [[String: Any]]) -> (createdCount: Int, exportedIds: [String]) {
 // ... core logic from syncFromFCM ...
 return (createdCount, exportedIds)
}

Then, syncFromFCM and handleAppleRemindersSync can call this shared method and handle the results accordingly. This will make the code more modular and easier to maintain.

Comment on lines 20 to 22
for (final id in ids) {
await _markActionItemExported(id);
}
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

You are awaiting _markActionItemExported for each ID in a loop. This executes the API calls sequentially, which can be inefficient if there are many IDs to process.

To improve performance, you can run these asynchronous operations in parallel using Future.wait. This will send all the requests concurrently and wait for all of them to complete.

Suggested change
for (final id inids) {
await _markActionItemExported(id);
}
if (ids.isNotEmpty) {
await Future.wait(ids.map((id) =>_markActionItemExported(id)));
}

Comment on lines 53 to 55
for (final id in exportedIds) {
await _markActionItemExported(id);
}
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the markExportedBatch handler, you are awaiting _markActionItemExported for each ID in a loop. This results in sequential API calls.

For better performance, especially with multiple action items, you should execute these calls in parallel using Future.wait.

Suggested change
for (final id inexportedIds) {
await _markActionItemExported(id);
}
if (exportedIds.isNotEmpty) {
await Future.wait(exportedIds.map((id) =>_markActionItemExported(id)));
}

mdmohsin7 and others added 3 commits February 20, 2026 21:02
Move the core batch-sync logic (parse JSON, dedup, create EKReminders,
persist dedup set) into a single public method so both the background
silent-push path (AppDelegate) and the foreground MethodChannel path
share the same implementation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove duplicated batch-sync logic from AppDelegate and call the
shared method on AppleRemindersService. Remove unused EventKit
import and iso8601DateFormatter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Run all _markActionItemExported HTTP calls concurrently instead
of sequentially, in both the markExportedBatch handler and the
triggerSyncFromFCM method.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

1 more reviewer

@gemini-code-assist gemini-code-assist[bot] gemini-code-assist[bot] left review comments

Reviewers whose approvals may not affect merge requirements

At least 1 approving review is required to merge this pull request.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

AltStyle によって変換されたページ (->オリジナル) /