fix(ios): emit every plugin event on the main thread

Merged
OR
24d ago
+67-27

What

  • Every event the iOS plugin emits now goes through notifyListenersOnMain, so it's delivered on the main thread. That covers appReady, updateAvailable, set, noNeedUpdate, updateFailed, appReloaded, updateCheckResult, breakingAvailable / majorAvailable and the download-failure events.
  • notifyListenersOnMain gains a retainUntilConsumed parameter (default false), so retained events keep their behavior.
  • CapgoUpdater.notifyListeners reuses the same helper instead of its own copy of the main-thread hop.

Why

  • CAPPlugin stores listeners in an unsynchronized NSMutableDictionary (ionic-team/capacitor#8157). addListener / removeListener run on the plugin's queue, so notifying from another thread can crash in -[__NSDictionaryM objectForKey:] inside -[CAPPlugin notifyListeners:data:retainUntilConsumed:].
  • Most events already hop to main through notifyListenersOnMain, but appReady is still emitted from DispatchQueue.global() in sendReadyToJs, and updateAvailable from background download work. We shipped a patch for exactly these calls in our app after seeing this crash in production.
  • I've also opened a fix in Capacitor core (ionic-team/capacitor#8635). This change keeps apps on current and older Capacitor versions safe regardless.

How

  • Mechanical: replaced the remaining direct self.notifyListeners(...) calls with self.notifyListenersOnMain(...). Calls already on the main thread stay synchronous, so ordering and existing behavior on main are unchanged.

Testing

  • New testSendReadyToJsNotifiesAppReadyOnMainThread: calls sendReadyToJs and asserts appReady arrives on the main thread with retainUntilConsumed: true.
    • Without the fix: XCTAssertEqual failed: ("Optional(false)") is not equal to ("Optional(true)")
    • With the fix: passes.
  • Full iOS suite, xcodebuild test -scheme CapgoCapacitorUpdater -destination 'platform=iOS Simulator,name=iPhone 17' (iOS 26.5, Xcode 26.6): 202 tests, 0 failures (main: 201, 0 failures).
  • SwiftLint 0.65.1 with @ionic/swiftlint-config: same warning count before and after on both changed files, none new.

Not Tested

  • Android is unchanged and not affected.
  • Didn't run bun run verify / bun run fmt (no Bun on this machine); ran xcodebuild tests and SwiftLint directly instead.
  • Not exercised in the example app or Maestro flows.

This PR was prepared with an AI agent (Claude Code); I reviewed the change and the test results.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • Bug Fixes
    • Improved delivery of app-ready notifications, including when events are emitted outside the main thread.
    • App-ready notifications are now retained until consumed, helping ensure the application receives readiness updates.