Skip to content

Commit be973cf

Browse files
nan-liclaude
andauthored
fix: detect when Identity verification becomes turned off (#1674)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent bb8284c commit be973cf

12 files changed

Lines changed: 380 additions & 12 deletions

iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1320,7 +1320,12 @@ - (void)onUserStateDidChangeWithState:(OSUserChangedState * _Nonnull)state {
13201320

13211321
#pragma mark OSUserJwtConfigListener Methods
13221322
- (void)onRequiresUserAuthChangedFrom:(enum OSRequiresUserAuth)from to:(enum OSRequiresUserAuth)to {
1323-
// This callback is unused, the controller will fetch when subscription ID changes
1323+
// Identity Verification was turned off: a fetch deferred waiting for a JWT may never be
1324+
// retried via onJwtUpdated once auth is off, so release it here
1325+
if (to == OSRequiresUserAuthOff && shouldRetryGetInAppMessagesOnJwtUpdated) {
1326+
shouldRetryGetInAppMessagesOnJwtUpdated = false;
1327+
[self getInAppMessagesFromServer];
1328+
}
13241329
}
13251330

13261331
- (void)onJwtUpdatedWithExternalId:(NSString *)externalId token:(NSString *)token {

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSCustomEventsExecutor.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,35 @@ extension OSCustomEventsExecutor: OSUserJwtConfigListener {
391391
// If auth changed from false or unknown to true, drop invalid items
392392
if to == .on {
393393
removeInvalidDeltasAndRequests()
394+
} else if to == .off {
395+
// Identity Verification was turned off: release requests parked awaiting a JWT.
396+
reQueueAllPendingRequests()
394397
}
395398
}
396399

397400
func onJwtUpdated(externalId: String, token: String?) {
398401
reQueuePendingRequestsForExternalId(externalId: externalId)
399402
}
400403

404+
/// Identity Verification was turned off: move every auth-pended request, across all
405+
/// external IDs, back into the request queue and flush.
406+
private func reQueueAllPendingRequests() {
407+
self.dispatchQueue.async {
408+
guard !self.pendingAuthRequests.isEmpty else {
409+
return
410+
}
411+
for (_, requests) in self.pendingAuthRequests {
412+
for request in requests {
413+
self.requestQueue.append(request)
414+
}
415+
}
416+
self.pendingAuthRequests = [:]
417+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
418+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
419+
self.processRequestQueue(inBackground: false)
420+
}
421+
}
422+
401423
private func reQueuePendingRequestsForExternalId(externalId: String) {
402424
self.dispatchQueue.async {
403425
guard let requests = self.pendingAuthRequests[externalId] else {

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,16 +397,43 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
397397

398398
extension OSIdentityOperationExecutor: OSUserJwtConfigListener {
399399
func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) {
400-
// If auth changed from false or unknown to true, process requests
400+
// If auth changed from false or unknown to true, drop now-invalid requests
401401
if to == .on {
402402
removeInvalidDeltasAndRequests()
403+
} else if to == .off {
404+
// Identity Verification was turned off: release requests parked awaiting a JWT.
405+
reQueueAllPendingRequests()
403406
}
404407
}
405408

406409
func onJwtUpdated(externalId: String, token: String?) {
407410
reQueuePendingRequestsForExternalId(externalId: externalId)
408411
}
409412

413+
/// Identity Verification was turned off: move every auth-pended request, across all
414+
/// external IDs, back into the request queue and flush
415+
private func reQueueAllPendingRequests() {
416+
self.dispatchQueue.async {
417+
guard !self.pendingAuthRequests.isEmpty else {
418+
return
419+
}
420+
for (_, requests) in self.pendingAuthRequests {
421+
for request in requests {
422+
if let addRequest = request as? OSRequestAddAliases {
423+
self.addRequestQueue.append(addRequest)
424+
} else if let removeRequest = request as? OSRequestRemoveAlias {
425+
self.removeRequestQueue.append(removeRequest)
426+
}
427+
}
428+
}
429+
self.pendingAuthRequests = [:]
430+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
431+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
432+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
433+
self.processRequestQueue(inBackground: false)
434+
}
435+
}
436+
410437
private func reQueuePendingRequestsForExternalId(externalId: String) {
411438
self.dispatchQueue.async {
412439
guard let requests = self.pendingAuthRequests[externalId] else {

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,16 +380,38 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
380380

381381
extension OSPropertyOperationExecutor: OSUserJwtConfigListener {
382382
func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) {
383-
// If auth changed from false or unknown to true, process requests
383+
// If auth changed from false or unknown to true, drop now-invalid requests
384384
if to == .on {
385385
removeInvalidDeltasAndRequests()
386+
} else if to == .off {
387+
// Identity Verification was turned off: release requests parked awaiting a JWT.
388+
reQueueAllPendingRequests()
386389
}
387390
}
388391

389392
func onJwtUpdated(externalId: String, token: String?) {
390393
reQueuePendingRequestsForExternalId(externalId: externalId)
391394
}
392395

396+
/// Identity Verification was turned off: move every auth-pended request, across all
397+
/// external IDs, back into the update queue and flush.
398+
private func reQueueAllPendingRequests() {
399+
self.dispatchQueue.async {
400+
guard !self.pendingAuthRequests.isEmpty else {
401+
return
402+
}
403+
for (_, requests) in self.pendingAuthRequests {
404+
for request in requests {
405+
self.updateRequestQueue.append(request)
406+
}
407+
}
408+
self.pendingAuthRequests = [:]
409+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
410+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
411+
self.processRequestQueue(inBackground: false)
412+
}
413+
}
414+
393415
private func reQueuePendingRequestsForExternalId(externalId: String) {
394416
self.dispatchQueue.async {
395417
guard let requests = self.pendingAuthRequests[externalId] else {

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,13 +541,40 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener {
541541
func onRequiresUserAuthChanged(from: OneSignalOSCore.OSRequiresUserAuth, to: OneSignalOSCore.OSRequiresUserAuth) {
542542
if to == .on {
543543
removeInvalidDeltasAndRequests()
544+
} else if to == .off {
545+
// Identity Verification was turned off: release requests parked awaiting a JWT.
546+
reQueueAllPendingRequests()
544547
}
545548
}
546549

547550
func onJwtUpdated(externalId: String, token: String?) {
548551
reQueuePendingRequestsForExternalId(externalId: externalId)
549552
}
550553

554+
/// Identity Verification was turned off: move every auth-pended request, across all
555+
/// external IDs, back into the add/remove queues and flush.
556+
private func reQueueAllPendingRequests() {
557+
self.dispatchQueue.async {
558+
guard !self.pendingAuthRequests.isEmpty else {
559+
return
560+
}
561+
for (_, requests) in self.pendingAuthRequests {
562+
for request in requests {
563+
if let addRequest = request as? OSRequestCreateSubscription {
564+
self.addRequestQueue.append(addRequest)
565+
} else if let removeRequest = request as? OSRequestDeleteSubscription {
566+
self.removeRequestQueue.append(removeRequest)
567+
}
568+
}
569+
}
570+
self.pendingAuthRequests = [:]
571+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
572+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
573+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
574+
self.processRequestQueue(inBackground: false)
575+
}
576+
}
577+
551578
func handleUnauthorizedError(externalId: String, request: OSUserRequest) {
552579
if jwtConfig.isRequired ?? false {
553580
self.pendRequestUntilAuthUpdated(request, externalId: externalId)
@@ -615,15 +642,15 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener {
615642
self.addRequestQueue.remove(at: index)
616643
}
617644
}
618-
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
645+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
619646

620647
for (index, request) in self.removeRequestQueue.enumerated().reversed() {
621648
if request.identityModel.externalId == nil {
622649
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests dropped \(request)")
623650
self.removeRequestQueue.remove(at: index)
624651
}
625652
}
626-
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
653+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
627654
}
628655
}
629656
}

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -769,9 +769,12 @@ extension OSUserExecutor {
769769

770770
extension OSUserExecutor: OSUserJwtConfigListener {
771771
func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) {
772-
// If auth changed from false or unknown to true, process requests
772+
// If auth changed from false or unknown to true, drop now-invalid requests
773773
if to == .on {
774774
removeInvalidRequests()
775+
} else if to == .off {
776+
// Identity Verification is turned off: release requests parked awaiting a JWT
777+
reQueueAllPendingRequests()
775778
}
776779
self.executePendingRequests()
777780
}
@@ -780,6 +783,25 @@ extension OSUserExecutor: OSUserJwtConfigListener {
780783
reQueuePendingRequestsForExternalId(externalId: externalId)
781784
}
782785

786+
/// Identity Verification was turned off: move every auth-pended request, across all
787+
/// external IDs, back into the request queue and flush
788+
private func reQueueAllPendingRequests() {
789+
self.dispatchQueue.async {
790+
guard !self.pendingAuthRequests.isEmpty else {
791+
return
792+
}
793+
for (_, requests) in self.pendingAuthRequests {
794+
for request in requests {
795+
self.userRequestQueue.append(request)
796+
}
797+
}
798+
self.pendingAuthRequests = [:]
799+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue)
800+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
801+
self.executePendingRequests(withDelay: true)
802+
}
803+
}
804+
783805
private func reQueuePendingRequestsForExternalId(externalId: String) {
784806
self.dispatchQueue.async {
785807
guard let requests = self.pendingAuthRequests[externalId] else {

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -699,15 +699,12 @@ extension OneSignalUserManagerImpl {
699699
}
700700

701701
/**
702-
This is called when remote params does not return the property `IOS_JWT_REQUIRED`.
703-
It is likely this feature is not enabled for the app, so we will assume it is off.
704-
However, don't overwrite the value if this has already been set.
702+
This is called when remote params does not return the property `IOS_JWT_REQUIRED`. A missing value means Identity Verification
703+
is off for this app, so we set it to off unconditionally — including when it was previously on. The `requiresUserAuth` didSet only
704+
fires listeners when the value actually changes, so re-confirming an existing off value is a no-op.
705705
*/
706706
@objc
707707
public func remoteParamsReturnedUnknownRequiresUserAuth() {
708-
guard jwtConfig.isRequired == nil else {
709-
return
710-
}
711708
OneSignalLog.onesignalLog(.LL_DEBUG, message: "remoteParamsReturnedUnknownRequiresUserAuth called")
712709
jwtConfig.isRequired = false
713710
}

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,4 +278,41 @@ final class IdentityExecutorTests: XCTestCase {
278278

279279
XCTAssertEqual(removeAliasRequests.count, 3)
280280
}
281+
282+
/// When Identity Verification is turned off, alias requests parked awaiting a JWT must be
283+
/// released and sent without a token (no JWT will ever arrive once auth is off).
284+
func testReleasePendingRequests_OnIdentityVerificationTurnedOff() {
285+
/* Setup */
286+
let mocks = Mocks()
287+
mocks.setAuthRequired(true)
288+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
289+
290+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
291+
// No JWT token, so the request is parked awaiting a JWT while Identity Verification is on.
292+
// Use the user manager's executor because the JWT-config callback fires only on subscribed
293+
// executors; start() initializes and subscribes them.
294+
OneSignalUserManagerImpl.sharedInstance.start()
295+
let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor!
296+
297+
let aliases = userA_Aliases
298+
MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases)
299+
300+
let userJwtInvalidatedListener = MockUserJwtInvalidatedListener()
301+
OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener)
302+
303+
executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases))
304+
305+
/* When: the request is parked because no token is present */
306+
executor.processDeltaQueue(inBackground: false)
307+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
308+
XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
309+
310+
/* When: Identity Verification is turned off */
311+
mocks.setAuthRequired(false)
312+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
313+
314+
/* Then: the parked request is released and sent, with no JWT invalidation */
315+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self))
316+
XCTAssertFalse(userJwtInvalidatedListener.invalidatedCallbackWasCalled)
317+
}
281318
}

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/OSCustomEventsExecutorTests.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,4 +428,36 @@ final class OSCustomEventsExecutorTests: XCTestCase {
428428
// Verify user properties are still present
429429
XCTAssertEqual(payload["user_key"] as? String, "user_value")
430430
}
431+
432+
// MARK: - Identity Verification turned off
433+
434+
/// When Identity Verification is turned off, custom-event requests parked awaiting a JWT must
435+
/// be released and sent without a token (no JWT will ever arrive once auth is off).
436+
func testReleasePendingRequests_OnIdentityVerificationTurnedOff() {
437+
/* Setup */
438+
let mocks = CustomEventsMocks()
439+
// Turn Identity Verification on via the shared config, which the user manager's executor reads.
440+
OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = true
441+
442+
let user = OneSignalUserMocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
443+
// start() initializes sharedInstance.customEventsExecutor and subscribes it as a JWT listener.
444+
OneSignalUserManagerImpl.sharedInstance.start()
445+
let executor = OneSignalUserManagerImpl.sharedInstance.customEventsExecutor!
446+
447+
mocks.client.fireSuccessForAllRequests = true
448+
let delta = createCustomEventDelta(name: "iv_off_event", properties: ["key": "value"], identityModel: user.identityModel)
449+
executor.enqueueDelta(delta)
450+
451+
/* When: the request is parked because no token is present */
452+
executor.processDeltaQueue(inBackground: false)
453+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
454+
XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestCustomEvents.self))
455+
456+
/* When: Identity Verification is turned off */
457+
OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = false
458+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
459+
460+
/* Then: the parked request is released and sent */
461+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCustomEvents.self))
462+
}
431463
}

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,4 +213,41 @@ final class PropertyExecutorTests: XCTestCase {
213213
}
214214
XCTAssertEqual(updateRequests.count, 3)
215215
}
216+
217+
/// When Identity Verification is turned off, requests parked awaiting a JWT must be
218+
/// released and sent without a token (no JWT will ever arrive once auth is off).
219+
func testReleasePendingRequests_OnIdentityVerificationTurnedOff() {
220+
/* Setup */
221+
let mocks = Mocks()
222+
mocks.setAuthRequired(true)
223+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
224+
225+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
226+
// No JWT token, so the request is parked awaiting a JWT while Identity Verification is on.
227+
// Use the user manager's executor because the JWT-config callback fires only on subscribed
228+
// executors; start() initializes and subscribes them.
229+
OneSignalUserManagerImpl.sharedInstance.start()
230+
let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor!
231+
232+
let tags = ["testUserA": "true"]
233+
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
234+
235+
let userJwtInvalidatedListener = MockUserJwtInvalidatedListener()
236+
OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener)
237+
238+
executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags))
239+
240+
/* When: the request is parked because no token is present */
241+
executor.processDeltaQueue(inBackground: false)
242+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
243+
XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
244+
245+
/* When: Identity Verification is turned off */
246+
mocks.setAuthRequired(false)
247+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
248+
249+
/* Then: the parked request is released and sent, with no JWT invalidation */
250+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
251+
XCTAssertFalse(userJwtInvalidatedListener.invalidatedCallbackWasCalled)
252+
}
216253
}

0 commit comments

Comments
 (0)