Skip to content

fix: [SDK-4814] Live Activities Confirmed Receipts exceeding Delivered count#1681

Open
nan-li wants to merge 4 commits into
mainfrom
nan/sdk-4814
Open

fix: [SDK-4814] Live Activities Confirmed Receipts exceeding Delivered count#1681
nan-li wants to merge 4 commits into
mainfrom
nan/sdk-4814

Conversation

@nan-li

@nan-li nan-li commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Stop Live Activities from reporting a receive receipt more than once per device, which inflated the dashboard's Confirmed Receipts count above Delivered.

Details

Reproduced this manually by starting a live activity on the device, opening the app, swiping it away, reopening the app… It looks like ActivityKit re-emits content updates on app relaunches even when the content of the live activity does not change. The SDK had been treating each update as a separate message and was not deduplicating.

Motivation

For Live Activities, the dashboard's Confirmed Receipts count could exceed Delivered (SDK-4814). A receive receipt for a given notificationId was reported to OneSignal more than once per device because:

  • The receipt was attributed to the Activity's current content snapshot (activity.content.state) instead of the content actually delivered in that update, so a burst of content updates could mis-attribute the notificationId.
  • On success the receipt request was forgotten with no record that it had already been sent, so a later content update — or ActivityKit re-emitting the active activity's content on app relaunch — re-sent it.

Scope

  • Receive receipts are attributed to the iterated content.state of each update rather than the live activity.content snapshot.
  • The receive-receipts request cache is now the single source of truth for dedup: sent receipts are kept (shouldForgetWhenSuccessful = false) as markers, so both in-session re-emits and post-relaunch re-emits for the same notificationId are suppressed.
  • The cache TTL is reduced from 30 to 3 days, bounding how long sent markers and unsent retries are retained.
  • No change to Live Activity start/update token handling, notification display, or open behavior.

Testing

Unit testing

  • Added testReceiveReceiptsNotResentForSameNotificationIdAfterRelaunch, a regression test that sends a receipt through one executor, then has a second executor (simulating an app relaunch sharing the same persisted cache) re-enqueue the same notificationId and verifies it is not re-sent.
  • Updated testReceiveReceiptsWithSuccessfulRequest and testReceiveReceiptsRequestNotExecutedWithSameNotificationId to assert the sent receipt is retained in the cache as a dedup marker (requestSuccessful == true) rather than removed.

Manual testing

On-device, with a Live Activity on the Lock Screen, swiped the app away to terminate it and reopened it — the path where ActivityKit re-emits the active activity's current content on launch. Before the fix this sent a duplicate Confirmed Receipt for the same notificationId; after the fix the duplicate no longer reproduces.

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

nan-li and others added 4 commits June 29, 2026 09:23
…red content

Read the notificationId from the content yielded by this contentUpdates
iteration instead of activity.content (the latest snapshot), which can
advance while the loop is suspended and mis-attribute the receipt to a
different update.

Co-authored-by: Cursor <cursoragent@cursor.com>
A receive receipt for a notificationId was re-sent on later content
updates or app relaunches once the prior request was forgotten on
success, inflating Confirmed Receipts above Delivered. Track sent
notificationIds in an in-memory set, claimed at enqueue, so each is
reported at most once per session, and shorten the unsent-receipt
retry window from 30 to 7 days.

Co-authored-by: Cursor <cursoragent@cursor.com>
…unches

The in-memory dedup set did not survive app relaunches, so when ActivityKit
re-emits an active activity's content on launch the same notificationId was
reported again. Keep sent receipts in the persisted cache as dedup markers
(shouldForgetWhenSuccessful = false); supersedes == false drops the re-send
and pollPendingRequests skips successful entries, making the cache the single
source of truth. Remove the redundant in-memory set and shorten the receive-
receipts cache TTL from 7 to 3 days. Adds a relaunch regression test.

Co-authored-by: Cursor <cursoragent@cursor.com>
The relaunch regression test pushed OSLiveActivitiesExecutorTests over
SwiftLint's type_body_length error threshold, failing the CI lint step.
Collapse the repeated per-test subscribed-user setup into a single helper,
which removes the duplication and keeps the class body under the limit.

Co-authored-by: Cursor <cursoragent@cursor.com>
@nan-li

nan-li commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Ran multi-model-review with GPT5.5, Opus 4.8, Codex

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