Skip to content

Commit ecec40d

Browse files
authored
Merge pull request #573 from Iterable/jay/MOB-4754-last-push-payload
[MOB-4754] add lastPushPayload to keychain
2 parents 9773e60 + 9f70b4e commit ecec40d

File tree

10 files changed

+291
-136
lines changed

10 files changed

+291
-136
lines changed

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
5531CDAC22A997A4000D05E2 /* IterableInboxViewControllerUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5585DF9022A877E6000A32B9 /* IterableInboxViewControllerUITests.swift */; };
1616
5531CDAE22A9C992000D05E2 /* ClassExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5531CDAD22A9C992000D05E2 /* ClassExtensionsTests.swift */; };
1717
5536781F2576FF9000DB3652 /* IterableUtilTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5536781E2576FF9000DB3652 /* IterableUtilTests.swift */; };
18+
5555425028BED1B400DB5D20 /* KeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5555424F28BED1B400DB5D20 /* KeychainWrapper.swift */; };
1819
556FB1EA244FAF6A00EDF6BD /* InAppPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 556FB1E9244FAF6A00EDF6BD /* InAppPresenter.swift */; };
1920
557AE6BF24A56E5E00B57750 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557AE6BE24A56E5E00B57750 /* Auth.swift */; };
2021
5585DF8F22A73390000A32B9 /* IterableInboxViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5585DF8E22A73390000A32B9 /* IterableInboxViewControllerTests.swift */; };
@@ -396,6 +397,7 @@
396397
552A0AA6280E1FDA00A80963 /* DeepLinkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeepLinkManager.swift; sourceTree = "<group>"; };
397398
5531CDAD22A9C992000D05E2 /* ClassExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassExtensionsTests.swift; sourceTree = "<group>"; };
398399
5536781E2576FF9000DB3652 /* IterableUtilTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableUtilTests.swift; sourceTree = "<group>"; };
400+
5555424F28BED1B400DB5D20 /* KeychainWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainWrapper.swift; sourceTree = "<group>"; };
399401
556FB1E9244FAF6A00EDF6BD /* InAppPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPresenter.swift; sourceTree = "<group>"; };
400402
557AE6BE24A56E5E00B57750 /* Auth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = "<group>"; };
401403
5585DF8E22A73390000A32B9 /* IterableInboxViewControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableInboxViewControllerTests.swift; sourceTree = "<group>"; };
@@ -1263,10 +1265,11 @@
12631265
ACE34AB121376ACB00691224 /* Local Storage */ = {
12641266
isa = PBXGroup;
12651267
children = (
1266-
ACE34AB62139D70B00691224 /* LocalStorageProtocol.swift */,
1267-
ACE34AB221376B1000691224 /* LocalStorage.swift */,
1268-
AC52C5B52729CE44000DCDCF /* IterableUserDefaults.swift */,
12691268
AC52C5B9272A8BC2000DCDCF /* IterableKeychain.swift */,
1269+
AC52C5B52729CE44000DCDCF /* IterableUserDefaults.swift */,
1270+
5555424F28BED1B400DB5D20 /* KeychainWrapper.swift */,
1271+
ACE34AB221376B1000691224 /* LocalStorage.swift */,
1272+
ACE34AB62139D70B00691224 /* LocalStorageProtocol.swift */,
12701273
);
12711274
name = "Local Storage";
12721275
sourceTree = "<group>";
@@ -1823,6 +1826,7 @@
18231826
AC84510922910A0C0052BB8F /* RequestCreator.swift in Sources */,
18241827
ACB8273F22372A5C00DB17D3 /* IterableHtmlMessageViewController.swift in Sources */,
18251828
AC5812F624F3A90F007E6D36 /* OfflineRequestProcessor.swift in Sources */,
1829+
5555425028BED1B400DB5D20 /* KeychainWrapper.swift in Sources */,
18261830
AC81918A22713A400014955E /* AbstractDiffCalculator.swift in Sources */,
18271831
ACA95D2D275494A100AF4666 /* InboxViewRepresentable.swift in Sources */,
18281832
AC684A88222F4FDD00F29749 /* InAppDisplayer.swift in Sources */,

swift-sdk/Constants.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ enum Const {
6363
static let email = "itbl_email"
6464
static let userId = "itbl_userid"
6565
static let authToken = "itbl_auth_token"
66+
static let lastPushPayloadAndExpiration = "itbl_last_push_payload_and_expiration"
6667
}
6768
}
6869

swift-sdk/Internal/InternalIterableAPI.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
4141
}
4242

4343
var lastPushPayload: [AnyHashable: Any]? {
44-
localStorage.getPayload(currentDate: dateProvider.currentDate)
44+
localStorage.getLastPushPayload(dateProvider.currentDate)
4545
}
4646

4747
var attributionInfo: IterableAttributionInfo? {
@@ -522,7 +522,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
522522
let expiration = Calendar.current.date(byAdding: .hour,
523523
value: Const.UserDefault.payloadExpiration,
524524
to: dateProvider.currentDate)
525-
localStorage.save(payload: payload, withExpiration: expiration)
525+
localStorage.saveLastPushPayload(payload, withExpiration: expiration)
526526

527527
if let metadata = IterablePushNotificationMetadata.metadata(fromLaunchOptions: payload) {
528528
if let templateId = metadata.templateId {

swift-sdk/Internal/IterableKeychain.swift

Lines changed: 77 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
import Foundation
66

77
class IterableKeychain {
8+
init(wrapper: KeychainWrapper = KeychainWrapper()) {
9+
self.wrapper = wrapper
10+
}
11+
812
var email: String? {
913
get {
1014
let data = wrapper.data(forKey: Const.Keychain.Key.email)
@@ -21,7 +25,6 @@ class IterableKeychain {
2125

2226
wrapper.set(data, forKey: Const.Keychain.Key.email)
2327
}
24-
2528
}
2629

2730
var userId: String? {
@@ -48,6 +51,7 @@ class IterableKeychain {
4851

4952
return data.flatMap { String(data: $0, encoding: .utf8) }
5053
}
54+
5155
set {
5256
guard let token = newValue,
5357
let data = token.data(using: .utf8) else {
@@ -59,134 +63,104 @@ class IterableKeychain {
5963
}
6064
}
6165

62-
init(wrapper: KeychainWrapper = KeychainWrapper()) {
63-
self.wrapper = wrapper
66+
func getLastPushPayload(currentDate: Date) -> [AnyHashable: Any]? {
67+
guard let payloadExpirationPair = getPayloadExpirationPairFromKeychain() else {
68+
return nil
69+
}
70+
71+
if isLastPushPayloadExpired(expiration: payloadExpirationPair.expiration, currentDate: currentDate) {
72+
removePayloadExpirationPairFromKeychain()
73+
return nil
74+
}
75+
76+
return decodeJsonPayload(payloadExpirationPair.payload)
6477
}
6578

66-
private let wrapper: KeychainWrapper
67-
}
68-
69-
/// Basic wrapper for keychain
70-
/// This should have no dependency on Iterable classes
71-
class KeychainWrapper {
72-
init(serviceName: String = Const.Keychain.serviceName) {
73-
self.serviceName = serviceName
79+
func setLastPushPayload(_ payload: [AnyHashable: Any]?, withExpiration expiration: Date?) {
80+
guard let payload = payload, JSONSerialization.isValidJSONObject(payload) else {
81+
removePayloadExpirationPairFromKeychain()
82+
return
83+
}
84+
85+
savePayloadExpirationPairToKeychain(payload: payload, expiration: expiration)
7486
}
7587

76-
@discardableResult
77-
func set(_ value: Data, forKey key: String) -> Bool {
78-
var keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key)
79-
80-
keychainQueryDictionary[SecValueData] = value
88+
// MARK: - PRIVATE/INTERNAL
89+
90+
private let wrapper: KeychainWrapper
91+
92+
private func getPayloadExpirationPairFromKeychain() -> (payload: Data, expiration: Date?)? {
93+
// get the value from the keychain
94+
guard let keychainValue = wrapper.data(forKey: Const.Keychain.Key.lastPushPayloadAndExpiration) else {
95+
return nil
96+
}
8197

82-
// Assign default protection - Protect the keychain entry so it's only valid when the device is unlocked
83-
keychainQueryDictionary[SecAttrAccessible] = SecAttrAccessibleWhenUnlocked
98+
// decode the payload/expiration pair
99+
guard let payloadExpirationPair = try? JSONDecoder().decode(LastPushPayloadValue.self, from: keychainValue) else {
100+
return nil
101+
}
84102

85-
let status: OSStatus = SecItemAdd(keychainQueryDictionary as CFDictionary, nil)
103+
// cast the payload as a JSON object
104+
guard let lastPushPayloadJSON = try? JSONSerialization.jsonObject(with: payloadExpirationPair.payload, options: []) as? [AnyHashable: Any] else {
105+
return nil
106+
}
86107

87-
if status == errSecSuccess {
88-
return true
89-
} else if status == errSecDuplicateItem {
90-
return update(value, forKey: key)
91-
} else {
92-
return false
108+
guard let lastPushPayloadData = try? JSONSerialization.data(withJSONObject: lastPushPayloadJSON) else {
109+
return nil
93110
}
111+
112+
return (payload: lastPushPayloadData, expiration: payloadExpirationPair.expiration)
94113
}
95114

96-
func data(forKey key: String) -> Data? {
97-
var keychainQueryDictionary = setupKeychainQueryDictionary(forKey: key)
115+
private func savePayloadExpirationPairToKeychain(payload: [AnyHashable: Any]?, expiration: Date?) {
116+
guard let payload = payload else {
117+
removePayloadExpirationPairFromKeychain()
118+
return
119+
}
98120

99-
// Limit search results to one
100-
keychainQueryDictionary[SecMatchLimit] = SecMatchLimitOne
121+
guard let payloadAsData = encodeJsonPayload(payload) else {
122+
return
123+
}
101124

102-
// Specify we want Data/CFData returned
103-
keychainQueryDictionary[SecReturnData] = CFBooleanTrue
125+
let payloadExpirationPair = LastPushPayloadValue(payload: payloadAsData, expiration: expiration)
104126

105-
// Search
106-
var result: AnyObject?
107-
let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result)
127+
guard let encodedPair = try? JSONEncoder().encode(payloadExpirationPair) else {
128+
return
129+
}
108130

109-
return status == noErr ? result as? Data : nil
131+
wrapper.set(encodedPair, forKey: Const.Keychain.Key.lastPushPayloadAndExpiration)
110132
}
111133

112-
@discardableResult
113-
func removeValue(forKey key: String) -> Bool {
114-
let keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key)
115-
116-
// Delete
117-
let status: OSStatus = SecItemDelete(keychainQueryDictionary as CFDictionary)
118-
119-
if status == errSecSuccess {
120-
return true
121-
} else {
122-
return false
134+
private func encodeJsonPayload(_ json: [AnyHashable: Any]?) -> Data? {
135+
guard let json = json, JSONSerialization.isValidJSONObject(json) else {
136+
return nil
123137
}
138+
139+
return try? JSONSerialization.data(withJSONObject: json)
124140
}
125141

126-
@discardableResult
127-
func removeAll() -> Bool {
128-
var keychainQueryDictionary: [String: Any] = [SecClass: SecClassGenericPassword]
129-
130-
keychainQueryDictionary[SecAttrService] = serviceName
131-
132-
let status: OSStatus = SecItemDelete(keychainQueryDictionary as CFDictionary)
133-
134-
if status == errSecSuccess {
135-
return true
136-
} else {
137-
return false
142+
private func decodeJsonPayload(_ data: Data?) -> [AnyHashable: Any]? {
143+
guard let data = data else {
144+
return nil
138145
}
146+
147+
return try? JSONSerialization.jsonObject(with: data) as? [AnyHashable: Any]
139148
}
140149

141-
142-
private let serviceName: String
143-
144-
private func setupKeychainQueryDictionary(forKey key: String) -> [String: Any] {
145-
// Setup default access as generic password (rather than a certificate, internet password, etc)
146-
var keychainQueryDictionary: [String: Any] = [SecClass: SecClassGenericPassword]
147-
148-
// Uniquely identify this keychain accessor
149-
keychainQueryDictionary[SecAttrService] = serviceName
150-
151-
// Uniquely identify the account who will be accessing the keychain
152-
let encodedIdentifier: Data? = key.data(using: .utf8)
153-
154-
keychainQueryDictionary[SecAttrGeneric] = encodedIdentifier
155-
156-
keychainQueryDictionary[SecAttrAccount] = encodedIdentifier
157-
158-
keychainQueryDictionary[SecAttrSynchronizable] = CFBooleanFalse
159-
160-
return keychainQueryDictionary
150+
private func removePayloadExpirationPairFromKeychain() {
151+
wrapper.removeValue(forKey: Const.Keychain.Key.lastPushPayloadAndExpiration)
161152
}
162153

163-
private func update(_ value: Data, forKey key: String) -> Bool {
164-
let keychainQueryDictionary: [String: Any] = setupKeychainQueryDictionary(forKey: key)
165-
let updateDictionary = [SecValueData: value]
166-
167-
// Update
168-
let status: OSStatus = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary)
169-
170-
if status == errSecSuccess {
171-
return true
172-
} else {
154+
private func isLastPushPayloadExpired(expiration: Date?, currentDate: Date) -> Bool {
155+
guard let expiration = expiration else {
173156
return false
174157
}
158+
159+
return !(expiration.timeIntervalSinceReferenceDate > currentDate.timeIntervalSinceReferenceDate)
175160
}
176161

177-
private let SecValueData = kSecValueData as String
178-
private let SecAttrAccessible: String = kSecAttrAccessible as String
179-
private let SecAttrAccessibleWhenUnlocked = kSecAttrAccessibleWhenUnlocked
180-
private let SecClass: String = kSecClass as String
181-
private let SecClassGenericPassword = kSecClassGenericPassword
182-
private let SecAttrService: String = kSecAttrService as String
183-
private let SecAttrGeneric: String = kSecAttrGeneric as String
184-
private let SecAttrAccount: String = kSecAttrAccount as String
185-
private let SecAttrSynchronizable: String = kSecAttrSynchronizable as String
186-
private let CFBooleanTrue = kCFBooleanTrue
187-
private let CFBooleanFalse = kCFBooleanFalse
188-
private let SecMatchLimit: String = kSecMatchLimit as String
189-
private let SecMatchLimitOne = kSecMatchLimitOne
190-
private let SecReturnData: String = kSecReturnData as String
162+
private struct LastPushPayloadValue: Codable {
163+
let payload: Data
164+
let expiration: Date?
165+
}
191166
}
192-

swift-sdk/Internal/IterableUserDefaults.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,23 @@ class IterableUserDefaults {
8282
try? save(dict: payload, withKey: .payload, andExpiration: expiration)
8383
}
8484

85+
func getLastPushPayloadAndExpirationPair() -> (payload: [AnyHashable: Any]?, expiration: Date?)? {
86+
guard let encodedEnvelope = userDefaults.value(forKey: UserDefaultsKey.payload.value) as? Data else {
87+
return nil
88+
}
89+
90+
do {
91+
let envelope = try JSONDecoder().decode(Envelope.self, from: encodedEnvelope)
92+
let decoded = try JSONSerialization.jsonObject(with: envelope.payload, options: []) as? [AnyHashable: Any]
93+
94+
return (payload: decoded, envelope.expiration)
95+
} catch {
96+
return nil
97+
}
98+
}
99+
100+
// create migration function for email, userId, authToken here
101+
85102
// MARK: Private implementation
86103

87104
private let userDefaults: UserDefaults

0 commit comments

Comments
 (0)