Skip to content

fix: [SDK-5139] display foreground notifications by default - #1989

Merged
fadi-george merged 5 commits into
mainfrom
fadi/sdk-5139
Sep 1, 2026
Merged

fix: [SDK-5139] display foreground notifications by default#1989
fadi-george merged 5 commits into
mainfrom
fadi/sdk-5139

Conversation

@fadi-george

Copy link
Copy Markdown
Collaborator

Description

One Line Summary

Display foreground notifications automatically unless a listener calls preventDefault().

Details

Motivation

The native bridges prevent foreground display before dispatching to JavaScript to avoid an asynchronous bridge race. Previously, that meant adding a log-only listener suppressed the notification unless the app explicitly called display().

Scope

  • Track the synchronous JavaScript listener decision and display automatically when no listener prevents it.
  • Support multiple listeners, with any preventDefault() call suppressing automatic display.
  • Preserve explicit and deferred display without duplicate native display calls.
  • Keep notifications displaying if a listener throws while allowing remaining listeners to run.
  • No public API changes.

Testing

Unit testing

  • Added coverage for automatic display, no listeners, multiple listeners, prevention, deferred display, explicit display, duplicate display calls, and thrown listeners.
  • vp check
  • vp test: 272 tests passed
  • vp run build
  • Multi-model review completed with Claude Opus 5, GPT 5.6 Sol, and Cursor Grok 4.6.

Manual testing

Tested on Android and iOS with:

  • handleForegroundWillDisplay containing only a log.
  • preventDefault() uncommented.
  • preventDefault() followed by display().
  • preventDefault() followed by display() after approximately 25 seconds.

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
  • Any Public API changes are explained in the PR details and conform to existing APIs

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 a review from a team as a code owner September 1, 2026 19:00

@sherwinski sherwinski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should-fix (not inline-able)

MIGRATION_GUIDE.md:330-341 still documents the old model. Only examples/build.md was updated. The migration guide is the file users read when they hit this behavior, and its example implies preventDefault() is the normal path. Add a line that says foreground notifications now display automatically, and that preventDefault() is only needed to hold display.

This is a behavior change for any app that registered a foregroundWillDisplay handler as a silent-suppress mechanism. Before this branch the notification never showed. Now it does. That deserves an explicit note in the guide and in the release notes, not only a comment in the demo.

this.dispatchNotificationWillDisplayHandlers(event);
} finally {
if (!isDefaultPrevented(event) && !isDisplayRequested(event)) {
event.getNotification().display();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should-fix: this path bypasses the injected native module.

EventManager gets RNOneSignal: Spec in the constructor, but OSNotification.display() uses the module-level NativeOneSignal singleton. So the class talks to two different native handles.

The test file shows the effect: EventManager.test.ts must now import the global mockRNOneSignal to assert a call made by the object under test, while every other assertion in that file uses the injected mockModule.

Both handles resolve to the same module in production, so there is no runtime bug. But the injection is now misleading. Two options:

  1. Pass the Spec into NotificationWillDisplayEvent and let it use the injected handle.
  2. Call this.RNOneSignal.displayNotification(...) here, and let the event track display state only.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Both handles are the same singleton in production because EventManager is constructed with RNOneSignal. OSNotification.display() and NotificationWillDisplayEvent.preventDefault() already use that singleton, while the injected handle is used for event subscriptions. Calling the injected module only for automatic display would split display behavior and bypass the notification wrapper’s idempotency tracking, and passing Spec into the exported event would change its public constructor. I’m keeping the existing notification action pattern.

const event = new NotificationWillDisplayEvent(payload as OSNotification);
try {
this.dispatchNotificationWillDisplayHandlers(event);
} finally {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should-fix: a throw from this finally block discards the handler error.

If display() fails (for example, the native module is not loaded), the new exception replaces the pending exception from dispatchNotificationWillDisplayHandlers, and the original handler error is lost.

A local try/catch keeps the handler error as the visible one:

} finally {
  if (!isDefaultPrevented(event) && !isDisplayRequested(event)) {
    try {
      event.getNotification().display();
    } catch (error) {
      console.error('OneSignal: could not display foreground notification', error);
    }
  }
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is technically possible, but it requires both a handler and the native display bridge to throw. A missing module fails earlier at getEnforcing(), and both native display implementations log and return on cache misses rather than throwing. The suggested catch would also swallow a real display failure when no handler error exists, so I’m keeping the current propagation behavior.

}
}

private dispatchNotificationWillDisplayHandlers(event: NotificationWillDisplayEvent) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should-fix: this method duplicates dispatchHandlers for one event.

It is dispatchHandlers plus two changes, and both changes apply to every event, not only this one:

  1. The array copy on the next line fixes the case where a handler removes itself during dispatch. dispatchHandlers (line 154) still iterates the live array, so it skips the next handler in that case.
  2. Error isolation. NOTIFICATION_WILL_DISPLAY now runs all handlers when one throws. Every other event still stops at the first throw.

Suggest one of two things: fold the behavior into dispatchHandlers with an options argument, or keep the split and add a comment that says why this event is different. As written, a reader must diff the two methods to find the difference.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added a comment explaining that every foreground handler must run because any one can prevent automatic display. I kept this path separate to avoid changing error behavior for unrelated events.

Co-authored-by: Cursor <cursoragent@cursor.com>
@fadi-george

Copy link
Copy Markdown
Collaborator Author

Addressed the valid review points in 5d6ba24: updated the migration guide, clarified that late preventDefault calls cannot stop display, restored the notification ID assertion, and documented why foreground dispatch is specialized.

@fadi-george
fadi-george merged commit 684e025 into main Sep 1, 2026
2 checks passed
@fadi-george
fadi-george deleted the fadi/sdk-5139 branch September 1, 2026 21:27
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.

2 participants