Skip to content

Commit aca978e

Browse files
committed
store property update requests that need auth
When a requests fails with a 401 due to JWT or fails when preparing for execution we remove the request from the request queue and add it to the pending dictionary. Once we get the onJWTUpdated callback for that externalId we requeue the pending requests and try again. fixup property operations
1 parent f3cb3d8 commit aca978e

File tree

2 files changed

+121
-2
lines changed

2 files changed

+121
-2
lines changed

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

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ private struct OSCombinedProperties {
6363
class OSPropertyOperationExecutor: OSOperationExecutor {
6464
var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA]
6565
var deltaQueue: [OSDelta] = []
66+
var pendingAuthRequests: [String: [OSRequestUpdateProperties]] = [String:[OSRequestUpdateProperties]]()
6667
var updateRequestQueue: [OSRequestUpdateProperties] = []
6768
let newRecordsState: OSNewRecordsState
6869
let jwtConfig: OSUserJwtConfig
@@ -268,19 +269,44 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
268269
}
269270
}
270271

271-
func handleUnauthorizedError(externalId: String, error: NSError) {
272+
func handleUnauthorizedError(externalId: String, error: NSError, request: OSRequestUpdateProperties) {
272273
if (jwtConfig.isRequired ?? false) {
274+
self.pendRequestUntilAuthUpdated(request, externalId: externalId)
273275
OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error)
274276
}
275277
}
278+
279+
func pendRequestUntilAuthUpdated(_ request: OSRequestUpdateProperties, externalId: String?) {
280+
self.dispatchQueue.async {
281+
self.updateRequestQueue.removeAll(where: { $0 == request})
282+
guard let externalId = externalId else {
283+
return
284+
}
285+
var requests = self.pendingAuthRequests[externalId] ?? []
286+
let inQueue = requests.contains(where: {$0 == request})
287+
guard !inQueue else {
288+
return
289+
}
290+
requests.append(request)
291+
self.pendingAuthRequests[externalId] = requests
292+
}
293+
}
276294

277295
func executeUpdatePropertiesRequest(_ request: OSRequestUpdateProperties, inBackground: Bool) {
278296
guard !request.sentToClient else {
279297
return
280298
}
299+
300+
guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else {
301+
pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId)
302+
return
303+
}
304+
281305
guard request.prepareForExecution(newRecordsState: newRecordsState) else {
282306
return
283307
}
308+
309+
print("ECM executing properties request: %@", request.identityModel.externalId)
284310
request.sentToClient = true
285311

286312
let backgroundTaskIdentifier = PROPERTIES_EXECUTOR_BACKGROUND_TASK + UUID().uuidString
@@ -319,7 +345,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor {
319345
OneSignalUserManagerImpl.sharedInstance._logout()
320346
} else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) {
321347
if let externalId = request.identityModel.externalId {
322-
self.handleUnauthorizedError(externalId: externalId, error: nsError)
348+
self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request)
323349
}
324350
request.sentToClient = false
325351
} else if responseType != .retryable {
@@ -347,6 +373,21 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener {
347373

348374
func onJwtUpdated(externalId: String, token: String?) {
349375
print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))")
376+
reQueuePendingRequestsForExternalId(externalId: externalId)
377+
}
378+
379+
private func reQueuePendingRequestsForExternalId(externalId: String) {
380+
self.dispatchQueue.async {
381+
guard let requests = self.pendingAuthRequests[externalId] else {
382+
return
383+
}
384+
for request in requests {
385+
self.updateRequestQueue.append(request)
386+
}
387+
self.pendingAuthRequests[externalId] = nil
388+
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue)
389+
self.processRequestQueue(inBackground: false)
390+
}
350391
}
351392

352393
private func removeInvalidDeltasAndRequests() {

iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,82 @@ final class PropertyExecutorTests: XCTestCase {
141141
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
142142
XCTAssertTrue(invalidatedCallbackWasCalled)
143143
}
144+
145+
func testUpdateRequests_Retry_OnTokenUpdate() {
146+
147+
/* Setup */
148+
let mocks = Mocks()
149+
mocks.setAuthRequired(true)
150+
OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true
151+
152+
let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
153+
user.identityModel.jwtBearerToken = userA_InvalidJwtToken
154+
155+
// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
156+
let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor!
157+
158+
let tags = ["testUserA" : "true"]
159+
MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags)
160+
executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
161+
162+
var invalidatedCallbackWasCalled = false
163+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
164+
invalidatedCallbackWasCalled = true
165+
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
166+
OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken)
167+
}
168+
169+
/* When */
170+
executor.processDeltaQueue(inBackground: false)
171+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
172+
173+
/* Then */
174+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
175+
XCTAssertTrue(invalidatedCallbackWasCalled)
176+
XCTAssertEqual(mocks.client.networkRequestCount, 2)
177+
}
178+
179+
func testUpdateRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() {
180+
/* Setup */
181+
let mocks = Mocks()
182+
183+
mocks.setAuthRequired(true)
184+
185+
let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID)
186+
userA.identityModel.jwtBearerToken = userA_InvalidJwtToken
187+
188+
let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID)
189+
userB.identityModel.jwtBearerToken = userA_InvalidJwtToken
190+
// We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor
191+
let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor!
192+
193+
let tags = ["testUserA" : "true"]
194+
MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags)
195+
196+
executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userA.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
197+
executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userB.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags))
198+
199+
var invalidatedCallbackWasCalled = false
200+
OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in
201+
invalidatedCallbackWasCalled = true
202+
}
203+
204+
/* When */
205+
executor.processDeltaQueue(inBackground: false)
206+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
207+
208+
MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags)
209+
OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken)
210+
211+
OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5)
212+
213+
/* Then */
214+
// The executor should execute this request since identity verification is required and the token was set
215+
XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self))
216+
XCTAssertTrue(invalidatedCallbackWasCalled)
217+
let updateRequests = mocks.client.executedRequests.filter { request in
218+
request.isKind(of: OSRequestUpdateProperties.self)
219+
}
220+
XCTAssertEqual(updateRequests.count, 3)
221+
}
144222
}

0 commit comments

Comments
 (0)