Skip to content

Commit d949bc0

Browse files
author
“Akshay
committed
Merge branch 'feature/JWTImrovements' into JWT_Improvement_Part2
# Conflicts: # swift-sdk.xcodeproj/project.pbxproj # swift-sdk/Internal/AuthManager.swift # swift-sdk/Internal/RequestProcessorUtil.swift # swift-sdk/IterableAuthManagerProtocol.swift # tests/common/MockAuthManager.swift
2 parents bbe68c3 + b90f1bd commit d949bc0

18 files changed

+257
-78
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55

6+
## [6.5.3]
7+
### Changed
8+
- Deprecated support for iOS 10 and iOS 11.
9+
610
## [6.5.2]
711
### Fixed
812
- The Privacy Manifest has been relocated to the resources folder within the SDK. This adjustment facilitates the inclusion of the SDK's privacy manifest in the generation process.

Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ GEM
281281

282282
PLATFORMS
283283
arm64-darwin-21
284+
arm64-darwin-22
284285
x86_64-darwin-20
285286

286287
DEPENDENCIES

Iterable-iOS-AppExtensions.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |s|
22
s.name = "Iterable-iOS-AppExtensions"
33
s.module_name = "IterableAppExtensions"
4-
s.version = "6.5.2"
4+
s.version = "6.5.3"
55
s.summary = "App Extensions for Iterable SDK"
66

77
s.description = <<-DESC

Iterable-iOS-SDK.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |s|
22
s.name = "Iterable-iOS-SDK"
33
s.module_name = "IterableSDK"
4-
s.version = "6.5.2"
4+
s.version = "6.5.3"
55
s.summary = "Iterable's official SDK for iOS"
66

77
s.description = <<-DESC

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,7 @@
401401
ACFF42B02465B4AE00FDF10D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFF42AF2465B4AE00FDF10D /* AppDelegate.swift */; };
402402
BA2BB8192BADD5A500EA0229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */; };
403403
BA2BB81A2BADD5A500EA0229 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */; };
404+
E9003E012BF4DF15004AB45B /* RetryPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9003E002BF4DF15004AB45B /* RetryPolicy.swift */; };
404405
E9BF47962B46D5DC0033DB69 /* IterableEmbeddedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BF47952B46D5DC0033DB69 /* IterableEmbeddedView.swift */; };
405406
E9BF47982B46DEB30033DB69 /* IterableEmbeddedView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9BF47972B46DEB30033DB69 /* IterableEmbeddedView.xib */; };
406407
E9FF7FD12BFCBD90000409ED /* AuthFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */; };
@@ -807,6 +808,7 @@
807808
ACFF42AE24656ECF00FDF10D /* ui-tests-app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "ui-tests-app.entitlements"; sourceTree = "<group>"; };
808809
ACFF42AF2465B4AE00FDF10D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
809810
BA2BB8182BADD5A500EA0229 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
811+
E9003E002BF4DF15004AB45B /* RetryPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryPolicy.swift; sourceTree = "<group>"; };
810812
E9BF47952B46D5DC0033DB69 /* IterableEmbeddedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IterableEmbeddedView.swift; sourceTree = "<group>"; };
811813
E9BF47972B46DEB30033DB69 /* IterableEmbeddedView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = IterableEmbeddedView.xib; sourceTree = "<group>"; };
812814
E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFailure.swift; sourceTree = "<group>"; };
@@ -1069,6 +1071,7 @@
10691071
AC78F0E6253D7F09006378A5 /* IterablePushNotificationMetadata.swift */,
10701072
E9FF7FD02BFCBD90000409ED /* AuthFailure.swift */,
10711073
E9FF7FD22BFCBDB9000409ED /* AuthFailureReason.swift */,
1074+
E9003E002BF4DF15004AB45B /* RetryPolicy.swift */,
10721075
);
10731076
path = "swift-sdk";
10741077
sourceTree = "<group>";
@@ -2095,6 +2098,7 @@
20952098
AC72A0D220CF4D12004D7997 /* IterableUtil.swift in Sources */,
20962099
AC32E16821DD55B900BD4F83 /* OrderedDictionary.swift in Sources */,
20972100
ACF406232507BC72005FD775 /* NetworkMonitor.swift in Sources */,
2101+
E9003E012BF4DF15004AB45B /* RetryPolicy.swift in Sources */,
20982102
AC1712892416AEF400F2BB0E /* WebViewProtocol.swift in Sources */,
20992103
AC3C10F9213F46A900A9B839 /* IterableLogging.swift in Sources */,
21002104
55DD207F26A0D83800773CC7 /* IterableAuthManagerProtocol.swift in Sources */,

swift-sdk/Constants.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum Const {
1515

1616
static let deepLinkRegex = "/a/[a-zA-Z0-9]+"
1717
static let href = "href"
18+
static let exponentialFactor = 2.0
1819

1920
enum Http {
2021
static let GET = "GET"
@@ -286,6 +287,8 @@ enum JsonValue {
286287
enum Code {
287288
static let badApiKey = "BadApiKey"
288289
static let invalidJwtPayload = "InvalidJwtPayload"
290+
static let badAuthorizationHeader = "BadAuthorizationHeader"
291+
static let jwtUserIdentifiersMismatched = "JwtUserIdentifiersMismatched"
289292
}
290293
}
291294

swift-sdk/Internal/AuthManager.swift

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import Foundation
66

77
class AuthManager: IterableAuthManagerProtocol {
88
init(delegate: IterableAuthDelegate?,
9+
authRetryPolicy: RetryPolicy,
910
expirationRefreshPeriod: TimeInterval,
1011
localStorage: LocalStorageProtocol,
1112
dateProvider: DateProviderProtocol) {
1213
ITBInfo()
1314

1415
self.delegate = delegate
16+
self.authRetryPolicy = authRetryPolicy
1517
self.localStorage = localStorage
1618
self.dateProvider = dateProvider
1719
self.expirationRefreshPeriod = expirationRefreshPeriod
@@ -35,26 +37,44 @@ class AuthManager: IterableAuthManagerProtocol {
3537
hasFailedPriorAuth = false
3638
}
3739

38-
func requestNewAuthToken(hasFailedPriorAuth: Bool = false, onSuccess: AuthTokenRetrievalHandler? = nil) {
40+
func requestNewAuthToken(hasFailedPriorAuth: Bool = false,
41+
onSuccess: AuthTokenRetrievalHandler? = nil,
42+
shouldIgnoreRetryPolicy: Bool) {
3943
ITBInfo()
4044

41-
guard !pendingAuth else {
42-
return
43-
}
44-
45-
guard !self.hasFailedPriorAuth || !hasFailedPriorAuth else {
45+
if shouldPauseRetry(shouldIgnoreRetryPolicy) || pendingAuth || hasFailedAuth(hasFailedPriorAuth) {
4646
return
4747
}
4848

4949
self.hasFailedPriorAuth = hasFailedPriorAuth
50-
5150
pendingAuth = true
5251

52+
if shouldUseLastValidToken(shouldIgnoreRetryPolicy) {
53+
// if some JWT retry had valid token it will not fetch the auth token again from developer function
54+
onAuthTokenReceived(retrievedAuthToken: authToken, onSuccess: onSuccess)
55+
return
56+
}
57+
5358
delegate?.onAuthTokenRequested { [weak self] retrievedAuthToken in
59+
self?.pendingAuth = false
60+
self?.retryCount+=1
5461
self?.onAuthTokenReceived(retrievedAuthToken: retrievedAuthToken, onSuccess: onSuccess)
5562
}
5663
}
5764

65+
private func hasFailedAuth(_ hasFailedPriorAuth: Bool) -> Bool {
66+
return self.hasFailedPriorAuth && hasFailedPriorAuth
67+
}
68+
69+
private func shouldPauseRetry(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
70+
return (!shouldIgnoreRetryPolicy && pauseAuthRetry) ||
71+
(retryCount >= authRetryPolicy.maxRetry && !shouldIgnoreRetryPolicy)
72+
}
73+
74+
private func shouldUseLastValidToken(_ shouldIgnoreRetryPolicy: Bool) -> Bool {
75+
return isLastAuthTokenValid && !shouldIgnoreRetryPolicy
76+
}
77+
5878
func setNewToken(_ newToken: String) {
5979
ITBInfo()
6080

@@ -69,6 +89,7 @@ class AuthManager: IterableAuthManagerProtocol {
6989
storeAuthToken()
7090

7191
clearRefreshTimer()
92+
isLastAuthTokenValid = false
7293
}
7394

7495
// MARK: - Private/Internal
@@ -79,11 +100,39 @@ class AuthManager: IterableAuthManagerProtocol {
79100
private var pendingAuth: Bool = false
80101
private var hasFailedPriorAuth: Bool = false
81102

103+
private var authRetryPolicy: RetryPolicy
104+
private var retryCount: Int = 0
105+
private var isLastAuthTokenValid: Bool = false
106+
private var pauseAuthRetry: Bool = false
107+
private var isTimerScheduled: Bool = false
108+
82109
private weak var delegate: IterableAuthDelegate?
83110
private let expirationRefreshPeriod: TimeInterval
84111
private var localStorage: LocalStorageProtocol
85112
private let dateProvider: DateProviderProtocol
86113

114+
func pauseAuthRetries(_ pauseAuthRetry: Bool) {
115+
self.pauseAuthRetry = pauseAuthRetry
116+
resetRetryCount()
117+
}
118+
119+
func setIsLastAuthTokenValid(_ isValid: Bool) {
120+
isLastAuthTokenValid = isValid
121+
}
122+
123+
func getNextRetryInterval() -> Double {
124+
var nextRetryInterval = Double(authRetryPolicy.retryInterval)
125+
if authRetryPolicy.retryBackoff == .exponential {
126+
nextRetryInterval = Double(nextRetryInterval) * pow(Const.exponentialFactor, Double(retryCount - 1))
127+
}
128+
129+
return nextRetryInterval
130+
}
131+
132+
private func resetRetryCount() {
133+
retryCount = 0
134+
}
135+
87136
private func storeAuthToken() {
88137
localStorage.authToken = authToken
89138
}
@@ -93,37 +142,34 @@ class AuthManager: IterableAuthManagerProtocol {
93142

94143
authToken = localStorage.authToken
95144

96-
queueAuthTokenExpirationRefresh(authToken)
145+
_ = queueAuthTokenExpirationRefresh(authToken)
97146
}
98147

99148
private func onAuthTokenReceived(retrievedAuthToken: String?, onSuccess: AuthTokenRetrievalHandler? = nil) {
100149
ITBInfo()
101150

102151
pendingAuth = false
103152

104-
guard retrievedAuthToken != nil else {
153+
if retrievedAuthToken != nil {
154+
let isRefreshQueued = queueAuthTokenExpirationRefresh(retrievedAuthToken, onSuccess: onSuccess)
155+
if !isRefreshQueued {
156+
onSuccess?(authToken)
157+
}
158+
} else {
105159
handleAuthFailure(failedAuthToken: nil, reason: .authTokenNull)
106-
107-
/// by default, schedule a refresh for 10s
108-
scheduleAuthTokenRefreshTimer(10)
109-
110-
return
160+
scheduleAuthTokenRefreshTimer(interval: getNextRetryInterval(), successCallback: onSuccess)
111161
}
112162

113163
authToken = retrievedAuthToken
114164

115165
storeAuthToken()
116-
117-
queueAuthTokenExpirationRefresh(authToken)
118-
119-
onSuccess?(authToken)
120166
}
121167

122168
func handleAuthFailure(failedAuthToken: String?, reason: AuthFailureReason) {
123169
delegate?.onAuthFailure(AuthFailure(userKey: IterableUtil.getEmailOrUserId(), failedAuthToken: failedAuthToken, failedRequestTime: IterableUtil.secondsFromEpoch(for: dateProvider.currentDate), failureReason: reason))
124170
}
125171

126-
private func queueAuthTokenExpirationRefresh(_ authToken: String?) {
172+
private func queueAuthTokenExpirationRefresh(_ authToken: String?, onSuccess: AuthTokenRetrievalHandler? = nil) -> Bool {
127173
ITBInfo()
128174

129175
clearRefreshTimer()
@@ -132,32 +178,47 @@ class AuthManager: IterableAuthManagerProtocol {
132178
handleAuthFailure(failedAuthToken: authToken, reason: .authTokenPayloadInvalid)
133179

134180
/// schedule a default timer of 10 seconds if we fall into this case
135-
scheduleAuthTokenRefreshTimer(10)
181+
scheduleAuthTokenRefreshTimer(interval: getNextRetryInterval(), successCallback: onSuccess)
136182

137-
return
183+
return true
138184
}
139185

140186
let timeIntervalToRefresh = TimeInterval(expirationDate) - dateProvider.currentDate.timeIntervalSince1970 - expirationRefreshPeriod
141-
142-
scheduleAuthTokenRefreshTimer(timeIntervalToRefresh)
187+
if timeIntervalToRefresh > 0 {
188+
scheduleAuthTokenRefreshTimer(interval: timeIntervalToRefresh, isScheduledRefresh: true, successCallback: onSuccess)
189+
return true
190+
}
191+
return false
143192
}
144193

145-
private func scheduleAuthTokenRefreshTimer(_ interval: TimeInterval) {
194+
func scheduleAuthTokenRefreshTimer(interval: TimeInterval, isScheduledRefresh: Bool = false, successCallback: AuthTokenRetrievalHandler? = nil) {
146195
ITBInfo()
196+
if shouldSkipTokenRefresh(isScheduledRefresh: isScheduledRefresh) {
197+
// we only stop schedule token refresh if it is called from retry (in case of failure). The normal auth token refresh schedule would work
198+
return
199+
}
147200

148201
expirationRefreshTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
202+
self?.isTimerScheduled = false
149203
if self?.localStorage.email != nil || self?.localStorage.userId != nil {
150-
self?.requestNewAuthToken(hasFailedPriorAuth: false)
204+
self?.requestNewAuthToken(hasFailedPriorAuth: false, onSuccess: successCallback, shouldIgnoreRetryPolicy: isScheduledRefresh)
151205
} else {
152206
ITBDebug("Email or userId is not available. Skipping token refresh")
153207
}
154208
}
209+
210+
isTimerScheduled = true
211+
}
212+
213+
private func shouldSkipTokenRefresh(isScheduledRefresh: Bool) -> Bool {
214+
return (pauseAuthRetry && !isScheduledRefresh) || isTimerScheduled
155215
}
156216

157217
private func clearRefreshTimer() {
158218
ITBInfo()
159219

160220
expirationRefreshTimer?.invalidate()
221+
isTimerScheduled = false
161222
expirationRefreshTimer = nil
162223
}
163224

swift-sdk/Internal/DependencyContainerProtocol.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ extension DependencyContainerProtocol {
5151

5252
func createAuthManager(config: IterableConfig) -> IterableAuthManagerProtocol {
5353
AuthManager(delegate: config.authDelegate,
54+
authRetryPolicy: config.retryPolicy,
5455
expirationRefreshPeriod: config.expiringAuthTokenRefreshPeriod,
5556
localStorage: localStorage,
5657
dateProvider: dateProvider)

swift-sdk/Internal/InternalIterableAPI.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
582582
private func onLogin(_ authToken: String? = nil) {
583583
ITBInfo()
584584

585+
self.authManager.pauseAuthRetries(false)
585586
if let authToken = authToken {
586587
self.authManager.setNewToken(authToken)
587588
completeUserLogin()
@@ -599,7 +600,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
599600
if token != nil {
600601
self?.completeUserLogin()
601602
}
602-
})
603+
}, shouldIgnoreRetryPolicy: true)
603604
}
604605

605606
private func completeUserLogin() {

swift-sdk/Internal/OfflineRequestProcessor.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ struct OfflineRequestProcessor: RequestProcessorProtocol {
319319
return RequestCreator(auth: authProvider.auth, deviceMetadata: deviceMetadata)
320320
}
321321

322-
private func sendIterableRequest(requestGenerator: (RequestCreator) -> Result<IterableRequest, IterableError>,
322+
private func sendIterableRequest(requestGenerator: @escaping (RequestCreator) -> Result<IterableRequest, IterableError>,
323323
successHandler onSuccess: OnSuccessHandler?,
324324
failureHandler onFailure: OnFailureHandler?,
325325
identifier: String) -> Pending<SendRequestValue, SendRequestError> {
@@ -343,11 +343,25 @@ struct OfflineRequestProcessor: RequestProcessorProtocol {
343343
SendRequestError.from(error: error)
344344
}.flatMap { taskId -> Pending<SendRequestValue, SendRequestError> in
345345
let pendingTask = notificationListener.futureFromTask(withTaskId: taskId)
346-
return RequestProcessorUtil.apply(successHandler: onSuccess,
346+
let result = RequestProcessorUtil.apply(successHandler: onSuccess,
347347
andFailureHandler: onFailure,
348348
andAuthManager: authManager,
349349
toResult: pendingTask,
350350
withIdentifier: identifier)
351+
result.onError { error in
352+
if error.httpStatusCode == 401, RequestProcessorUtil.matchesJWTErrorCode(error.iterableCode) {
353+
authManager?.handleAuthFailure(failedAuthToken: authManager?.getAuthToken(), reason: RequestProcessorUtil.getMappedErrorCodeForMessage(error.reason ?? ""))
354+
authManager?.setIsLastAuthTokenValid(false)
355+
let retryInterval = authManager?.getNextRetryInterval() ?? 1
356+
DispatchQueue.main.async {
357+
authManager?.scheduleAuthTokenRefreshTimer(interval: retryInterval, isScheduledRefresh: false, successCallback: { _ in
358+
_ = sendIterableRequest(requestGenerator: requestGenerator, successHandler: onSuccess, failureHandler: onFailure, identifier: identifier)
359+
})
360+
}
361+
362+
}
363+
}
364+
return result
351365
}
352366
}
353367

0 commit comments

Comments
 (0)