Skip to content

Commit 791b8b4

Browse files
committed
Fixes
1 parent 838c794 commit 791b8b4

File tree

9 files changed

+102
-14
lines changed

9 files changed

+102
-14
lines changed

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@
187187
8AAA8BC32D07310600DF8220 /* IterableInboxViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8BA52D07310600DF8220 /* IterableInboxViewController.swift */; };
188188
8AAA8BC42D07310600DF8220 /* IterableAppIntegration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B992D07310600DF8220 /* IterableAppIntegration.swift */; };
189189
8AAA8BCD2D07310600DF8220 /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B252D07310600DF8220 /* RetryPolicy.swift */; };
190+
9313470310D6B94EBF01743D /* AuthTokenValidityState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D3BD1417B5702B16498AFF /* AuthTokenValidityState.swift */; };
190191
8AAA8BD32D07310600DF8220 /* IterableEmbeddedManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B282D07310600DF8220 /* IterableEmbeddedManagerProtocol.swift */; };
191192
8AAA8BD52D07310600DF8220 /* AuthFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B2D2D07310600DF8220 /* AuthFailure.swift */; };
192193
8AAA8BD62D07310600DF8220 /* AuthFailureReason.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8AAA8B2E2D07310600DF8220 /* AuthFailureReason.swift */; };
@@ -644,6 +645,7 @@
644645
8AAA8B232D07310600DF8220 /* IterableInAppMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableInAppMessage.swift; sourceTree = "<group>"; };
645646
8AAA8B242D07310600DF8220 /* IterablePushNotificationMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterablePushNotificationMetadata.swift; sourceTree = "<group>"; };
646647
8AAA8B252D07310600DF8220 /* RetryPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryPolicy.swift; sourceTree = "<group>"; };
648+
54D3BD1417B5702B16498AFF /* AuthTokenValidityState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTokenValidityState.swift; sourceTree = "<group>"; };
647649
8AAA8B272D07310600DF8220 /* IterableAuthManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableAuthManagerProtocol.swift; sourceTree = "<group>"; };
648650
8AAA8B282D07310600DF8220 /* IterableEmbeddedManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableEmbeddedManagerProtocol.swift; sourceTree = "<group>"; };
649651
8AAA8B292D07310600DF8220 /* IterableEmbeddedUpdateDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableEmbeddedUpdateDelegate.swift; sourceTree = "<group>"; };
@@ -1049,6 +1051,7 @@
10491051
8AAA8B222D07310600DF8220 /* IterableEmbeddedMessage.swift */,
10501052
8AAA8B232D07310600DF8220 /* IterableInAppMessage.swift */,
10511053
8AAA8B242D07310600DF8220 /* IterablePushNotificationMetadata.swift */,
1054+
54D3BD1417B5702B16498AFF /* AuthTokenValidityState.swift */,
10521055
8AAA8B252D07310600DF8220 /* RetryPolicy.swift */,
10531056
);
10541057
path = Models;
@@ -2212,6 +2215,7 @@
22122215
8AAA8BC32D07310600DF8220 /* IterableInboxViewController.swift in Sources */,
22132216
8AAA8BC42D07310600DF8220 /* IterableAppIntegration.swift in Sources */,
22142217
8AAA8BCD2D07310600DF8220 /* RetryPolicy.swift in Sources */,
2218+
9313470310D6B94EBF01743D /* AuthTokenValidityState.swift in Sources */,
22152219
8AAA8BD32D07310600DF8220 /* IterableEmbeddedManagerProtocol.swift in Sources */,
22162220
8AAA8BD52D07310600DF8220 /* AuthFailure.swift in Sources */,
22172221
8AAA8BD62D07310600DF8220 /* AuthFailureReason.swift in Sources */,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//
2+
// Copyright © 2025 Iterable. All rights reserved.
3+
//
4+
5+
import Foundation
6+
7+
@objc public enum AuthTokenValidityState: Int {
8+
case unknown = 0
9+
case valid = 1
10+
case invalid = 2
11+
}

swift-sdk/Core/Protocols/IterableAuthManagerProtocol.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ import Foundation
1414
func handleAuthFailure(failedAuthToken: String?, reason: AuthFailureReason)
1515
func pauseAuthRetries(_ pauseAuthRetry: Bool)
1616
func setIsLastAuthTokenValid(_ isValid: Bool)
17+
func getLastAuthTokenState() -> AuthTokenValidityState
1718
func getNextRetryInterval() -> Double
1819
}

swift-sdk/Internal/AuthManager.swift

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class AuthManager: IterableAuthManagerProtocol {
7272
}
7373

7474
private func shouldUseLastValidToken(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
75-
return isLastAuthTokenValid && !shouldIgnoreRetryPolicy
75+
return lastAuthTokenState == .valid && !shouldIgnoreRetryPolicy
7676
}
7777

7878
func setNewToken(_ newToken: String) {
@@ -97,7 +97,7 @@ class AuthManager: IterableAuthManagerProtocol {
9797
localStorage.unknownUserUpdate = nil
9898
}
9999

100-
isLastAuthTokenValid = false
100+
lastAuthTokenState = .unknown
101101
}
102102

103103
// MARK: - Private/Internal
@@ -110,7 +110,7 @@ class AuthManager: IterableAuthManagerProtocol {
110110

111111
private var authRetryPolicy: RetryPolicy
112112
private var retryCount: Int = 0
113-
private var isLastAuthTokenValid: Bool = false
113+
private var lastAuthTokenState: AuthTokenValidityState = .unknown
114114
private var pauseAuthRetry: Bool = false
115115
private var isTimerScheduled: Bool = false
116116

@@ -128,11 +128,22 @@ class AuthManager: IterableAuthManagerProtocol {
128128
}
129129

130130
func setIsLastAuthTokenValid(_ isValid: Bool) {
131-
isLastAuthTokenValid = isValid
132131
if isValid {
132+
lastAuthTokenState = .valid
133133
NotificationCenter.default.post(name: .iterableAuthTokenRefreshed, object: nil)
134+
} else {
135+
// Only transition to .invalid from .valid.
136+
// When state is .unknown (token just refreshed, awaiting validation),
137+
// keep it as .unknown — the next successful request will set .valid.
138+
if lastAuthTokenState == .valid {
139+
lastAuthTokenState = .invalid
140+
}
134141
}
135142
}
143+
144+
func getLastAuthTokenState() -> AuthTokenValidityState {
145+
return lastAuthTokenState
146+
}
136147

137148
func getNextRetryInterval() -> Double {
138149
var nextRetryInterval = Double(authRetryPolicy.retryInterval)
@@ -161,14 +172,19 @@ class AuthManager: IterableAuthManagerProtocol {
161172

162173
private func onAuthTokenReceived(retrievedAuthToken: String?, onSuccess: AuthTokenRetrievalHandler? = nil) {
163174
ITBInfo()
164-
175+
165176
pendingAuth = false
166-
177+
167178
// Set the new token first
168179
authToken = retrievedAuthToken
169180
storeAuthToken()
170-
181+
171182
if retrievedAuthToken != nil {
183+
// Only transition to .unknown from .invalid (auth recovery).
184+
// When state is .valid (normal scheduled refresh), keep it .valid.
185+
if lastAuthTokenState == .invalid {
186+
lastAuthTokenState = .unknown
187+
}
172188
let isRefreshQueued = queueAuthTokenExpirationRefresh(retrievedAuthToken, onSuccess: onSuccess)
173189
if !isRefreshQueued {
174190
onSuccess?(authToken)

swift-sdk/Internal/InternalIterableAPI.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
3737
authManager.getAuthToken()
3838
}
3939
}
40+
41+
var lastAuthTokenState: AuthTokenValidityState {
42+
get {
43+
authManager.getLastAuthTokenState()
44+
}
45+
}
4046

4147
var deviceId: String {
4248
if let value = localStorage.deviceId {

swift-sdk/Internal/api-client/Request/OfflineRequestProcessor.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ struct OfflineRequestProcessor: RequestProcessorProtocol {
2222
self.deviceMetadata = deviceMetadata
2323
self.taskScheduler = taskScheduler
2424
self.taskRunner = taskRunner
25-
notificationListener = NotificationListener(notificationCenter: notificationCenter)
25+
notificationListener = NotificationListener(notificationCenter: notificationCenter,
26+
authManager: authManager)
2627
}
2728

2829
var autoRetry: Bool {
@@ -423,16 +424,21 @@ struct OfflineRequestProcessor: RequestProcessorProtocol {
423424
}
424425

425426
private class NotificationListener: NSObject {
426-
init(notificationCenter: NotificationCenterProtocol) {
427+
init(notificationCenter: NotificationCenterProtocol,
428+
authManager: IterableAuthManagerProtocol? = nil) {
427429
ITBInfo("OfflineRequestProcessor.NotificationListener.init()")
428430
self.notificationCenter = notificationCenter
431+
self.authManager = authManager
429432
super.init()
430433
self.notificationCenter.addObserver(self,
431434
selector: #selector(onTaskFinishedWithSuccess(notification:)),
432435
name: .iterableTaskFinishedWithSuccess, object: nil)
433436
self.notificationCenter.addObserver(self,
434437
selector: #selector(onTaskFinishedWithNoRetry(notification:)),
435438
name: .iterableTaskFinishedWithNoRetry, object: nil)
439+
self.notificationCenter.addObserver(self,
440+
selector: #selector(onTaskFinishedWithRetry(notification:)),
441+
name: .iterableTaskFinishedWithRetry, object: nil)
436442
}
437443

438444
deinit {
@@ -464,6 +470,18 @@ struct OfflineRequestProcessor: RequestProcessorProtocol {
464470
ITBError("Could not find taskId for notification")
465471
}
466472
}
473+
474+
@objc
475+
private func onTaskFinishedWithRetry(notification: Notification) {
476+
ITBInfo()
477+
if let taskSendRequestError = IterableNotificationUtil.notificationToTaskSendRequestError(notification) {
478+
let error = taskSendRequestError.sendRequestError
479+
if error.httpStatusCode == 401, RequestProcessorUtil.matchesJWTErrorCode(error.iterableCode) {
480+
ITBInfo("JWT auth failure in offline task, invalidating auth token state")
481+
authManager?.setIsLastAuthTokenValid(false)
482+
}
483+
}
484+
}
467485

468486
private func addPendingTask(taskId: String) -> Pending<SendRequestValue, SendRequestError> {
469487
let result = Fulfill<SendRequestValue, SendRequestError>()
@@ -501,6 +519,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol {
501519
}
502520

503521
private let notificationCenter: NotificationCenterProtocol
522+
private weak var authManager: IterableAuthManagerProtocol?
504523
private var pendingTasksMap = [String: Fulfill<SendRequestValue, SendRequestError>]()
505524
private var pendingTasksQueue = DispatchQueue(label: "pendingTasks")
506525
}

swift-sdk/SDK/IterableAPI.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ import UIKit
3333
implementation?.authToken
3434
}
3535
}
36+
37+
/// The current validity state of the auth token as determined by the SDK
38+
public static var lastAuthTokenState: AuthTokenValidityState {
39+
get {
40+
implementation?.lastAuthTokenState ?? .unknown
41+
}
42+
}
3643

3744
/// The `userInfo` dictionary which came with last push
3845
public static var lastPushPayload: [AnyHashable: Any]? {

tests/business-critical-integration/integration-test-app/IterableSDK-Integration-Tester/App/OfflineRetry/OfflineRetryTestViewController.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -717,12 +717,32 @@ final class OfflineRetryTestViewController: UIViewController {
717717
token = IterableAPI.authToken
718718
}
719719
let remaining = JwtHelper.remainingLabel(token: token)
720-
if remaining == "Expired" || remaining == "No Token" {
721-
authStatusLabel.text = remaining
720+
721+
// Use SDK's auth validity state instead of just JWT expiry
722+
let sdkState = IterableAPI.lastAuthTokenState
723+
switch sdkState {
724+
case .valid:
725+
if remaining == "Expired" || remaining == "No Token" {
726+
authStatusLabel.text = remaining
727+
authStatusLabel.textColor = .systemRed
728+
} else {
729+
authStatusLabel.text = "Valid (\(remaining))"
730+
authStatusLabel.textColor = .systemGreen
731+
}
732+
case .invalid:
733+
authStatusLabel.text = "Invalid (\(remaining))"
722734
authStatusLabel.textColor = .systemRed
723-
} else {
724-
authStatusLabel.text = "Valid (\(remaining))"
725-
authStatusLabel.textColor = .systemGreen
735+
case .unknown:
736+
if remaining == "Expired" || remaining == "No Token" {
737+
authStatusLabel.text = remaining
738+
authStatusLabel.textColor = .systemRed
739+
} else {
740+
authStatusLabel.text = "Unknown (\(remaining))"
741+
authStatusLabel.textColor = .systemOrange
742+
}
743+
@unknown default:
744+
authStatusLabel.text = remaining
745+
authStatusLabel.textColor = .systemGray
726746
}
727747
}
728748

tests/common/MockAuthManager.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ class MockAuthManager: IterableAuthManagerProtocol {
5454
func setIsLastAuthTokenValid(_ isValid: Bool) {
5555
isLastAuthTokenValid = isValid
5656
}
57+
58+
func getLastAuthTokenState() -> AuthTokenValidityState {
59+
return isLastAuthTokenValid ? .valid : .invalid
60+
}
5761

5862
func getNextRetryInterval() -> Double {
5963
getNextRetryIntervalCalled = true

0 commit comments

Comments
 (0)