Skip to content

fix: [SDK-4481] dedupe listeners and harden plugin lifecycle - #13

Merged
fadi-george merged 21 commits into
mainfrom
fadi/sdk-4481
May 8, 2026
Merged

fix: [SDK-4481] dedupe listeners and harden plugin lifecycle#13
fadi-george merged 21 commits into
mainfrom
fadi/sdk-4481

Conversation

@fadi-george

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Stops the Capacitor SDK from registering duplicate native listeners on OneSignal.initialize() re-entry, and rounds out the plugin lifecycle on both platforms.

Details

Motivation

Calling OneSignal.initialize() from JS more than once per plugin instance (effect re-runs, hot reload, StrictMode mount→unmount→remount) was layering additional native observers on top of the existing ones. Both EventProducer.subscribe on Android and NSMutableArray addObject: on iOS append without identity dedup, so each re-entry produced a parallel listener path. The same shape existed at the JS bridge layer in three of the four namespace classes, and a stale Vite prebundle in the demo was hiding the fix from local validation. This PR fixes the underlying dedup contract on both platforms, fixes a few related lifecycle correctness issues that surfaced during the investigation, and adds dev scripts so the demo can iterate on this kind of issue with live reload.

Scope

Fixed:

  • Duplicate foregroundWillDisplay, click, permissionChange, pushSubscriptionChange, userStateChange, and the five IAM lifecycle events firing N times after N init calls (Android, iOS, JS).
  • Memory leak: notificationWillDisplayCache and preventDefaultCache growing unbounded; entries are now consumed on read.
  • Memory leak: MainScope() coroutines leaking past Activity.onDestroy() on Android.
  • Cold-start race where the SDK's ApplicationService never saw MainActivity.onResume() because Capacitor initialized the SDK after the resume had already fired, leaving isInForeground = false.
  • Cold-start UNNotificationResponse taps being dropped on iOS because the OneSignal SDK rejected them while no appId was set.
  • A regression where displayNotification and proceedWithWillDisplay would race on the cache and reject one of them; both are now idempotent.
  • Demo Vite prebundle going stale after SDK reinstall, masking SDK source changes from the running dev server.

Not changed: public API surface, native event payloads, JS event payloads, the OneSignal Android/iOS SDK versions in use.

Testing

Unit testing

vp test passes 177/177 with 100% line/function/statement coverage. New tests added:

  • InAppMessagesNamespace.test.ts — bridge-singleton test (3 add calls produce 1 bridge sub) and fan-out test (1 native event reaches all subscribers) for all five IAM events.
  • PushSubscriptionNamespace.test.ts — bridge-singleton test for change.
  • UserNamespace.test.ts — bridge-singleton test for change.

Manual testing

Tested for IAMs and Notification listeners:

  • tested closing app then click notification (triggers click listener)
  • tested clicking notification while app is active
  • tested click notification after minimizing
  • tested unmount/remount/hot reloading
  • tested w/ and w/o preventDefault and display notification

For Android:

  • tested extra flow of clicking back button then clicking notification

Affected code checklist

  • Notifications
    • Display
    • Open
    • Push Processing
    • Confirm Deliveries
  • Outcomes
  • Sessions
  • In-App Messaging
  • REST API requests
  • Public API changes

Checklist

Overview

  • I have filled out all REQUIRED sections above
  • PR does one thing — all changes converge on the same goal: making the plugin's listener lifecycle correct end-to-end on both platforms.
  • Any Public API changes are explained in the PR details and conform to existing APIs (no public API changes; behavior matches the documented contract that addEventListener is additive and initialize is idempotent)

Testing

  • I have included test coverage for these changes, or explained why they are not needed
  • All automated tests pass, or I explained why that is not possible
  • I have personally tested this on my device, or explained why that is not possible

Final pass

  • Code is as readable as possible.
  • I have reviewed this PR myself, ensuring it meets each checklist item

Made with Cursor

@fadi-george
fadi-george requested review from a team and nan-li May 7, 2026 23:22
Comment thread OneSignalCapacitorPlugin.podspec
Comment thread ios/Sources/OneSignalCapacitorPlugin/OneSignalCapacitorPlugin.swift
Comment thread ios/Sources/OSCapacitorLaunchOptions/OSCapacitorLaunchOptions.m
Comment thread ios/Sources/OSCapacitorLaunchOptions/OSCapacitorLaunchOptions.m Outdated
@fadi-george

Copy link
Copy Markdown
Contributor Author

@claude review

Comment thread ios/Sources/OSCapacitorLaunchOptions/OSCapacitorLaunchOptions.m
Comment thread examples/demo_pods/src/hooks/useOneSignal.ts Outdated

@abdulraqeeb33 abdulraqeeb33 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

one small nit otherwise looks fine to me

@fadi-george
fadi-george merged commit ad68687 into main May 8, 2026
6 checks passed
@fadi-george
fadi-george deleted the fadi/sdk-4481 branch May 8, 2026 17:54
@fadi-george fadi-george mentioned this pull request May 8, 2026
@nan-li
nan-li restored the fadi/sdk-4481 branch June 10, 2026 21:38
@nan-li
nan-li deleted the fadi/sdk-4481 branch June 10, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants