fix: detect when Identity verification becomes turned off#1674
Conversation
|
@claude review |
b000fdc to
aef1699
Compare
|
@claude review |
| - (void)onRequiresUserAuthChangedFrom:(enum OSRequiresUserAuth)from to:(enum OSRequiresUserAuth)to { | ||
| // This callback is unused, the controller will fetch when subscription ID changes | ||
| // Identity Verification was turned off: a fetch deferred waiting for a JWT may never be | ||
| // retried via onJwtUpdated once auth is off, so release it here | ||
| if (to == OSRequiresUserAuthOff && shouldRetryGetInAppMessagesOnJwtUpdated) { | ||
| shouldRetryGetInAppMessagesOnJwtUpdated = false; | ||
| [self getInAppMessagesFromServer]; | ||
| } |
There was a problem hiding this comment.
🔴 The new IV-off handler in onRequiresUserAuthChangedFrom:to: only retries the IAM fetch when shouldRetryGetInAppMessagesOnJwtUpdated is set, but getInAppMessagesFromServer also defers via shouldRetryGetInAppMessagesOnUserChange when getAliasForCurrentUser returns nil — e.g. an anon user with no external_id while IV is on, or the unknown-IV boot window. Once IV flips off the alias becomes resolvable from onesignal_id, but no user-state observer fires (it is guarded by ID equality), so the deferred fetch is stranded until the next session. Consider draining shouldRetryGetInAppMessagesOnUserChange in the same handler for symmetry.
Extended reasoning...
The gap
OSMessagingController.getInAppMessagesFromServer has two distinct deferral flags:
shouldRetryGetInAppMessagesOnJwtUpdated— set when the user header is missing (no JWT) or on a 401 response.shouldRetryGetInAppMessagesOnUserChange— set whengetAliasForCurrentUserreturns nil, or whenonesignalIdis nil.
The PR's new handler at OSMessagingController.m:1322-1328 only consumes the JWT flag. The user-change flag is silently ignored on the IV-off transition, even though that transition itself can be the event that makes the previously-blocked alias resolvable.
How the user-change flag gets set under IV-on
OSUserUtils.getAlias is the source of the alias used by the IAM fetch. Its logic:
guard let jwtRequired = jwtConfig.isRequired else { return nil }
if jwtRequired, let externalId = identityModel.externalId {
return OSAliasPair(OS_EXTERNAL_ID, externalId)
} else if !jwtRequired, let onesignalId = identityModel.onesignalId {
return OSAliasPair(OS_ONESIGNAL_ID, onesignalId)
}
return nilSo under IV-on, an anon user (no external_id) yields a nil alias — the fetch is blocked at the alias check and the user-change flag is set. Likewise during the unknown-IV boot window (isRequired == nil), getAlias returns nil regardless of which IDs are populated, so any IAM fetch that runs before remote params resolve also lands in the user-change branch.
Why the user observer won't recover this
fireUserStateChanged in OSUserUtils.swift is guarded by guard prevOnesignalId != newOnesignalId || prevExternalId != newExternalId. It is only invoked from OSIdentityModel.hydrateModel (id changes) and from _logout under IV-on. The IV-off transition by itself does not change onesignal_id or external_id for a cached user, so the snapshot-equality guard short-circuits and the user observer never re-fires. Result: the deferred fetch stays stranded until the next cold start (or some unrelated identity change).
Step-by-step proof
- App launches with a cached anon user (has
onesignal_id, noexternal_id) and IV currently on. - Push subscription becomes valid →
onPushSubscriptionDidChangeWithStatecallsgetInAppMessagesFromServer. getAliasForCurrentUserreturns nil under IV-on (anon, no external_id) → line 295 setsshouldRetryGetInAppMessagesOnUserChange = trueand returns.- Remote params come back without
jwt_required→remoteParamsReturnedUnknownRequiresUserAuth(post-PR) setsjwtConfig.isRequired = falseunconditionally → firesonRequiresUserAuthChanged(on, off)to all subscribers includingOSMessagingController. - The new handler checks
shouldRetryGetInAppMessagesOnJwtUpdated— it is false — and returns without retrying. - No user-state observer fires (
onesignal_id/external_idunchanged). IAMs remain unfetched until the next cold start.
If step 5 had also released the user-change flag, the very next call to getInAppMessagesFromServer would succeed: the !jwtRequired branch of getAlias now resolves OS_ONESIGNAL_ID, the fetch executes and IAMs arrive in the current session.
Impact
In-app messages don't display for the current session for the affected users (anon-with-cached-onesignalId, or any user where the IAM fetch fired during the unknown-IV boot window). The PR's explicit motivation is IV-off recovery, and the handler's own comment — "a fetch deferred waiting for a JWT may never be retried via onJwtUpdated once auth is off, so release it here" — applies symmetrically to a fetch deferred waiting for a user-state change.
Fix
Add a parallel check in the same handler:
if (to == OSRequiresUserAuthOff) {
if (shouldRetryGetInAppMessagesOnJwtUpdated || shouldRetryGetInAppMessagesOnUserChange) {
shouldRetryGetInAppMessagesOnJwtUpdated = false;
shouldRetryGetInAppMessagesOnUserChange = false;
[self getInAppMessagesFromServer];
}
}4d9f72f to
7699bbd
Compare
A missing jwt_required field previously set the flag off only when it was still unknown, so an app that had Identity Verification on stayed on after it was disabled remotely. Treat a missing field as off unconditionally so the on->off transition is actually detected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d off While auth is required, requests without a valid JWT are parked per external ID awaiting a token. Once Identity Verification is turned off no token will arrive, so each JWT listener (User, Identity, Subscription, Property, and CustomEvents executors, plus the IAM controller) now releases the parked work and flushes it on the off transition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
removeInvalidDeltasAndRequests saved updateRequestQueue under both the add and remove request queue keys, corrupting those caches when Identity Verification is enabled. Persist the matching addRequestQueue and removeRequestQueue instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add per-executor tests asserting parked requests are released and sent without auth once Identity Verification is turned off, plus OSUserJwtConfig tests verifying a missing jwt_required remote param resolves to off (including the previously-on case) and that an unchanged value does not re-fire listeners. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
aef1699 to
d4d113e
Compare
Description
One Line Summary
Detect when Identity Verification is turned off and release any requests that were waiting on a JWT.
Details
Motivation
The SDK detected Identity Verification (IV) turning on but never handled it turning off. Two gaps:
jwt_requiredfield in remote params only set IV off when the value was previously unknown, so an app that had IV on stayed on after it was disabled remotely — the on→off transition was never even produced.OSUserJwtConfigListeneracted on the off transition, so requests parked per-external-id awaiting a JWT (each executor'spendingAuthRequests) were stranded forever — once IV is off, no JWT will ever arrive to release them.Scope
jwt_requiredremote param now always resolves IV to off (including the previously-on case).OSOperationRepo(deltas still flush on the normal poll cadence).removeInvalidDeltasAndRequestspersistedupdateRequestQueueunder the add/remove subscription queue keys.Testing
Unit testing
Added per-executor tests asserting parked requests are released and sent without auth on the off transition, plus
OSUserJwtConfigtests verifying a missingjwt_requiredresolves to off (including previously-on) and that an unchanged value does not re-fire listeners. All pass inOneSignalUserTests.Manual testing
Not tested on a physical device; behavior is covered by the unit tests above.
Affected code checklist
Checklist
Overview
Testing
Final pass