Skip to content

fix: [SDK-5313] run in-app message trigger and pause changes on the main queue - #1755

Open
nan-li wants to merge 2 commits into
mainfrom
nan/sdk-5313
Open

nan-li wants to merge 2 commits into
mainfrom
nan/sdk-5313

Conversation

@nan-li

@nan-li nan-li commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Run in-app trigger changes and pause evaluation on the main queue, where the message state already lives. SDK-5313, fixes #1753.

Details

Motivation

addTriggers, removeTriggers, clearTriggers and unpausing evaluated messages on the calling thread. They read messages, redisplayedInAppMessages and earlySessionTriggers without a lock. Meanwhile, the fetch completion and dismiss path write to the same state on the main queue.

React Native calls these methods from a background queue. If the operations overlap, the app crashes in evaluateRedisplayedInAppMessages with EXC_BAD_ACCESS.

Scope

  • The new runOnMainQueue: method in OSMessagingController runs a block inline on the main thread or dispatches it asynchronously from any other thread.
  • addTriggers:, removeTriggersForKeys: and clearTriggers run their bodies through the helper.
  • clearTriggers reads the keys inside the block, so it also clears a trigger queued immediately before it.
  • setInAppMessagingPaused: still sets the flag synchronously, which means paused reads back immediately. The evaluation runs through the helper, and so does the check for a showing message before a dismiss is queued. A main-thread caller runs that check at call time, as before.
  • messageIsNotActive: deletes the message inside the main-queue block it already used for the window cleanup. The HTML request fails on the session queue, so the delete used to replace messages off main too.
  • Callers already on the main thread see no change. Neither the public API nor requests change.

Testing

Unit testing

TriggerThreadingTests seeds one message with redisplay state, calls each entry point from a global queue while holding the main thread, then drains the main queue. The add and remove tests verify that the redisplay check and the trigger controller aren't touched on the calling thread and run on the main thread afterward. The clear test verifies the same for the trigger controller and that a trigger queued from the same background call just before it is removed. The unpause test verifies the flag reads back immediately and that evaluation runs on the main thread. All four fail without this fix.

testPreviewIAMIsDisplayedOnPause now drains the main queue before its assertion, so a pause that wrongly queues a dismiss with nothing showing fails it.

Manual testing

The OneSignalInAppMessagesTests bundle ran locally on an iPhone 17 Pro simulator with Xcode 27. Not run on a device.

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

🤖 Generated with Claude Code

@nan-li nan-li changed the title fix: [SDK-5313] run trigger and pause changes on the main queue fix: [SDK-5313] run in-app message trigger and pause changes on the main queue Sep 24, 2026
@nan-li
nan-li force-pushed the nan/sdk-5313 branch 2 times, most recently from 3d63c3c to 9d03890 Compare September 24, 2026 01:05
…ain queue

addTriggers, removeTriggers, clearTriggers and unpausing read and mutate the
controller's message collections on the caller's thread, while the fetch
completion and the dismiss path write them on the main queue. A wrapper that
calls these off main, such as React Native, can crash inside
evaluateRedisplayedInAppMessages when the two overlap.

Hop the bodies onto the main queue when called off it and run them inline when
already there. The pause flag is still written before the hop so the getter
reflects the call immediately, and clearTriggers reads the keys on the main
queue so a trigger queued just ahead of it is cleared too.

The pause branch now checks isInAppMessageShowing on the main queue, and
messageIsNotActive deletes the message on the main queue as well, since the
HTML request failure that reaches it arrives on the session queue.
…dismiss

Pausing queued the showing check itself on the main queue, so a pause with
nothing showing followed by a message or preview in the same run-loop turn
dismissed that message before it rendered and marked it seen. Run the check
through the main-queue helper instead, so a main-thread caller checks at call
time as before and a background caller checks on main, and keep only the
dismiss deferred.

The preview-on-pause integration test now drains the main queue before its
assertion so this cannot regress.
@nan-li
nan-li marked this pull request as ready for review September 24, 2026 16:29
@nan-li
nan-li requested a review from a team September 25, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant