Skip to content

fix: [SDK-5314] JWT - drop the Identity Verification beta queues and limit cached queue - #1758

Merged
nan-li merged 11 commits into
5.8-mainfrom
nan/sdk-5314
Oct 7, 2026
Merged

nan-li merged 11 commits into
5.8-mainfrom
nan/sdk-5314

Conversation

@nan-li

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

Copy link
Copy Markdown
Contributor

Description

One Line Summary

Remove the 5.3.0-beta pending-auth queues at start, cap cached values, make UserDefaults archiving exception-safe, and re-create a cached user whose Create User was lost. SDK-5314.

Details

Motivation

Devices upgrading from the 5.3.0-beta line carry five *_PENDING_QUEUE_KEY blobs that this branch never reads, and on affected devices those blobs and the regular queue keys are large. Archiving or writing an oversized queue is what crashes the beta today, and the UserDefaults helper here would crash the same way on an unarchivable or oversized value. A device whose only Create User sat in one of those blobs has an external_id and no onesignal_id, and nothing in the SDK would ever create it.

Scope

  • OneSignalUserManagerImpl.start() removes the five beta pending keys before the executors read their caches. Their names return to OneSignalCommonDefines.h as OS_IV_BETA_*.
  • Every cached queue read in the five executors and the operation repo passes a 1 MB cap, OS_USER_DEFAULTS_MAX_VALUE_BYTES, the same limit the write side applies. A larger blob is removed and logged, not decoded. The one-time transfer-subscription migration read is left as is.
  • OneSignalUserDefaults catches exceptions from archiving and unarchiving instead of raising into the host app. A blob that is not archived data or cannot be decoded is logged and the default returned; a capped read also removes it, an uncapped read leaves it to the module that wrote it. A nil decode is treated like a missing key. A value that cannot be archived leaves the previous blob in place. A value whose archive is larger than 1 MB is not written and the key is removed: nothing the SDK caches is legitimately that large, and a previous blob left under the key would resend the Requests it held, which is the symptom this PR exists to stop. CFPreferences rejects values of 4 MB and more anyway.
  • OSUserExecutor queues a Create User at start for a cached user that has no onesignal_id when nothing queued will supply one. The beta parked the only copy of that Request under a key this PR removes, and an oversized or undecodable queue loses it too; without this the device stays unreachable, since a login as the same user is a no-op. Under Identity Verification the Create User parks and asks the app for a token like any other; an anonymous cached user is left alone there, since it is never created. The recovery sees only the user start() loaded from the cache, captured when the executor is created, so a user that a login or a legacy-player migration sets up during the same start is not re-created.
  • The beta's pending copies are dropped, not migrated. The beta also merged them into the regular queue keys, which this branch reads within the cap.
  • No request payload changes. A dropped queue means its Requests are not sent.

Testing

Unit testing

Seven tests in OneSignalCoreTests cover a real archive over the cap, a blob within the cap, an unreadable blob, an unarchivable value leaving the cached value in place, a value over the limit taking the previous blob with it, and blobs that are not archived data or cannot be decoded being removed only by a capped read. One test in OneSignalUserTests seeds all five beta keys plus a real archive over the cap under the update key, written directly since the save path now refuses it, drives start(), and asserts the keys are gone and the queue is empty. Without the cap that archive decodes to strings, fails the cast and leaves the key as is, so the test fails. CachedUserWithoutAnIdTests covers the re-created Create User: an identified and an anonymous cached user with the requirement off, no duplicate when the Create User is still queued, the token ask and the signed Create User with the requirement on, and an anonymous cached user left alone under it. A fresh start still sends exactly one Create User and a legacy-player start queues none.

Manual testing

OneSignalCoreTests and OneSignalUserTests run on an iPhone 17 Pro simulator (iOS 26.5), and the full Reduced test plan ran on this branch merged with 5.8-main at 630b355, 597 tests with no failures. The dev app from the first revision of this branch installed over 5.3.0-beta-03 state with 8 parked Requests: the two beta keys present on that device removed at start, no crash, Requests held until a token arrives. The bug-bash run of 5 October reproduced the lost Create User on that upgrade path (scenario 12.3 in the test doc); the re-created Create User is covered by unit tests only so far. 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 marked this pull request as ready for review September 24, 2026 16:36
@nan-li nan-li changed the title fix: [SDK-5314] drop the Identity Verification beta queues and guard cached queue blobs fix: [SDK-5314] drop the Identity Verification beta queues and guard cached queue Sep 24, 2026
@nan-li nan-li changed the title fix: [SDK-5314] drop the Identity Verification beta queues and guard cached queue fix: [SDK-5314] JWT - drop the Identity Verification beta queues and limit cached queue Sep 25, 2026
@nan-li
nan-li requested a review from a team September 25, 2026 16:58
@fadi-george

Copy link
Copy Markdown
Collaborator

Potential issues:

  • Possibly blocking: OSUserExecutor.swift:170, the 1 MB cap drops the whole request queue, including a pending Create User for the cached user. start() then loads that user from disk and never recreates it, so it never gets a onesignal_id and all its later updates stay stuck. Most likely on beta devices, since their parked logins were merged into this queue. Can you confirm or rule this out?
  • Non-blocking: OneSignalUserDefaults.m:194, the write limit is 4 MB but the read limit is 1 MB. A queue between 1 and 4 MB saves fine, then gets silently dropped on the next launch. Probably worth fixing with the one above.
  • Nit: OneSignalUserTests.swift:608, IdentityVerificationBetaCacheTests sits between the optedIn doc comment and RemoteDisableOptedInTests, so that comment now describes the wrong class.

@abdulraqeeb33 abdulraqeeb33 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.

Two places removeValueForKey deletes a blob the rest of this helper would have kept.

  1. Unarchive @catch runs for every caller, including the identity model store. Gate that remove on maxBytes > 0.
  2. The 4 MB write branch should return without removing, same as archive failure.

Comment thread iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m
Comment thread iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalUserDefaults.m
nan-li added 4 commits October 6, 2026 00:03
The beta cache test class sat between the note and RemoteDisableOptedInTests.
The queue reads are the ones that self-heal by deletion. An uncapped read is a model store or a cache that owns its key, so a blob that is not archived data or raises on decode is now logged and left in place unless maxBytes is set.
…alues

A queue between the 1 MB read cap and the 4 MB write limit was saved, then dropped at the next launch. The write path now refuses the same 1 MB, and still removes the previous blob: a queue that large is the bloat the read cap drops, and an older blob left under the key would resend the Requests it held. The beta cache test writes its oversized archive directly, since the save path refuses it now.
A cached user with no onesignal_id has nothing left to create it once its queue is dropped as oversized or undecodable, or when a 5.3.0-beta build parked the Request under a key start() now removes. A login as the same user is a no-op, so the device stayed unreachable until a logout. The user executor now queues a Create User at start unless a Create User, Fetch Identity By Subscription or Identify User for that model is already queued. Under Identity Verification the Request parks and asks the app for a token; an anonymous cached user is left alone there, since it is never created.
@nan-li

nan-li commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Potential issues:

  • Possibly blocking: OSUserExecutor.swift:170, the 1 MB cap drops the whole request queue, including a pending Create User for the cached user. start() then loads that user from disk and never recreates it, so it never gets a onesignal_id and all its later updates stay stuck. Most likely on beta devices, since their parked logins were merged into this queue. Can you confirm or rule this out?
  • Non-blocking: OneSignalUserDefaults.m:194, the write limit is 4 MB but the read limit is 1 MB. A queue between 1 and 4 MB saves fine, then gets silently dropped on the next launch. Probably worth fixing with the one above.
  • Nit: OneSignalUserTests.swift:608, IdentityVerificationBetaCacheTests sits between the optedIn doc comment and RemoteDisableOptedInTests, so that comment now describes the wrong class.
  • Confirmed. The cap, an undecodable queue, and a beta device whose parked Create User sat only under a deleted pending key all leave a user with an external_id and no onesignal_id. I hit the last one on the simulator. start() now queues a Create User for a cached user with no onesignal_id when nothing queued will supply one, except an anonymous user under Identity Verification. Covered by CachedUserWithoutAnIdTests.
  • Done, one 1 MB constant for both ends.
  • Done, the class moved below RemoteDisableOptedInTests.

@abdulraqeeb33 abdulraqeeb33 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.

Read-path gate is done. The write wipe stays.

One remaining: recovery reads live _user on the executor queue. Path 2/3 can publish a new user before their append, so this block can enqueue a second Create User. Snapshot the Path 1 user in init.

Comment thread iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift Outdated
@nan-li
nan-li requested a review from abdulraqeeb33 October 6, 2026 19:27
The recovery read the live user on the executor queue. Path 2 and 3 of start() set a new user before their own Request is appended, so a block that ran in between queued a second Create User on a fresh start, or one ahead of a legacy player's Fetch Identity By Subscription. The user is now captured in init, where it can only be the one Path 1 loaded, and the queue check still runs on the executor queue after the transfer migration so a migrated Create User is seen. Two tests pin the fresh-start and legacy-player paths.

@abdulraqeeb33 abdulraqeeb33 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.

Done in 5498904. Recovery now takes the Path 1 user captured in init. Nil means Path 2/3, and it returns before any async work.

@nan-li
nan-li merged commit f4e0153 into 5.8-main Oct 7, 2026
4 checks passed
@nan-li
nan-li deleted the nan/sdk-5314 branch October 7, 2026 16:23
nan-li added a commit that referenced this pull request Oct 7, 2026
OneSignalUserManagerImpl.swift reached 1003 lines once #1758 landed on 5.8-main, over
SwiftLint's 1000-line file_length error, so every PR against the branch failed the lint
job. The helper moves to OneSignalUserManagerImpl+Jwt.swift with the rest of the Identity
Verification surface and start() still calls it. No behavior change.
@nan-li nan-li mentioned this pull request Oct 7, 2026
9 of 18 tasks
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