Skip to content

Commit 483a92a

Browse files
nan-liclaude
andcommitted
fix(custom events): full JWT integration for OSCustomEventsExecutor
Mirrors the JWT-aware pattern of OSPropertyOperationExecutor so custom events behave consistently with the other user-scoped executors: - Executor init now takes jwtConfig: OSUserJwtConfig and subscribes as an OSUserJwtConfigListener. - uncacheDeltas / uncacheRequests drop entries whose identity model has no externalId when JWT is required, and load pendingAuthRequests from the new persistence key. - processDeltaQueue is gated on jwtConfig.isRequired being known, and drops in-flight deltas for JWT-required users without externalId. - executeRequest checks addJWTHeaderIsValid; on failure it pends the request via pendRequestUntilAuthUpdated. 401 responses trigger handleUnauthorizedError, which invalidates the JWT and pends the request for retry when the JWT updates. - onRequiresUserAuthChanged drops invalid in-flight deltas/requests when auth turns on; onJwtUpdated re-queues this externalId's pending requests. Request-level: OSRequestCustomEvents.prepareForExecution no longer checks JWT inline (now handled by the executor before prepareForExecution), matching the OSRequestUpdateProperties shape. Renamed the call from the non-existent addPushSubscriptionIdToAdditionalHeaders to the branch's addPushSubscriptionToAdditionalHeaders. Adds OS_CUSTOM_EVENTS_EXECUTOR and OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY constants. Updates OneSignalUserManagerImpl to pass jwtConfig at init, and the test mock to do likewise. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3dac9fe commit 483a92a

5 files changed

Lines changed: 176 additions & 35 deletions

File tree

iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,10 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
385385
#define OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY"
386386

387387
// Custom Events Executor
388+
#define OS_CUSTOM_EVENTS_EXECUTOR @"OS_CUSTOM_EVENTS_EXECUTOR"
388389
#define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY"
389390
#define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY"
391+
#define OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY"
390392

391393
// Live Activies Executor
392394
#define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY"

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

Lines changed: 164 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,19 @@ class OSCustomEventsExecutor: OSOperationExecutor {
4646
}
4747

4848
var supportedDeltas: [String] = [OS_CUSTOM_EVENT_DELTA]
49-
private var deltaQueue: [OSDelta] = []
50-
private var requestQueue: [OSRequestCustomEvents] = []
51-
private let newRecordsState: OSNewRecordsState
49+
var deltaQueue: [OSDelta] = []
50+
var requestQueue: [OSRequestCustomEvents] = []
51+
var pendingAuthRequests: [String: [OSRequestCustomEvents]] = [String: [OSRequestCustomEvents]]()
52+
let newRecordsState: OSNewRecordsState
53+
let jwtConfig: OSUserJwtConfig
5254

5355
// The executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `requestQueue`.
5456
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSCustomEventsExecutor", target: .global())
5557

56-
init(newRecordsState: OSNewRecordsState) {
58+
init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) {
5759
self.newRecordsState = newRecordsState
60+
self.jwtConfig = jwtConfig
61+
self.jwtConfig.subscribe(self, key: OS_CUSTOM_EVENTS_EXECUTOR)
5862
// Read unfinished deltas and requests from cache, if any...
5963
uncacheDeltas()
6064
uncacheRequests()
@@ -63,10 +67,17 @@ class OSCustomEventsExecutor: OSOperationExecutor {
6367
private func uncacheDeltas() {
6468
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
6569
for (index, delta) in deltaQueue.enumerated().reversed() {
66-
if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil {
70+
guard let model = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else {
6771
// The identity model does not exist, drop this Delta
6872
OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(delta)")
6973
deltaQueue.remove(at: index)
74+
continue
75+
}
76+
77+
// If JWT is on but the external ID does not exist, drop this Delta
78+
if jwtConfig.isRequired == true, model.externalId == nil {
79+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSCustomEventsExecutor.uncacheDeltas dropped \(delta)")
80+
deltaQueue.remove(at: index)
7081
}
7182
}
7283
self.deltaQueue = deltaQueue
@@ -79,27 +90,44 @@ class OSCustomEventsExecutor: OSOperationExecutor {
7990
}
8091

8192
private func uncacheRequests() {
82-
if var requestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCustomEvents] {
83-
// Hook each uncached Request to the model in the store
84-
for (index, request) in requestQueue.enumerated().reversed() {
85-
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
86-
// 1. The identity model exist in the repo, set it to be the Request's model
87-
request.identityModel = identityModel
88-
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
89-
// 2. The request can be sent, add the model to the repo
90-
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
91-
} else {
92-
// 3. The identitymodel do not exist AND this request cannot be sent, drop this Request
93-
OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(request)")
94-
requestQueue.remove(at: index)
93+
var requestQueue: [OSRequestCustomEvents] = []
94+
95+
if let cachedQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCustomEvents] {
96+
requestQueue = cachedQueue
97+
}
98+
99+
if let pendingRequests = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY, defaultValue: [:]) as? [String: [OSRequestCustomEvents]] {
100+
for requests in pendingRequests.values {
101+
for request in requests {
102+
requestQueue.append(request)
95103
}
96104
}
97-
self.requestQueue = requestQueue
98-
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
99-
} else {
100-
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor error encountered reading from cache for \(OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY)")
101-
self.requestQueue = []
102105
}
106+
107+
// Hook each uncached Request to the model in the store
108+
for (index, request) in requestQueue.enumerated().reversed() {
109+
if jwtConfig.isRequired == true,
110+
request.identityModel.externalId == nil
111+
{
112+
// remove if jwt is on but the model does not have external ID
113+
requestQueue.remove(at: index)
114+
continue
115+
}
116+
117+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) {
118+
// 1. The identity model exist in the repo, set it to be the Request's model
119+
request.identityModel = identityModel
120+
} else if request.prepareForExecution(newRecordsState: newRecordsState) {
121+
// 2. The request can be sent, add the model to the repo
122+
OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel)
123+
} else {
124+
// 3. The identitymodel do not exist AND this request cannot be sent, drop this Request
125+
OneSignalLog.onesignalLog(.LL_WARN, message: "OSCustomEventsExecutor.init dropped: \(request)")
126+
requestQueue.remove(at: index)
127+
}
128+
}
129+
self.requestQueue = requestQueue
130+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
103131
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSCustomEventsExecutor successfully uncached Requests: \(requestQueue)")
104132
}
105133

@@ -119,6 +147,11 @@ class OSCustomEventsExecutor: OSOperationExecutor {
119147
/// The `deltaQueue` can contain events for multiple users. They will remain as Deltas if there is no onesignal ID yet for its user.
120148
/// This method will be used in an upcoming release that combine multiple events.
121149
func processDeltaQueueWithBatching(inBackground: Bool) {
150+
guard jwtConfig.isRequired != nil else {
151+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSCustomEventsExecutor processDeltaQueueWithBatching returning early due to requiresAuth: \(String(describing: jwtConfig.isRequired))")
152+
return
153+
}
154+
122155
self.dispatchQueue.async {
123156
if self.deltaQueue.isEmpty {
124157
// Delta queue is empty but there may be pending requests
@@ -140,6 +173,13 @@ class OSCustomEventsExecutor: OSOperationExecutor {
140173
continue
141174
}
142175

176+
// If JWT is on but the external ID does not exist, drop this Delta
177+
if self.jwtConfig.isRequired == true, identityModel.externalId == nil {
178+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSCustomEventsExecutor.processDeltaQueue dropped \(delta)")
179+
self.deltaQueue.remove(at: index)
180+
continue
181+
}
182+
143183
guard let properties = delta.value as? [String: Any] else {
144184
// This should not happen as there are preventative typing measures before this step
145185
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor.processDeltaQueue dropped due to invalid properties: \(delta)")
@@ -181,6 +221,11 @@ class OSCustomEventsExecutor: OSOperationExecutor {
181221
}
182222

183223
func processDeltaQueue(inBackground: Bool) {
224+
guard jwtConfig.isRequired != nil else {
225+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSCustomEventsExecutor processDeltaQueue returning early due to requiresAuth: \(String(describing: jwtConfig.isRequired))")
226+
return
227+
}
228+
184229
self.dispatchQueue.async {
185230
if self.deltaQueue.isEmpty {
186231
// Delta queue is empty but there may be pending requests
@@ -198,6 +243,13 @@ class OSCustomEventsExecutor: OSOperationExecutor {
198243
continue
199244
}
200245

246+
// If JWT is on but the external ID does not exist, drop this Delta
247+
if self.jwtConfig.isRequired == true, identityModel.externalId == nil {
248+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSCustomEventsExecutor.processDeltaQueue dropped \(delta)")
249+
self.deltaQueue.remove(at: index)
250+
continue
251+
}
252+
201253
guard let properties = delta.value as? [String: Any] else {
202254
// This should not happen as there are preventative typing measures before this step
203255
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor.processDeltaQueue dropped due to invalid properties: \(delta)")
@@ -258,10 +310,41 @@ class OSCustomEventsExecutor: OSOperationExecutor {
258310
}
259311
}
260312

261-
private func executeRequest(_ request: OSRequestCustomEvents, inBackground: Bool) {
313+
func handleUnauthorizedError(externalId: String, request: OSRequestCustomEvents) {
314+
if jwtConfig.isRequired ?? false {
315+
self.pendRequestUntilAuthUpdated(request, externalId: externalId)
316+
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId)
317+
}
318+
}
319+
320+
func pendRequestUntilAuthUpdated(_ request: OSRequestCustomEvents, externalId: String?) {
321+
self.dispatchQueue.async {
322+
self.requestQueue.removeAll(where: { $0 == request})
323+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
324+
guard let externalId = externalId else {
325+
return
326+
}
327+
var requests = self.pendingAuthRequests[externalId] ?? []
328+
let inQueue = requests.contains(where: {$0 == request})
329+
guard !inQueue else {
330+
return
331+
}
332+
requests.append(request)
333+
self.pendingAuthRequests[externalId] = requests
334+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
335+
}
336+
}
337+
338+
func executeRequest(_ request: OSRequestCustomEvents, inBackground: Bool) {
262339
guard !request.sentToClient else {
263340
return
264341
}
342+
343+
guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
344+
pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId)
345+
return
346+
}
347+
265348
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
266349
return
267350
}
@@ -284,7 +367,12 @@ class OSCustomEventsExecutor: OSOperationExecutor {
284367
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSCustomEventsExecutor request failed with error: \(error.debugDescription)")
285368
self.dispatchQueue.async {
286369
let responseType = OSNetworkingUtils.getResponseStatusType(error.code)
287-
if responseType != .retryable {
370+
if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
371+
if let externalId = request.identityModel.externalId {
372+
self.handleUnauthorizedError(externalId: externalId, request: request)
373+
}
374+
request.sentToClient = false
375+
} else if responseType != .retryable {
288376
// Fail, no retry, remove from cache and queue
289377
self.requestQueue.removeAll(where: { $0 == request})
290378
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
@@ -298,13 +386,64 @@ class OSCustomEventsExecutor: OSOperationExecutor {
298386
}
299387
}
300388

389+
extension OSCustomEventsExecutor: OSUserJwtConfigListener {
390+
func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) {
391+
// If auth changed from false or unknown to true, drop invalid items
392+
if to == .on {
393+
removeInvalidDeltasAndRequests()
394+
}
395+
}
396+
397+
func onJwtUpdated(externalId: String, token: String?) {
398+
reQueuePendingRequestsForExternalId(externalId: externalId)
399+
}
400+
401+
private func reQueuePendingRequestsForExternalId(externalId: String) {
402+
self.dispatchQueue.async {
403+
guard let requests = self.pendingAuthRequests[externalId] else {
404+
return
405+
}
406+
for request in requests {
407+
self.requestQueue.append(request)
408+
}
409+
self.pendingAuthRequests[externalId] = nil
410+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
411+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests)
412+
self.processRequestQueue(inBackground: false)
413+
}
414+
}
415+
416+
private func removeInvalidDeltasAndRequests() {
417+
self.dispatchQueue.async {
418+
for (index, delta) in self.deltaQueue.enumerated().reversed() {
419+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId),
420+
identityModel.externalId == nil
421+
{
422+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSCustomEventsExecutor.removeInvalidDeltasAndRequests dropped \(delta)")
423+
self.deltaQueue.remove(at: index)
424+
}
425+
}
426+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
427+
428+
for (index, request) in self.requestQueue.enumerated().reversed() {
429+
if request.identityModel.externalId == nil {
430+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSCustomEventsExecutor.removeInvalidDeltasAndRequests dropped \(request)")
431+
self.requestQueue.remove(at: index)
432+
}
433+
}
434+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY, withValue: self.requestQueue)
435+
}
436+
}
437+
}
438+
301439
extension OSCustomEventsExecutor: OSLoggable {
302440
func logSelf() {
303441
OneSignalLog.onesignalLog(.LL_VERBOSE, message:
304442
"""
305443
OSCustomEventsExecutor has the following queues:
306444
requestQueue: \(self.requestQueue)
307445
deltaQueue: \(self.deltaQueue)
446+
pendingAuthRequests: \(self.pendingAuthRequests)
308447
309448
"""
310449
)

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
193193
let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
194194
let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
195195
let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
196-
let customEventsExecutor = OSCustomEventsExecutor(newRecordsState: newRecordsState)
196+
let customEventsExecutor = OSCustomEventsExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig)
197197
self.propertyExecutor = propertyExecutor
198198
self.identityExecutor = identityExecutor
199199
self.subscriptionExecutor = subscriptionExecutor

iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCustomEvents.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@ class OSRequestCustomEvents: OneSignalRequest, OSUserRequest {
3838
var identityModel: OSIdentityModel
3939

4040
func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool {
41-
if let onesignalId = identityModel.onesignalId,
42-
newRecordsState.canAccess(onesignalId),
43-
let appId = OneSignalConfigManager.getAppId()
44-
{
45-
_ = self.addPushSubscriptionIdToAdditionalHeaders()
46-
self.path = "apps/\(appId)/custom_events"
47-
return true
48-
} else {
41+
guard let onesignalId = identityModel.onesignalId,
42+
newRecordsState.canAccess(onesignalId),
43+
let appId = OneSignalConfigManager.getAppId()
44+
else {
4945
return false
5046
}
47+
48+
_ = self.addPushSubscriptionToAdditionalHeaders()
49+
self.path = "apps/\(appId)/custom_events"
50+
return true
5151
}
5252

5353
init(events: [[String: Any]], identityModel: OSIdentityModel) {

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/OSCustomEventsExecutorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ private class CustomEventsMocks {
4040

4141
init() {
4242
OneSignalCoreImpl.setSharedClient(client)
43-
customEventsExecutor = OSCustomEventsExecutor(newRecordsState: newRecordsState)
43+
customEventsExecutor = OSCustomEventsExecutor(newRecordsState: newRecordsState, jwtConfig: OSUserJwtConfig())
4444
}
4545
}
4646

0 commit comments

Comments
 (0)