diff --git a/OptimoveSDK/Sources/Classes/CoreData/Versions/EventCDv1ToEventCDv2MigrationPolicy.swift b/OptimoveSDK/Sources/Classes/CoreData/Versions/EventCDv1ToEventCDv2MigrationPolicy.swift index 7175c394..98fed9a5 100644 --- a/OptimoveSDK/Sources/Classes/CoreData/Versions/EventCDv1ToEventCDv2MigrationPolicy.swift +++ b/OptimoveSDK/Sources/Classes/CoreData/Versions/EventCDv1ToEventCDv2MigrationPolicy.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import GenericJSON import OptimoveCore final class EventCDv1ToEventCDv2MigrationPolicy: NSEntityMigrationPolicy { diff --git a/OptimoveSDK/Sources/Classes/JSON/Initialization.swift b/OptimoveSDK/Sources/Classes/JSON/Initialization.swift deleted file mode 100644 index ed11c86c..00000000 --- a/OptimoveSDK/Sources/Classes/JSON/Initialization.swift +++ /dev/null @@ -1,138 +0,0 @@ -/* - MIT License - - Copyright (c) 2017 Tomáš Znamenáček - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -private struct InitializationError: Error {} - -extension JSON { - /// Create a JSON value from anything. - /// - /// Argument has to be a valid JSON structure: A `Double`, `Int`, `String`, - /// `Bool`, an `Array` of those types or a `Dictionary` of those types. - /// - /// You can also pass `nil` or `NSNull`, both will be treated as `.null`. - init(_ value: Any) throws { - switch value { - case _ as NSNull: - self = .null - case let opt as Any? where opt == nil: - self = .null - case let num as NSNumber: - if num.isBool { - self = .bool(num.boolValue) - } else { - self = .number(num.doubleValue) - } - case let str as String: - self = .string(str) - case let bool as Bool: - self = .bool(bool) - case let array as [Any]: - self = try .array(array.map(JSON.init)) - case let dict as [String: Any]: - self = try .object(dict.mapValues(JSON.init)) - default: - throw InitializationError() - } - } -} - -extension JSON { - /// Create a JSON value from an `Encodable`. This will give you access to the “raw” - /// encoded JSON value the `Encodable` is serialized into. - init(encodable: T) throws { - let encoded = try JSONEncoder().encode(encodable) - self = try JSONDecoder().decode(JSON.self, from: encoded) - } -} - -extension JSON: ExpressibleByBooleanLiteral { - init(booleanLiteral value: Bool) { - self = .bool(value) - } -} - -extension JSON: ExpressibleByNilLiteral { - init(nilLiteral _: ()) { - self = .null - } -} - -extension JSON: ExpressibleByArrayLiteral { - init(arrayLiteral elements: JSON...) { - self = .array(elements) - } -} - -extension JSON: ExpressibleByDictionaryLiteral { - init(dictionaryLiteral elements: (String, JSON)...) { - var object: [String: JSON] = [:] - for (k, v) in elements { - object[k] = v - } - self = .object(object) - } -} - -extension JSON: ExpressibleByFloatLiteral { - init(floatLiteral value: Double) { - self = .number(value) - } -} - -extension JSON: ExpressibleByIntegerLiteral { - init(integerLiteral value: Int) { - self = .number(Double(value)) - } -} - -extension JSON: ExpressibleByStringLiteral { - init(stringLiteral value: String) { - self = .string(value) - } -} - -// MARK: - NSNumber - -private extension NSNumber { - /// Boolean value indicating whether this `NSNumber` wraps a boolean. - /// - /// For example, when using `NSJSONSerialization` Bool values are converted into `NSNumber` instances. - /// - /// - seealso: https://stackoverflow.com/a/49641315/3589408 - var isBool: Bool { - let objCType = String(cString: self.objCType) - if (compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (compare(falseNumber) == .orderedSame && objCType == falseObjCType) { - return true - } else { - return false - } - } -} - -private let trueNumber = NSNumber(value: true) -private let falseNumber = NSNumber(value: false) -private let trueObjCType = String(cString: trueNumber.objCType) -private let falseObjCType = String(cString: falseNumber.objCType) diff --git a/OptimoveSDK/Sources/Classes/JSON/JSON+init.swift b/OptimoveSDK/Sources/Classes/JSON/JSON+init.swift new file mode 100644 index 00000000..9ffe4317 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/JSON/JSON+init.swift @@ -0,0 +1,61 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation +import GenericJSON + +extension JSON { + // Convenience initializer to convert from [String: Any] + init?(dictionary: [String: Any]) { + var jsonDict = [String: JSON]() + + for (key, value) in dictionary { + if let stringValue = value as? String { + jsonDict[key] = JSON.string(stringValue) + } else if let intValue = value as? Int { + jsonDict[key] = JSON.number(Double(intValue)) + } else if let doubleValue = value as? Double { + jsonDict[key] = JSON.number(doubleValue) + } else if let boolValue = value as? Bool { + jsonDict[key] = JSON.bool(boolValue) + } else if let dictValue = value as? [String: Any] { + jsonDict[key] = JSON(dictionary: dictValue) + } else if let arrayValue = value as? [Any] { + jsonDict[key] = JSON(array: arrayValue) + } else { + // For unsupported types, you can either skip or handle them differently + // Skipping here + Logger.error("Skipping unsupported type: \(type(of: value))") + continue + } + } + + self = JSON.object(jsonDict) + } + + // Helper to convert an array + private init?(array: [Any]) { + var jsonArray: [JSON] = [] + + for value in array { + if let stringValue = value as? String { + jsonArray.append(JSON.string(stringValue)) + } else if let intValue = value as? Int { + jsonArray.append(JSON.number(Double(intValue))) + } else if let doubleValue = value as? Double { + jsonArray.append(JSON.number(doubleValue)) + } else if let boolValue = value as? Bool { + jsonArray.append(JSON.bool(boolValue)) + } else if let dictValue = value as? [String: Any] { + jsonArray.append(JSON(dictionary: dictValue)!) + } else if let arrayValue = value as? [Any] { + jsonArray.append(JSON(array: arrayValue)!) + } else { + // Handle unsupported types + Logger.error("Skipping unsupported type: \(type(of: value))") + continue + } + } + + self = JSON.array(jsonArray) + } +} diff --git a/OptimoveSDK/Sources/Classes/JSON/JSON.swift b/OptimoveSDK/Sources/Classes/JSON/JSON.swift deleted file mode 100644 index 5cd4fb5b..00000000 --- a/OptimoveSDK/Sources/Classes/JSON/JSON.swift +++ /dev/null @@ -1,102 +0,0 @@ -/* - MIT License - - Copyright (c) 2017 Tomáš Znamenáček - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -/// A JSON value representation. This is a bit more useful than the naïve `[String:Any]` type -/// for JSON values, since it makes sure only valid JSON values are present & supports `Equatable` -/// and `Codable`, so that you can compare values for equality and code and decode them into data -/// or strings. -@dynamicMemberLookup enum JSON: Equatable { - case string(String) - case number(Double) - case object([String: JSON]) - case array([JSON]) - case bool(Bool) - case null -} - -extension JSON: Codable { - func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch self { - case let .array(array): - try container.encode(array) - case let .object(object): - try container.encode(object) - case let .string(string): - try container.encode(string) - case let .number(number): - try container.encode(number) - case let .bool(bool): - try container.encode(bool) - case .null: - try container.encodeNil() - } - } - - init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - - if let object = try? container.decode([String: JSON].self) { - self = .object(object) - } else if let array = try? container.decode([JSON].self) { - self = .array(array) - } else if let string = try? container.decode(String.self) { - self = .string(string) - } else if let bool = try? container.decode(Bool.self) { - self = .bool(bool) - } else if let number = try? container.decode(Double.self) { - self = .number(number) - } else if container.decodeNil() { - self = .null - } else { - throw DecodingError.dataCorrupted( - .init(codingPath: decoder.codingPath, debugDescription: "Invalid JSON value.") - ) - } - } -} - -extension JSON: CustomDebugStringConvertible { - var debugDescription: String { - switch self { - case let .string(str): - return str.debugDescription - case let .number(num): - return num.debugDescription - case let .bool(bool): - return bool.description - case .null: - return "null" - default: - let encoder = JSONEncoder() - encoder.outputFormatting = [.prettyPrinted] - return try! String(data: encoder.encode(self), encoding: .utf8)! - } - } -} - -extension JSON: Hashable {} diff --git a/OptimoveSDK/Sources/Classes/JSON/Merging.swift b/OptimoveSDK/Sources/Classes/JSON/Merging.swift deleted file mode 100644 index a3ed3c79..00000000 --- a/OptimoveSDK/Sources/Classes/JSON/Merging.swift +++ /dev/null @@ -1,63 +0,0 @@ -/* - MIT License - - Copyright (c) 2017 Tomáš Znamenáček - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -extension JSON { - /// Return a new JSON value by merging two other ones - /// - /// If we call the current JSON value `old` and the incoming JSON value - /// `new`, the precise merging rules are: - /// - /// 1. If `old` or `new` are anything but an object, return `new`. - /// 2. If both `old` and `new` are objects, create a merged object like this: - /// 1. Add keys from `old` not present in `new` (“no change” case). - /// 2. Add keys from `new` not present in `old` (“create” case). - /// 3. For keys present in both `old` and `new`, apply merge recursively to their values (“update” case). - func merging(with new: JSON) -> JSON { - // If old or new are anything but an object, return new. - guard case let .object(lhs) = self, case let .object(rhs) = new else { - return new - } - - var merged: [String: JSON] = [:] - - // Add keys from old not present in new (“no change” case). - for (key, val) in lhs where rhs[key] == nil { - merged[key] = val - } - - // Add keys from new not present in old (“create” case). - for (key, val) in rhs where lhs[key] == nil { - merged[key] = val - } - - // For keys present in both old and new, apply merge recursively to their values. - for key in lhs.keys where rhs[key] != nil { - merged[key] = lhs[key]?.merging(with: rhs[key]!) - } - - return JSON.object(merged) - } -} diff --git a/OptimoveSDK/Sources/Classes/JSON/ObjcJSON.swift b/OptimoveSDK/Sources/Classes/JSON/ObjcJSON.swift new file mode 100644 index 00000000..19b32e65 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/JSON/ObjcJSON.swift @@ -0,0 +1,152 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation +import GenericJSON + +@objcMembers +class ObjcJSON: NSObject { + private var json: JSON + + // Initializer with JSON + init(json: JSON) { + self.json = json + } + + // Convenience initializer for empty JSON + override init() { + self.json = JSON.object([:]) + } + + subscript(key: String) -> ObjcJSON? { + get { + return jsonObjectForKey(key) + } + set { + if let adapter = newValue { + try? setJSONObject(adapter, forKey: key) + } + } + } + + var string: String? { + get { + return json.stringValue + } + set { + if let value = newValue { + json = JSON.string(value) + } + } + } + + var bool: Bool? { + get { + return json.boolValue + } + set { + if let value = newValue { + json = JSON.bool(value) + } + } + } + + var double: Double? { + get { + return json.doubleValue + } + set { + if let value = newValue { + json = JSON.number(value) + } + } + } + + var object: [String: ObjcJSON]? { + get { + return json.objectValue?.mapValues { ObjcJSON(json: $0) } + } + set { + if let value = newValue { + json = JSON.object(value.mapValues { $0.json }) + } + } + } + + var array: [ObjcJSON]? { + get { + return json.arrayValue?.map { ObjcJSON(json: $0) } + } + set { + if let value = newValue { + json = JSON.array(value.map { $0.json }) + } + } + } + + var isNull: Bool { + return json.isNull + } + + // MARK: - Accessor Methods + + // Get String value for a key + func stringValueForKey(_ key: String) -> String? { + return json[key]?.stringValue + } + + // Get Boolean value for a key + func boolValueForKey(_ key: String) -> Bool { + return json[key]?.boolValue ?? false + } + + // Get Double value for a key + func doubleValueForKey(_ key: String) -> Double { + return json[key]?.doubleValue ?? 0.0 + } + + // Get JSON object for a key + func jsonObjectForKey(_ key: String) -> ObjcJSON? { + guard let subJSON = json[key] else { return nil } + return ObjcJSON(json: subJSON) + } + + // MARK: - Mutator Methods + + // Set String value for a key + func setStringValue(_ value: String, forKey key: String) throws { + json = try json.merging(with: JSON( + [key: JSON.string(value)] + )) + } + + // Set Boolean value for a key + func setBoolValue(_ value: Bool, forKey key: String) throws { + json = try json.merging(with: JSON( + [key: JSON.bool(value)] + )) + } + + // Set Double value for a key + func setDoubleValue(_ value: Double, forKey key: String) throws { + json = try json.merging(with: JSON( + [key: JSON.number(value)] + )) + } + + // Set JSON object for a key + func setJSONObject(_ adapter: ObjcJSON, forKey key: String) throws { + json = json.merging(with: adapter.toGenericJSON()) + } + + // MARK: - Utility Methods + + // Convert to JSON String + func jsonString() -> String? { + return json.debugDescription + } + + // Convert back to GenericJSON for use in Swift + func toGenericJSON() -> JSON { + return json + } +} diff --git a/OptimoveSDK/Sources/Classes/JSON/Querying.swift b/OptimoveSDK/Sources/Classes/JSON/Querying.swift deleted file mode 100644 index 0d6d49d1..00000000 --- a/OptimoveSDK/Sources/Classes/JSON/Querying.swift +++ /dev/null @@ -1,128 +0,0 @@ -/* - MIT License - - Copyright (c) 2017 Tomáš Znamenáček - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. - */ - -import Foundation - -extension JSON { - /// Return the string value if this is a `.string`, otherwise `nil` - var stringValue: String? { - if case let .string(value) = self { - return value - } - return nil - } - - /// Return the double value if this is a `.number`, otherwise `nil` - var doubleValue: Double? { - if case let .number(value) = self { - return value - } - return nil - } - - /// Return the bool value if this is a `.bool`, otherwise `nil` - var boolValue: Bool? { - if case let .bool(value) = self { - return value - } - return nil - } - - /// Return the object value if this is an `.object`, otherwise `nil` - var objectValue: [String: JSON]? { - if case let .object(value) = self { - return value - } - return nil - } - - /// Return the array value if this is an `.array`, otherwise `nil` - var arrayValue: [JSON]? { - if case let .array(value) = self { - return value - } - return nil - } - - /// Return `true` iff this is `.null` - var isNull: Bool { - if case .null = self { - return true - } - return false - } - - /// If this is an `.array`, return item at index - /// - /// If this is not an `.array` or the index is out of bounds, returns `nil`. - subscript(index: Int) -> JSON? { - if case let .array(arr) = self, arr.indices.contains(index) { - return arr[index] - } - return nil - } - - /// If this is an `.object`, return item at key - subscript(key: String) -> JSON? { - if case let .object(dict) = self { - return dict[key] - } - return nil - } - - /// Dynamic member lookup sugar for string subscripts - /// - /// This lets you write `json.foo` instead of `json["foo"]`. - subscript(dynamicMember member: String) -> JSON? { - return self[member] - } - - /// Return the JSON type at the keypath if this is an `.object`, otherwise `nil` - /// - /// This lets you write `json[keyPath: "foo.bar.jar"]`. - subscript(keyPath keyPath: String) -> JSON? { - return queryKeyPath(keyPath.components(separatedBy: ".")) - } - - func queryKeyPath(_ path: T) -> JSON? where T: Collection, T.Element == String { - // Only object values may be subscripted - guard case let .object(object) = self else { - return nil - } - - // Is the path non-empty? - guard let head = path.first else { - return nil - } - - // Do we have a value at the required key? - guard let value = object[head] else { - return nil - } - - let tail = path.dropFirst() - - return tail.isEmpty ? value : value.queryKeyPath(tail) - } -} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift index 61cb364e..fe2d01a0 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift @@ -114,12 +114,17 @@ final class AnalyticsHelper { // MARK: Event Tracking func trackEvent(eventType: String, properties: [String: Any]?, immediateFlush: Bool) { - trackEvent(eventType: eventType, atTime: Date(), properties: properties, immediateFlush: immediateFlush) + trackEvent( + eventType: eventType, + atTime: Date(), + properties: properties, + immediateFlush: immediateFlush + ) } func trackEvent(eventType: String, atTime: Date, properties: [String: Any]?, immediateFlush: Bool, onSyncComplete: SyncCompletedBlock? = nil) { if eventType == "" || (properties != nil && !JSONSerialization.isValidJSONObject(properties as Any)) { - print("Ignoring invalid event with empty type or non-serializable properties") + Logger.error("Ignoring invalid event with empty type or non-serializable properties") return } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift index 19d748c5..12946faa 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import GenericJSON import OptimoveCore import UIKit @@ -177,9 +178,10 @@ class InAppManager { } func updateUserConsent(consentGiven: Bool) { - let props: [String: Any] = ["consented": consentGiven] - - Optimobile.trackEventImmediately(eventType: OptimobileEvent.IN_APP_CONSENT_CHANGED.rawValue, properties: props) + Optimobile.trackEventImmediately( + eventType: OptimobileEvent.IN_APP_CONSENT_CHANGED.rawValue, + properties: ["consented": consentGiven] + ) if consentGiven { UserDefaults.standard.set(consentGiven, forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) @@ -303,25 +305,34 @@ class InAppManager { UserDefaults.standard.set(Date(), forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) syncBarrier.signal() } - - let messagesToPersist = decodedBody as? [[AnyHashable: Any]] - if messagesToPersist == nil || messagesToPersist!.count == 0 { - onComplete?(0) - return - } - - self.persistInAppMessages(messages: messagesToPersist!) - onComplete?(1) - - DispatchQueue.main.async { - if UIApplication.shared.applicationState != .active { + do { + guard let decodedBody = decodedBody else { + onComplete?(0) + return + } + let messagesToPersist = try JSON(decodedBody) + if messagesToPersist == nil || messagesToPersist.count == 0 { + onComplete?(0) return } - DispatchQueue.global(qos: .default).async { - let messagesToPresent = self.getMessagesToPresent([InAppPresented.IMMEDIATELY.rawValue]) - self.presenter.queueMessagesForPresentation(messages: messagesToPresent, tickleIds: self.pendingTickleIds) + self.persistInAppMessages(messages: messagesToPersist) + onComplete?(1) + + DispatchQueue.main.async { + if UIApplication.shared.applicationState != .active { + return + } + + DispatchQueue.global(qos: .default).async { + let messagesToPresent = self.getMessagesToPresent([InAppPresented.IMMEDIATELY.rawValue]) + self.presenter.queueMessagesForPresentation(messages: messagesToPresent, tickleIds: self.pendingTickleIds) + } } + } catch { + Logger.error(error.localizedDescription) + onComplete?(-1) + syncBarrier.signal() } }, onFailure: { _, _, _ in onComplete?(-1) @@ -332,127 +343,93 @@ class InAppManager { } } - private func persistInAppMessages(messages: [[AnyHashable: Any]]) { + private func persistInAppMessages(messages: JSON) { guard let context = messagesContext else { NSLog("InAppManager: NSManagedObjectContext is nil in persistInAppMessages") return } context.performAndWait { - let entity: NSEntityDescription? = NSEntityDescription.entity(forEntityName: "Message", in: context) - - if entity == nil { - print("Failed to get entity description for Message, aborting!") - return - } - - var mostRecentUpdate = NSDate(timeIntervalSince1970: 0) - let dateParser = DateFormatter() - dateParser.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" - dateParser.locale = Locale(identifier: "en_US_POSIX") - dateParser.timeZone = TimeZone(secondsFromGMT: 0) - - var fetchedWithInbox = false - for message in messages { - let partId = message["id"] as! Int64 - - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Message") - fetchRequest.entity = entity - let predicate = NSPredicate(format: "id = %i", partId) - fetchRequest.predicate = predicate - - var fetchedObjects: [InAppMessageEntity] - do { - fetchedObjects = try context.fetch(fetchRequest) as! [InAppMessageEntity] - } catch { - continue + do { + guard let entity = NSEntityDescription.entity(forEntityName: "Message", in: context) else { + print("Failed to get entity description for Message, aborting!") + return } - // Upsert - let model: InAppMessageEntity = fetchedObjects.count == 1 ? fetchedObjects[0] : InAppMessageEntity(entity: entity!, insertInto: context) + var mostRecentUpdate = Date(timeIntervalSince1970: 0) + let dateParser = DateFormatter() + dateParser.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + dateParser.locale = Locale(identifier: "en_US_POSIX") + dateParser.timeZone = TimeZone(secondsFromGMT: 0) + + for message in try unwrap(messages.arrayValue) { + guard let partId = message.id?.doubleValue, + let updatedAtString = message.updatedAt?.stringValue, + let presentedWhenString = message.presentedWhen?.stringValue + else { + continue + } - model.id = partId - model.updatedAt = dateParser.date(from: message["updatedAt"] as! String)! as NSDate - if model.dismissedAt == nil { - model.dismissedAt = dateParser.date(from: message["openedAt"] as? String ?? "") as NSDate? - } - model.presentedWhen = message["presentedWhen"] as! String + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Message") + fetchRequest.entity = entity + fetchRequest.predicate = NSPredicate(format: "id = %lld", partId) - if model.readAt == nil { - model.readAt = dateParser.date(from: message["readAt"] as? String ?? "") as NSDate? - } + let fetchedObjects: [InAppMessageEntity] + do { + fetchedObjects = try context.fetch(fetchRequest) + } catch { + continue + } - if model.sentAt == nil { - model.sentAt = dateParser.date(from: message["sentAt"] as? String ?? "") as NSDate? - } + // Upsert + let model: InAppMessageEntity = fetchedObjects.count == 1 ? fetchedObjects[0] : InAppMessageEntity(entity: entity, insertInto: context) + + model.id = Int64(partId) + model.updatedAt = dateParser.date(from: updatedAtString)! + model.dismissedAt = dateParser.date(from: message["openedAt"]?.stringValue ?? "") + model.presentedWhen = presentedWhenString - model.content = message["content"] as! NSDictionary - model.data = message["data"] as? NSDictionary - model.badgeConfig = message["badge"] as? NSDictionary - model.inboxConfig = message["inbox"] as? NSDictionary + if model.readAt == nil { + model.readAt = dateParser.date(from: message["readAt"]?.stringValue ?? "") + } - if model.inboxConfig != nil { - // crude way to refresh when new inbox, updated readAt, updated inbox title/subtite - // may cause redundant refreshes (if message with inbox updated, but not inbox itself). - fetchedWithInbox = true + if model.sentAt == nil { + model.sentAt = dateParser.date(from: message["sentAt"]?.stringValue ?? "") + } - let inbox = model.inboxConfig! + model.content = ObjcJSON(json: message["content"] ?? .null) + model.data = ObjcJSON(json: message["data"] ?? .null) + model.badgeConfig = ObjcJSON(json: message["badge"] ?? .null) + model.inboxConfig = ObjcJSON(json: message["inbox"] ?? .null) - model.inboxFrom = dateParser.date(from: inbox["from"] as? String ?? "") as NSDate? - model.inboxTo = dateParser.date(from: inbox["to"] as? String ?? "") as NSDate? - } + if let inbox = model.inboxConfig?.toGenericJSON() { + model.inboxFrom = dateParser.date(from: inbox["from"]?.stringValue ?? "") + model.inboxTo = dateParser.date(from: inbox["to"]?.stringValue ?? "") + } - let inboxDeletedAt = message["inboxDeletedAt"] as? String - if inboxDeletedAt != nil { - model.inboxConfig = nil - model.inboxFrom = nil - model.inboxTo = nil - if model.dismissedAt == nil { - model.dismissedAt = dateParser.date(from: inboxDeletedAt!) as NSDate? + if let inboxDeletedAt = message["inboxDeletedAt"]?.stringValue { + model.inboxConfig = nil + model.inboxFrom = nil + model.inboxTo = nil + if model.dismissedAt == nil { + model.dismissedAt = dateParser.date(from: inboxDeletedAt) + } } - } - model.expiresAt = dateParser.date(from: message["expiresAt"] as? String ?? "") as NSDate? + model.expiresAt = dateParser.date(from: message["expiresAt"]?.stringValue ?? "") - if model.updatedAt.timeIntervalSince1970 > mostRecentUpdate.timeIntervalSince1970 { - mostRecentUpdate = model.updatedAt + if model.updatedAt.timeIntervalSince1970 > mostRecentUpdate.timeIntervalSince1970 { + mostRecentUpdate = model.updatedAt + } } - } - // Evict - var (idsEvicted, evictedWithInbox) = evictMessages(context: context) + // The rest of your method's code would go here... - do { - try context.save() + // Example of how you would update UserDefaults with the `mostRecentUpdate` Date: + UserDefaults.standard.set(mostRecentUpdate, forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) } catch { - print("Failed to persist messages: \(error)") - return - } - - // exceeders evicted after saving because fetchOffset is ignored when have unsaved changes - // https://stackoverflow.com/questions/10725252/possible-issue-with-fetchlimit-and-fetchoffset-in-a-core-data-query - let (exceederIdsEvicted, evictedExceedersWithInbox) = evictMessagesExceedingLimit(context: context) - if exceederIdsEvicted.count > 0 { - idsEvicted += exceederIdsEvicted - - do { - try context.save() - } catch { - print("Failed to evict exceeding messages: \(error)") - return - } - } - - for idEvicted in idsEvicted { - removeNotificationTickle(id: idEvicted) + Logger.error(error.localizedDescription) } - - UserDefaults.standard.set(mostRecentUpdate, forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) - - trackMessageDelivery(messages: messages) - - let inboxUpdated = fetchedWithInbox || evictedWithInbox || evictedExceedersWithInbox - OptimoveInApp.maybeRunInboxUpdatedHandler(inboxNeedsUpdate: inboxUpdated) } } @@ -548,7 +525,7 @@ class InAppManager { fetchRequest.includesPendingChanges = false fetchRequest.returnsObjectsAsFaults = false - let predicate = NSPredicate(format: "((presentedWhen IN %@) OR (id IN %@)) AND (dismissedAt = nil) AND (expiresAt = nil OR expiresAt > %@)", presentedWhenOptions, self.pendingTickleIds, NSDate()) + let predicate = NSPredicate(format: "((presentedWhen IN %@) OR (id IN %@)) AND (dismissedAt = nil) AND (expiresAt = nil OR expiresAt > %@)", presentedWhenOptions, self.pendingTickleIds, Date() as CVarArg) fetchRequest.predicate = predicate fetchRequest.sortDescriptors = [ @@ -619,9 +596,9 @@ class InAppManager { } if messageEntities.count == 1 { - messageEntities[0].dismissedAt = NSDate() + messageEntities[0].dismissedAt = Date() if messageEntities[0].readAt == nil { - messageEntities[0].readAt = NSDate() + messageEntities[0].readAt = Date() } } @@ -748,9 +725,9 @@ class InAppManager { messageEntities[0].inboxTo = nil messageEntities[0].inboxFrom = nil messageEntities[0].inboxConfig = nil - messageEntities[0].dismissedAt = NSDate() + messageEntities[0].dismissedAt = Date() if messageEntities[0].readAt == nil { - messageEntities[0].readAt = NSDate() + messageEntities[0].readAt = Date() } } @@ -799,7 +776,7 @@ class InAppManager { } if messageEntities.count == 1 { - messageEntities[0].readAt = NSDate() + messageEntities[0].readAt = Date() } do { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift index ec1525d8..4c345e2f 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import GenericJSON enum InAppPresented: String { case IMMEDIATELY = "immediately" @@ -11,71 +12,66 @@ enum InAppPresented: String { final class InAppMessageEntity: NSManagedObject { @NSManaged var id: Int64 - @NSManaged var updatedAt: NSDate + @NSManaged var updatedAt: Date @NSManaged var presentedWhen: String - @NSManaged var content: NSDictionary - @NSManaged var data: NSDictionary? - @NSManaged var badgeConfig: NSDictionary? - @NSManaged var inboxConfig: NSDictionary? - @NSManaged var dismissedAt: NSDate? - @NSManaged var inboxFrom: NSDate? - @NSManaged var inboxTo: NSDate? - @NSManaged var expiresAt: NSDate? - @NSManaged var readAt: NSDate? - @NSManaged var sentAt: NSDate? + @NSManaged var content: ObjcJSON + @NSManaged var data: ObjcJSON? + @NSManaged var badgeConfig: ObjcJSON? + @NSManaged var inboxConfig: ObjcJSON? + @NSManaged var dismissedAt: Date? + @NSManaged var inboxFrom: Date? + @NSManaged var inboxTo: Date? + @NSManaged var expiresAt: Date? + @NSManaged var readAt: Date? + @NSManaged var sentAt: Date? func isAvailable() -> Bool { - let availableFrom = inboxFrom as Date? - let availableTo = inboxTo as Date? - - if availableFrom != nil, availableFrom!.timeIntervalSinceNow > 0 { + if let availableFrom = inboxFrom, availableFrom.timeIntervalSinceNow > 0 { return false - } else if availableTo != nil, availableTo!.timeIntervalSinceNow < 0 { + } else if let availableTo = inboxTo, availableTo.timeIntervalSinceNow < 0 { return false } - return true } } -final class InAppMessage: NSObject { +final class InAppMessage { private(set) var id: Int64 - private(set) var updatedAt: NSDate - private(set) var content: NSDictionary - private(set) var data: NSDictionary? - private(set) var badgeConfig: NSDictionary? - private(set) var inboxConfig: NSDictionary? - private(set) var dismissedAt: NSDate? - private(set) var readAt: NSDate? - private(set) var sentAt: NSDate? + private(set) var updatedAt: Date + private(set) var content: ObjcJSON + private(set) var data: ObjcJSON? + private(set) var badgeConfig: ObjcJSON? + private(set) var inboxConfig: ObjcJSON? + private(set) var dismissedAt: Date? + private(set) var readAt: Date? + private(set) var sentAt: Date? init(entity: InAppMessageEntity) { id = Int64(entity.id) - updatedAt = entity.updatedAt.copy() as! NSDate - content = entity.content.copy() as! NSDictionary - data = entity.data?.copy() as? NSDictionary - badgeConfig = entity.badgeConfig?.copy() as? NSDictionary - inboxConfig = entity.inboxConfig?.copy() as? NSDictionary - dismissedAt = entity.dismissedAt?.copy() as? NSDate - readAt = entity.readAt?.copy() as? NSDate - sentAt = entity.sentAt?.copy() as? NSDate + updatedAt = entity.updatedAt + content = entity.content + data = entity.data + badgeConfig = entity.badgeConfig + inboxConfig = entity.inboxConfig + dismissedAt = entity.dismissedAt + readAt = entity.readAt + sentAt = entity.sentAt } - override func isEqual(_ object: Any?) -> Bool { - if let other = object as? InAppMessage { - return id == other.id - } - - return super.isEqual(object) + static func == (lhs: InAppMessage, rhs: InAppMessage) -> Bool { + return lhs.id == rhs.id } - override var hash: Int { - id.hashValue + func hash(into hasher: inout Hasher) { + hasher.combine(id) } } +// Ensure that the InAppMessage conforms to Equatable and Hashable to provide the isEqual and hash functionality. +extension InAppMessage: Equatable, Hashable {} + public struct InAppButtonPress { - public let deepLinkData: [AnyHashable: Any] + public let deepLinkData: JSON public let messageId: Int64 - public let messageData: NSDictionary? + public let messageData: JSON? } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift index 21926bac..b69fd3e0 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift @@ -1,5 +1,6 @@ // Copyright © 2022 Optimove. All rights reserved. +import GenericJSON import OptimoveCore import StoreKit import UIKit @@ -145,33 +146,40 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega defer { messageQueueLock.signal() } + do { + if messageQueue.count == 0 || displayMode == .paused { + DispatchQueue.main.async { + self.destroyViews() + } - if messageQueue.count == 0 || displayMode == .paused { - DispatchQueue.main.async { - self.destroyViews() + return } - return - } - - currentMessage = (messageQueue[0] as! InAppMessage) + currentMessage = (messageQueue[0] as! InAppMessage) - var ready = false + var ready = false - runOnMainThreadSync { - initViews() - self.loadingSpinner?.startAnimating() - ready = self.webViewReady - } + runOnMainThreadSync { + initViews() + self.loadingSpinner?.startAnimating() + ready = self.webViewReady + } - guard ready else { - return - } + guard ready else { + return + } - let content = NSMutableDictionary(dictionary: currentMessage!.content) - content["region"] = Optimobile.sharedInstance.config.region.rawValue + let content = try currentMessage! + .content + .toGenericJSON() + .merging(with: [ + JSON(["region": Optimobile.sharedInstance.config.region.rawValue]) + ]) - postClientMessage(type: "PRESENT_MESSAGE", data: content) + postClientMessage(type: "PRESENT_MESSAGE", data: content) + } catch { + Logger.error(error.localizedDescription) + } } func handleMessageClosed() { @@ -295,7 +303,9 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega } // Spinner - let loadingSpinner = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.gray) + let loadingSpinner = UIActivityIndicatorView( + style: UIActivityIndicatorView.Style.medium + ) self.loadingSpinner = loadingSpinner loadingSpinner.translatesAutoresizingMaskIntoConstraints = true @@ -333,17 +343,19 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega webViewReady = false } - func postClientMessage(type: String, data: Any?) { + func postClientMessage(type: String, data: JSON?) { guard let webView = webView else { return } do { - let msg: [String: Any] = ["type": type, "data": data != nil ? data! : NSNull()] - let json: Data = try JSONSerialization.data(withJSONObject: msg, options: []) - - let jsonMsg = String(data: json, encoding: .utf8) - let evalString = String(format: "postHostMessage(%@);", jsonMsg!) + let message = try JSON([ + [ + "type": type, + "data": data ?? .null + ] + ]) + let evalString = String(format: "postHostMessage(%@);", message.debugDescription) webView.evaluateJavaScript(evalString, completionHandler: nil) } catch { @@ -352,34 +364,37 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega } func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { - if message.name != "inAppHost" { - return - } + do { + if message.name != "inAppHost" { + return + } - let body = message.body as! NSDictionary - let type = body["type"] as! String + let body = try JSON(message.body) + let type = body["type"]?.stringValue - if type == "READY" { - runOnMainThreadSync { - self.webViewReady = true - } + if type == "READY" { + runOnMainThreadSync { + self.webViewReady = true + } - presentFromQueue() - } else if type == "MESSAGE_OPENED" { - loadingSpinner?.stopAnimating() - Optimobile.sharedInstance.inAppManager.handleMessageOpened(message: currentMessage!) - } else if type == "MESSAGE_CLOSED" { - handleMessageClosed() - } else if type == "EXECUTE_ACTIONS" { - guard let body = message.body as? [AnyHashable: Any], - let data = body["data"] as? [AnyHashable: Any], - let actions = data["actions"] as? [NSDictionary] - else { - return + presentFromQueue() + } else if type == "MESSAGE_OPENED" { + loadingSpinner?.stopAnimating() + Optimobile.sharedInstance.inAppManager.handleMessageOpened(message: currentMessage!) + } else if type == "MESSAGE_CLOSED" { + handleMessageClosed() + } else if type == "EXECUTE_ACTIONS" { + guard let data = body["data"], + let actions = data["actions"] + else { + return + } + try handleActions(actions: actions) + } else { + print("Unknown message: \(message.body)") } - handleActions(actions: actions) - } else { - print("Unknown message: \(message.body)") + } catch { + Logger.error(error.localizedDescription) } } @@ -417,23 +432,27 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega cancelCurrentPresentationQueue(waitForViewCleanup: false) } - func handleActions(actions: [NSDictionary]) { + func handleActions(actions: JSON) throws { if let message = currentMessage { var hasClose = false var conversionEvent: String? - var conversionEventData: [String: Any]? - var userAction: NSDictionary? - - for action in actions { - let type = InAppAction(rawValue: action["type"] as! String)! - let data = action["data"] as? [AnyHashable: Any] + var conversionEventData: JSON? + var userAction: JSON? + + for action in try unwrap(actions.arrayValue) { + guard let type = action["type"]?.stringValue, + let inAppAction = InAppAction(rawValue: type) + else { + continue + } + let data = action["data"] - switch type { + switch inAppAction { case .CLOSE_MESSAGE: hasClose = true case .TRACK_EVENT: - conversionEvent = data?["eventType"] as? String - conversionEventData = data?["data"] as? [String: Any] + conversionEvent = data?["eventType"]?.stringValue + conversionEventData = data?["data"] default: userAction = action } @@ -445,7 +464,10 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega } if let conversionEvent = conversionEvent { - Optimobile.trackEventImmediately(eventType: conversionEvent, properties: conversionEventData) + Optimobile.trackEventImmediately( + eventType: conversionEvent, + properties: conversionEventData?.objectValue + ) } if userAction != nil { @@ -455,8 +477,8 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega } } - func handleUserAction(message: InAppMessage, userAction: NSDictionary) { - let type = userAction["type"] as! String + func handleUserAction(message: InAppMessage, userAction: JSON) { + let type = userAction["type"]?.stringValue if type == InAppAction.PROMPT_PUSH_PERMISSION.rawValue { Optimobile.pushRequestDeviceToken() @@ -465,12 +487,22 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega return } DispatchQueue.main.async { - let data = userAction.value(forKeyPath: "data.deepLink") as? [AnyHashable: Any] ?? [:] - let buttonPress = InAppButtonPress(deepLinkData: data, messageId: message.id, messageData: message.data) + guard let data = userAction.queryKeyPath(["data", "deepLink"]) else { + Logger.error("Deep link action missing deep link data") + return + } + let buttonPress = InAppButtonPress( + deepLinkData: data, + messageId: message.id, + messageData: message.data?.toGenericJSON() + ) Optimobile.sharedInstance.config.inAppDeepLinkHandlerBlock?(buttonPress) } } else if type == InAppAction.OPEN_URL.rawValue { - guard let url = URL(string: userAction.value(forKeyPath: "data.url") as! String) else { + guard let urlString = userAction.queryKeyPath(["data", "url"])?.stringValue, + let url = URL(string: urlString) + else { + Logger.error("Open URL action missing URL data") return } @@ -487,7 +519,7 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega if #available(iOS 10.3.0, *) { SKStoreReviewController.requestReview() } else { - NSLog("Requesting a rating not supported on this iOS version") + Logger.error("Requesting a rating not supported on this iOS version") } } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift index e2f4f9db..ac37d209 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift @@ -4,7 +4,7 @@ import CoreData import Foundation import OptimoveCore -class InAppInboxItem { +final class InAppInboxItem { private(set) var id: Int64 private(set) var title: String private(set) var subtitle: String @@ -12,7 +12,7 @@ class InAppInboxItem { private(set) var availableTo: Date? private(set) var dismissedAt: Date? private(set) var sentAt: Date - private(set) var data: NSDictionary? + private(set) var data: ObjcJSON? private var readAt: Date? private var imagePath: String? @@ -20,33 +20,37 @@ class InAppInboxItem { let mediaHelper: MediaHelper init(entity: InAppMessageEntity, mediaHelper: MediaHelper) { - id = Int64(entity.id) + id = entity.id self.mediaHelper = mediaHelper - let inboxConfig = entity.inboxConfig?.copy() as! [String: Any] + guard let inboxConfig = entity.inboxConfig else { + fatalError("InboxConfig is not a [String: Any] dictionary") + } + + guard let titleString = inboxConfig["title"]?.string, + let subtitleString = inboxConfig["subtitle"]?.string + else { + fatalError("Title or subtitle is not a String") + } - title = inboxConfig["title"] as! String - subtitle = inboxConfig["subtitle"] as! String + title = titleString + subtitle = subtitleString - availableFrom = entity.inboxFrom?.copy() as? Date - availableTo = entity.inboxTo?.copy() as? Date - dismissedAt = entity.dismissedAt?.copy() as? Date - readAt = entity.readAt?.copy() as? Date - data = entity.data?.copy() as? NSDictionary + availableFrom = entity.inboxFrom + availableTo = entity.inboxTo + dismissedAt = entity.dismissedAt + readAt = entity.readAt + data = entity.data - if let sentAtNonNil = entity.sentAt?.copy() as? Date { - sentAt = sentAtNonNil - } else { - sentAt = entity.updatedAt.copy() as! Date - } + sentAt = entity.sentAt ?? entity.updatedAt - imagePath = inboxConfig["imagePath"] as? String + imagePath = inboxConfig["imagePath"]?.string } func isAvailable() -> Bool { - if availableFrom != nil, availableFrom!.timeIntervalSinceNow > 0 { + if let availableFrom = availableFrom, availableFrom.timeIntervalSinceNow > 0 { return false - } else if availableTo != nil, availableTo!.timeIntervalSinceNow < 0 { + } else if let availableTo = availableTo, availableTo.timeIntervalSinceNow < 0 { return false } @@ -62,11 +66,8 @@ class InAppInboxItem { } func getImageUrl(width: UInt) -> URL? { - if let imagePathNotNil = imagePath { - return try? mediaHelper.getCompletePictureUrl( - pictureUrlString: imagePathNotNil, - width: width - ) + if let imagePath = imagePath { + return try? mediaHelper.getCompletePictureUrl(pictureUrlString: imagePath, width: width) } return nil diff --git a/OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift index d4bff2f0..c888b100 100644 --- a/OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift +++ b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Optimove. All rights reserved. import Foundation +import GenericJSON import OptimoveCore struct OptistreamEvent: Codable { diff --git a/OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift index 56454ca1..d0de5121 100644 --- a/OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift @@ -1,6 +1,7 @@ // Copyright © 2020 Optimove. All rights reserved. import Foundation +import GenericJSON /// Builds an Optistream event from internal event type. /// The `delivery_event` do not use this class in reason of memory consuption under Notification Service Extention. diff --git a/Package.resolved b/Package.resolved index 102fc929..65bc736b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { "object": { "pins": [ + { + "package": "GenericJSON", + "repositoryURL": "https://github.com/iwill/generic-json-swift", + "state": { + "branch": null, + "revision": "0a06575f4038b504e78ac330913d920f1630f510", + "version": "2.0.2" + } + }, { "package": "Mocker", "repositoryURL": "https://github.com/WeTransfer/Mocker", diff --git a/Package.swift b/Package.swift index 73b21dea..b4c584c1 100644 --- a/Package.swift +++ b/Package.swift @@ -24,12 +24,14 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.1"), + .package(url: "https://github.com/iwill/generic-json-swift", from: "2.0.2"), ], targets: [ .target( name: "OptimoveSDK", dependencies: [ "OptimoveCore", + .product(name: "GenericJSON", package: "generic-json-swift"), ], path: "OptimoveSDK/Sources" ),