diff --git a/OptimobileShared/AppGroupsHelper.swift b/OptimobileShared/AppGroupsHelper.swift deleted file mode 100644 index 521bbd66..00000000 --- a/OptimobileShared/AppGroupsHelper.swift +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -public enum AppGroupConfig { - public static var suffix: String = ".optimove" -} - -enum AppGroupsHelper { - static func isKumulosAppGroupDefined() -> Bool { - let containerUrl = getSharedContainerPath() - - return containerUrl != nil - } - - static func getSharedContainerPath() -> URL? { - return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: getKumulosGroupName()) - } - - static func getKumulosGroupName() -> String { - var targetBundle = Bundle.main - if targetBundle.bundleURL.pathExtension == "appex" { - let url = targetBundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent() - if let mainBundle = Bundle(url: url) { - targetBundle = mainBundle - } else { - print("AppGroupsHelper: Error, could not obtain main bundle from extension!") - } - } - - return "group.\(targetBundle.bundleIdentifier!)\(AppGroupConfig.suffix)" - } -} diff --git a/OptimobileShared/Extensions/Notifications.swift b/OptimobileShared/Extensions/Notifications.swift deleted file mode 100644 index dfd89739..00000000 --- a/OptimobileShared/Extensions/Notifications.swift +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright © 2023 Optimove. All rights reserved. - -import Foundation - -extension Notification.Name { - static let optimobileInializationFinished = Notification.Name("optimobileInializationFinished") -} diff --git a/OptimobileShared/KeyValPersistenceHelper.swift b/OptimobileShared/KeyValPersistenceHelper.swift deleted file mode 100644 index fe1365d2..00000000 --- a/OptimobileShared/KeyValPersistenceHelper.swift +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -protocol KeyValPersistent { - static func set(_ value: Any?, forKey: String) - static func object(forKey: String) -> Any? - static func removeObject(forKey: String) -} - -enum KeyValPersistenceHelper { - static func maybeMigrateUserDefaultsToAppGroups() { - let standardDefaults = UserDefaults.standard - let haveMigratedKey: String = OptimobileUserDefaultsKey.MIGRATED_TO_GROUPS.rawValue - if !AppGroupsHelper.isKumulosAppGroupDefined() { - standardDefaults.set(false, forKey: haveMigratedKey) - return - } - - guard let groupDefaults = UserDefaults(suiteName: AppGroupsHelper.getKumulosGroupName()) else { return } - if groupDefaults.bool(forKey: haveMigratedKey), standardDefaults.bool(forKey: haveMigratedKey) { - return - } - - let defaultsAsDict: [String: Any] = standardDefaults.dictionaryRepresentation() - for key in OptimobileUserDefaultsKey.sharedKeys { - groupDefaults.set(defaultsAsDict[key.rawValue], forKey: key.rawValue) - } - - standardDefaults.set(true, forKey: haveMigratedKey) - groupDefaults.set(true, forKey: haveMigratedKey) - } - - fileprivate static func getUserDefaults() -> UserDefaults { - if !AppGroupsHelper.isKumulosAppGroupDefined() { - return UserDefaults.standard - } - - if let suiteUserDefaults = UserDefaults(suiteName: AppGroupsHelper.getKumulosGroupName()) { - return suiteUserDefaults - } - - return UserDefaults.standard - } -} - -extension KeyValPersistenceHelper: KeyValPersistent { - static func set(_ value: Any?, forKey: String) { - getUserDefaults().set(value, forKey: forKey) - } - - static func object(forKey: String) -> Any? { - return getUserDefaults().object(forKey: forKey) - } - - static func removeObject(forKey: String) { - getUserDefaults().removeObject(forKey: forKey) - } -} diff --git a/OptimobileShared/MediaHelper.swift b/OptimobileShared/MediaHelper.swift deleted file mode 100644 index 066077b3..00000000 --- a/OptimobileShared/MediaHelper.swift +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -enum MediaHelper { - /// Use ``Region.US`` as fallback region. - static let mediaResizerBaseUrl: String = "https://i-us-east-1.app.delivery" - - static func getCompletePictureUrl(pictureUrl: String, width: UInt) -> URL? { - if pictureUrl.hasPrefix("https://") || pictureUrl.hasPrefix("http://") { - return URL(string: pictureUrl) - } - - let baseUrl = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) as? String ?? mediaResizerBaseUrl - - let completeString = "\(baseUrl)/\(width)x/\(pictureUrl)" - return URL(string: completeString) - } -} diff --git a/OptimobileShared/OptimobileHelper.swift b/OptimobileShared/OptimobileHelper.swift deleted file mode 100644 index 11594019..00000000 --- a/OptimobileShared/OptimobileHelper.swift +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -let KS_MESSAGE_TYPE_PUSH = 1 - -enum OptimobileHelper { - private static let installIdLock = DispatchSemaphore(value: 1) - static let userIdLock = DispatchSemaphore(value: 1) - - static var installId: String { - installIdLock.wait() - defer { - installIdLock.signal() - } - - if let existingID = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.INSTALL_UUID.rawValue) { - return existingID as! String - } - - let newID = UUID().uuidString - KeyValPersistenceHelper.set(newID, forKey: OptimobileUserDefaultsKey.INSTALL_UUID.rawValue) - - return newID - } - - /** - Returns the identifier for the user currently associated with the Kumulos installation record - - If no user is associated, it returns the Kumulos installation ID - */ - static var currentUserIdentifier: String { - userIdLock.wait() - defer { userIdLock.signal() } - if let userId = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) as! String? { - return userId - } - - return OptimobileHelper.installId - } - - static func getBadgeFromUserInfo(userInfo: [AnyHashable: Any]) -> NSNumber? { - let custom = userInfo["custom"] as? [AnyHashable: Any] - let aps = userInfo["aps"] as? [AnyHashable: Any] - - if custom == nil || aps == nil { - return nil - } - - let incrementBy: NSNumber? = custom!["badge_inc"] as? NSNumber - let badge: NSNumber? = aps!["badge"] as? NSNumber - - if badge == nil { - return nil - } - - var newBadge: NSNumber? = badge - if let incrementBy = incrementBy, let currentVal = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) as? NSNumber { - newBadge = NSNumber(value: currentVal.intValue + incrementBy.intValue) - - if newBadge!.intValue < 0 { - newBadge = 0 - } - } - - return newBadge - } -} diff --git a/OptimobileShared/PendingNotification.swift b/OptimobileShared/PendingNotification.swift deleted file mode 100644 index b4003d74..00000000 --- a/OptimobileShared/PendingNotification.swift +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -struct PendingNotification: Codable { - let id: Int - let deliveredAt: Date - let identifier: String -} diff --git a/OptimoveCore/Sources/Assets/.gitkeep b/OptimoveCore/Sources/Assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/OptimoveCore/Sources/Classes/Configurations/Tenant/RealtimeMetaData.swift b/OptimoveCore/Sources/Classes/Configurations/Tenant/RealtimeMetaData.swift deleted file mode 100644 index e473dcf1..00000000 --- a/OptimoveCore/Sources/Classes/Configurations/Tenant/RealtimeMetaData.swift +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright © 2017 Optimove. All rights reserved. - -import Foundation - -public struct TenantRealtimeConfig: Codable, Equatable { - public var realtimeGateway: URL - - public init(realtimeGateway: URL) { - self.realtimeGateway = realtimeGateway - } -} diff --git a/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift b/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift index 9d3548e2..3f96df9b 100644 --- a/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift +++ b/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift @@ -2,12 +2,13 @@ import Foundation -public extension Bundle { +extension Bundle { + /// Returns the bundle containing the host app. /// https://stackoverflow.com/a/27849695 - static func hostAppBundle() -> Bundle? { + static func hostAppBundle() -> Bundle { let mainBundle = Bundle.main if mainBundle.bundleURL.pathExtension == "appex" { - // Peel off two directory levels - SOME_APP.app/PlugIns/SOME_APP_EXTENSION.appex + // Peel off two directory levels - APP.app/PlugIns/APP_EXTENSION.appex let url = mainBundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent() if let hostBundle = Bundle(url: url) { return hostBundle @@ -15,4 +16,14 @@ public extension Bundle { } return mainBundle } + + /// Returns the bundle identifier of the host app. + static var hostAppBundleIdentifier: String { + return hostAppBundle().bundleIdentifier! + } + + /// Returns the app group identifier for the SDK app. + static var optimoveAppGroupIdentifier: String { + return "group.\(hostAppBundleIdentifier).optimove" + } } diff --git a/OptimoveCore/Sources/Classes/Extension/Result+Successful.swift b/OptimoveCore/Sources/Classes/Extension/Result+Successful.swift deleted file mode 100644 index af15ed09..00000000 --- a/OptimoveCore/Sources/Classes/Extension/Result+Successful.swift +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright © 2019 Optimove. All rights reserved. - -public extension Result { - var isSuccessful: Bool { - do { - _ = try get() - return true - } catch { - return false - } - } -} diff --git a/OptimoveCore/Sources/Classes/Extension/utsname+DeviceModel.swift b/OptimoveCore/Sources/Classes/Extension/utsname+DeviceModel.swift index 282b89b4..f0d34ae4 100644 --- a/OptimoveCore/Sources/Classes/Extension/utsname+DeviceModel.swift +++ b/OptimoveCore/Sources/Classes/Extension/utsname+DeviceModel.swift @@ -2,7 +2,7 @@ import Foundation -public extension utsname { +extension utsname { var deviceModel: String { var systemInfo = self uname(&systemInfo) diff --git a/OptimoveCore/Sources/Classes/MediaHelper.swift b/OptimoveCore/Sources/Classes/MediaHelper.swift new file mode 100644 index 00000000..161ec0cd --- /dev/null +++ b/OptimoveCore/Sources/Classes/MediaHelper.swift @@ -0,0 +1,36 @@ +// Copyright © 2022 Optimove. All rights reserved. + +import Foundation + +public struct MediaHelper { + enum Error: LocalizedError { + case noMediaUrlFound + case invalidPictureUrl(String) + } + + let storage: KeyValueStorage + + public init(storage: KeyValueStorage) { + self.storage = storage + } + + public func getCompletePictureUrl(pictureUrlString: String, width: UInt) throws -> URL { + if pictureUrlString.hasPrefix("https://") || pictureUrlString.hasPrefix("http://") { + guard let url = URL(string: pictureUrlString) else { + throw Error.invalidPictureUrl(pictureUrlString) + } + return url + } + + guard let mediaUrl: String = storage[.mediaURL] else { + throw Error.noMediaUrlFound + } + + let urlString = "\(mediaUrl)/\(width)x/\(pictureUrlString)" + guard let url = URL(string: urlString) else { + throw Error.invalidPictureUrl(urlString) + } + + return url + } +} diff --git a/OptimobileShared/OptimobileEvent.swift b/OptimoveCore/Sources/Classes/OptimobileEvent.swift similarity index 95% rename from OptimobileShared/OptimobileEvent.swift rename to OptimoveCore/Sources/Classes/OptimobileEvent.swift index 076455cf..77d3555e 100644 --- a/OptimobileShared/OptimobileEvent.swift +++ b/OptimoveCore/Sources/Classes/OptimobileEvent.swift @@ -1,6 +1,6 @@ // Copyright © 2023 Optimove. All rights reserved. -enum OptimobileEvent: String, Codable { +public enum OptimobileEvent: String, Codable { case DEEP_LINK_MATCHED = "k.deepLink.matched" case DEVICE_UNSUBSCRIBED = "k.push.deviceUnsubscribed" case ENGAGE_BEACON_ENTERED_PROXIMITY = "k.engage.beaconEnteredProximity" diff --git a/OptimoveCore/Sources/Classes/OptimobileHelper.swift b/OptimoveCore/Sources/Classes/OptimobileHelper.swift new file mode 100644 index 00000000..2e90014d --- /dev/null +++ b/OptimoveCore/Sources/Classes/OptimobileHelper.swift @@ -0,0 +1,55 @@ +// Copyright © 2022 Optimove. All rights reserved. + +import Foundation + +public struct OptimobileHelper { + static let installIdLock = DispatchSemaphore(value: 1) + public static let userIdLock = DispatchSemaphore(value: 1) + + let storage: KeyValueStorage + + public init(storage: KeyValueStorage) { + self.storage = storage + } + + public func installId() -> String { + OptimobileHelper.installIdLock.wait() + defer { + OptimobileHelper.installIdLock.signal() + } + + if let existingID: String = storage[.installUUID] { + return existingID + } + + let newID = UUID().uuidString + storage.set(value: newID, key: .installUUID) + return newID + } + + /** + Returns the identifier for the user currently associated with the Kumulos installation record + + If no user is associated, it returns the Kumulos installation ID + */ + public func currentUserIdentifier() -> String { + OptimobileHelper.userIdLock.wait() + defer { OptimobileHelper.userIdLock.signal() } + if let userId: String = storage[.userID] { + return userId + } + + return installId() + } + + public func getBadge(notification: PushNotification) -> Int? { + if let incrementBy = notification.badgeIncrement, + let current: Int = storage[.badgeCount] + { + let badge = current + incrementBy + return badge < 0 ? 0 : badge + } + + return notification.aps.badge + } +} diff --git a/OptimobileShared/OptimobileUserDefaultsKey.swift b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift similarity index 85% rename from OptimobileShared/OptimobileUserDefaultsKey.swift rename to OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift index f26e7889..824bc186 100644 --- a/OptimobileShared/OptimobileUserDefaultsKey.swift +++ b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift @@ -2,7 +2,7 @@ import Foundation -enum OptimobileUserDefaultsKey: String { +public enum OptimobileUserDefaultsKey: String, CaseIterable { case REGION = "KumulosEventsRegion" case MEDIA_BASE_URL = "KumulosMediaBaseUrl" case INSTALL_UUID = "KumulosUUID" @@ -10,9 +10,6 @@ enum OptimobileUserDefaultsKey: String { case BADGE_COUNT = "KumulosBadgeCount" case PENDING_NOTIFICATIONS = "KumulosPendingNotifications" case PENDING_ANALYTICS = "KumulosPendingAnalytics" - - // exist only in standard defaults for app - case MIGRATED_TO_GROUPS = "KumulosDidMigrateToAppGroups" case IN_APP_LAST_SYNCED_AT = "KumulosMessagesLastSyncedAt" case IN_APP_MOST_RECENT_UPDATED_AT = "KumulosInAppMostRecentUpdatedAt" case IN_APP_CONSENTED = "KumulosInAppConsented" diff --git a/OptimoveCore/Sources/Classes/PendingNotification.swift b/OptimoveCore/Sources/Classes/PendingNotification.swift new file mode 100644 index 00000000..4afa9cfe --- /dev/null +++ b/OptimoveCore/Sources/Classes/PendingNotification.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Optimove. All rights reserved. + +import Foundation + +public struct PendingNotification: Codable { + public let id: Int + public let deliveredAt: Date + public let identifier: String + + public init(id: Int, deliveredAt: Date = .init(), identifier: String) { + self.id = id + self.deliveredAt = deliveredAt + self.identifier = identifier + } +} diff --git a/OptimobileShared/PendingNotificationHelper.swift b/OptimoveCore/Sources/Classes/PendingNotificationHelper.swift similarity index 67% rename from OptimobileShared/PendingNotificationHelper.swift rename to OptimoveCore/Sources/Classes/PendingNotificationHelper.swift index dce70fe8..405de69c 100644 --- a/OptimobileShared/PendingNotificationHelper.swift +++ b/OptimoveCore/Sources/Classes/PendingNotificationHelper.swift @@ -2,8 +2,14 @@ import Foundation -enum PendingNotificationHelper { - static func remove(id: Int) { +public struct PendingNotificationHelper { + let storage: KeyValueStorage + + public init(storage: KeyValueStorage) { + self.storage = storage + } + + public func remove(id: Int) { var pendingNotifications = readAll() if let i = pendingNotifications.firstIndex(where: { $0.id == id }) { @@ -13,7 +19,7 @@ enum PendingNotificationHelper { } } - static func remove(identifier: String) { + public func remove(identifier: String) { var pendingNotifications = readAll() if let i = pendingNotifications.firstIndex(where: { $0.identifier == identifier }) { @@ -23,10 +29,10 @@ enum PendingNotificationHelper { } } - static func readAll() -> [PendingNotification] { + public func readAll() -> [PendingNotification] { var pendingNotifications = [PendingNotification]() - if let data = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.PENDING_NOTIFICATIONS.rawValue), - let decoded = try? JSONDecoder().decode([PendingNotification].self, from: data as! Data) + if let data: Data = storage[.pendingNotifications], + let decoded = try? JSONDecoder().decode([PendingNotification].self, from: data) { pendingNotifications = decoded } @@ -34,7 +40,7 @@ enum PendingNotificationHelper { return pendingNotifications } - static func add(notification: PendingNotification) { + public func add(notification: PendingNotification) { var pendingNotifications = readAll() if let _ = pendingNotifications.firstIndex(where: { $0.id == notification.id }) { @@ -46,9 +52,9 @@ enum PendingNotificationHelper { save(pendingNotifications: pendingNotifications) } - fileprivate static func save(pendingNotifications: [PendingNotification]) { + func save(pendingNotifications: [PendingNotification]) { if let data = try? JSONEncoder().encode(pendingNotifications) { - KeyValPersistenceHelper.set(data, forKey: OptimobileUserDefaultsKey.PENDING_NOTIFICATIONS.rawValue) + storage.set(value: data, key: .pendingNotifications) } } } diff --git a/OptimoveCore/Sources/Classes/PushNotification.swift b/OptimoveCore/Sources/Classes/PushNotification.swift new file mode 100644 index 00000000..03507d62 --- /dev/null +++ b/OptimoveCore/Sources/Classes/PushNotification.swift @@ -0,0 +1,117 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +/// Represents a push notification received from the server. +public struct PushNotification: Decodable { + public struct Aps: Decodable { + public struct Alert: Decodable { + public let title: String? + public let body: String? + } + + public let alert: Alert? + public let badge: Int? + public let sound: String? + /// The background notification flag. To perform a silent background update, specify the value 1 and don’t include the alert, badge, or sound keys in your payload. If this key is present with a value of 1, the system attempts to initialize your app in the background so that it can make updates to its user interface. If the app is already running in the foreground, this key has no effect. + public let isBackground: Bool + /// The notification service app extension flag. If the value is 1, the system passes the notification to your notification service app extension before delivery. Use your extension to modify the notification’s content. + public let isExtension: Bool + + private enum CodingKeys: String, CodingKey { + case alert + case badge + case sound + case isBackground = "content-available" + case isExtension = "mutable-content" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.alert = try container.decodeIfPresent(Alert.self, forKey: .alert) + self.badge = try container.decodeIfPresent(Int.self, forKey: .badge) + self.sound = try container.decodeIfPresent(String.self, forKey: .sound) + let isBackground = try container.decodeIfPresent(Int.self, forKey: .isBackground) + self.isBackground = isBackground == 1 + let isExtension = try container.decodeIfPresent(Int.self, forKey: .isExtension) + self.isExtension = isExtension == 1 + } + } + + public struct Attachment: Decodable { + public let pictureUrl: String? + } + + public struct Button: Decodable { + public struct Icon: Decodable { + public enum IconType: String, Decodable { + case custom + case system + } + + public let id: String + public let type: IconType + } + + public let id: String + public let icon: Icon? + public let text: String + } + + public struct Data: Decodable { + public let id: Int + + private enum CodingKeys: String, CodingKey { + case id + case data + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) + self.id = try data.decode(Int.self, forKey: CodingKeys.id) + } + } + + public let aps: Aps + public let attachment: PushNotification.Attachment? + /// Optimove badge + public let badgeIncrement: Int? + public let buttons: [PushNotification.Button]? + public let deeplink: PushNotification.Data? + public let message: PushNotification.Data + public let url: URL? + + private enum CodingKeys: String, CodingKey { + case a + case aps + case attachments + case badgeIncrement = "badge_inc" + case buttons = "k.buttons" + case custom + case deeplink = "k.deepLink" + case message = "k.message" + case u + } + + public init(userInfo: [AnyHashable: Any]) throws { + let data = try JSONSerialization.data(withJSONObject: userInfo) + let decoder = JSONDecoder() + self = try decoder.decode(PushNotification.self, from: data) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.aps = try container.decode(Aps.self, forKey: .aps) + self.attachment = try container.decodeIfPresent(Attachment.self, forKey: .attachments) + + let custom = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .custom) + self.badgeIncrement = try custom.decodeIfPresent(Int.self, forKey: .badgeIncrement) + self.url = try custom.decodeIfPresent(URL.self, forKey: .u) + + let a = try custom.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) + self.buttons = try a.decodeIfPresent([Button].self, forKey: .buttons) + self.deeplink = try a.decodeIfPresent(PushNotification.Data.self, forKey: .deeplink) + self.message = try a.decode(PushNotification.Data.self, forKey: .message) + } +} diff --git a/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift new file mode 100644 index 00000000..21947591 --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift @@ -0,0 +1,19 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public extension FileManager { + static func optimoveAppGroupURL() throws -> URL { + let suiteName = Bundle.optimoveAppGroupIdentifier + guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) else { + let message = """ + Unable to initialize UserDefault with suit name "\(suiteName)". + Highly possible that the client forgot to add the app group as described in the documentation. + Link: https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/SDK-Setup-Capabilities + """ + assertionFailure(message) + throw GuardError.custom(message) + } + return url + } +} diff --git a/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift new file mode 100644 index 00000000..5a172881 --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift @@ -0,0 +1,150 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +/// The enum used as keys for storage values. +public enum StorageKey: String, CaseIterable { + case installationID + case customerID + case configurationEndPoint + case initialVisitorId + case tenantToken + case visitorID + case version + case deviceResolutionWidth + case deviceResolutionHeight + case advertisingIdentifier + case migrationVersions /// For storing a migration history + case firstRunTimestamp + case pushNotificationChannels + case optitrackEndpoint + case tenantID + case userEmail + case siteID /// Legacy: See tenantID + case settingUserSuccess + case firstVisitTimestamp /// Legacy + /// Kumulos + case region + case mediaURL + case installUUID + case userID + case badgeCount + case pendingNotifications + case pendingAnaltics + case inAppLastSyncedAt + case inAppMostRecentUpdateAt + case inAppConsented + case dynamicCategory + case deferredLinkChecked = "KUMULOS_DDL_CHECKED" + + public static let inMemoryValues: Set = [.tenantToken, .version] + public static let appGroupValues: Set = [ + .badgeCount, + .dynamicCategory, + .installUUID, + .mediaURL, + .pendingNotifications, + .userID, + ] +} + +/// The protocol used as convenience accessor to storage values. +public protocol StorageValue { + var installationID: String? { get set } + var customerID: String? { get set } + var configurationEndPoint: URL? { get set } + var initialVisitorId: String? { get set } + var tenantToken: String? { get set } + var visitorID: String? { get set } + var version: String? { get set } + var deviceResolutionWidth: Float? { get set } + var deviceResolutionHeight: Float? { get set } + var advertisingIdentifier: String? { get set } + var optitrackEndpoint: URL? { get set } + var tenantID: Int? { get set } + var userEmail: String? { get set } + /// Legacy: See tenantID + var siteID: Int? { get set } + var isSettingUserSuccess: Bool? { get set } + /// Legacy. Use `firstRunTimestamp` instead + var firstVisitTimestamp: Int64? { get set } + + func getConfigurationEndPoint() throws -> URL + func getCustomerID() throws -> String + func getInitialVisitorId() throws -> String + func getTenantToken() throws -> String + func getVisitorID() throws -> String + func getVersion() throws -> String + func getDeviceResolutionWidth() throws -> Float + func getDeviceResolutionHeight() throws -> Float + /// Called when a migration is finished for the version. + mutating func finishedMigration(to version: String) + /// Use for checking if a migration was applied for the version. + func isAlreadyMigrated(to version: String) -> Bool + func getUserEmail() throws -> String + func getSiteID() throws -> Int +} + +/// The protocol used for convenience implementation of any storage technology below this protocol. +public protocol KeyValueStorage { + func set(value: Any?, key: StorageKey) + func value(for: StorageKey) -> Any? + subscript(_: StorageKey) -> T? { get set } +} + +/// ``UserDefaults`` uses as persistent ``KeyValueStorage``. +extension UserDefaults: KeyValueStorage { + public func set(value: Any?, key: StorageKey) { + set(value, forKey: key.rawValue) + } + + public func value(for key: StorageKey) -> Any? { + return value(forKey: key.rawValue) + } + + public subscript(key: StorageKey) -> T? { + get { + return value(for: key) as? T + } + set { + set(value: newValue, key: key) + } + } +} + +/// ``InMemoryStorage`` uses as in-memory ``KeyValueStorage``. +public final class InMemoryStorage: KeyValueStorage { + private var storage = [StorageKey: Any]() + private let queue = DispatchQueue(label: "com.optimove.sdk.inmemorystorage", attributes: .concurrent) + + public init() {} + + public func set(value: Any?, key: StorageKey) { + queue.async(flags: .barrier) { [self] in + storage[key] = value + } + } + + public subscript(key: StorageKey) -> T? { + get { + var result: T? + queue.sync { + result = storage[key] as? T + } + return result + } + set { + queue.async(flags: .barrier) { [self] in + storage[key] = newValue + } + } + } + + public func value(for key: StorageKey) -> Any? { + var result: Any? + queue.sync { + result = storage[key] + } + return result + } +} diff --git a/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift new file mode 100644 index 00000000..aaf6f3d6 --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift @@ -0,0 +1,19 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public extension UserDefaults { + static func optimoveAppGroup() throws -> UserDefaults { + let suiteName = Bundle.optimoveAppGroupIdentifier + guard let userDefaults = UserDefaults(suiteName: suiteName) else { + let message = """ + Unable to initialize UserDefault with suit name "\(suiteName)". + Highly possible that the client forgot to add the app group as described in the documentation. + Link: https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/SDK-Setup-Capabilities + """ + assertionFailure(message) + throw GuardError.custom(message) + } + return userDefaults + } +} diff --git a/OptimoveCore/Tests/Resources/notification-background.json b/OptimoveCore/Tests/Resources/notification-background.json new file mode 100644 index 00000000..4c85cdf2 --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-background.json @@ -0,0 +1,19 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + }, + "content-available": 1 + }, + "custom": { + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveCore/Tests/Resources/notification-badge.json b/OptimoveCore/Tests/Resources/notification-badge.json new file mode 100644 index 00000000..f59e75df --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-badge.json @@ -0,0 +1,21 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + }, + "badge": 42, + "mutable-content": 1 + }, + "custom": { + "badge_inc": 42, + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveCore/Tests/Resources/notification-buttons.json b/OptimoveCore/Tests/Resources/notification-buttons.json new file mode 100644 index 00000000..0abe1aab --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-buttons.json @@ -0,0 +1,41 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + }, + "mutable-content": 1 + }, + "custom": { + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + }, + "k.buttons": [ + { + "id": "1", + "text": "action_1" + }, + { + "id": "2", + "text": "action_2", + "icon": { + "type": "system", + "id": "sys_icon_id" + } + }, + { + "id": "3", + "text": "action_3", + "icon": { + "type": "custom", + "id": "custom_icon_id" + } + } + ] + } + } +} diff --git a/OptimoveCore/Tests/Resources/notification-deeplink.json b/OptimoveCore/Tests/Resources/notification-deeplink.json new file mode 100644 index 00000000..dbdd2a5e --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-deeplink.json @@ -0,0 +1,24 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + } + }, + "custom": { + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + }, + "k.deepLink": { + "type": 2, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveCore/Tests/Resources/notification-image.json b/OptimoveCore/Tests/Resources/notification-image.json new file mode 100644 index 00000000..3f3f004c --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-image.json @@ -0,0 +1,22 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + }, + "mutable-content": 1 + }, + "attachments": { + "pictureUrl": "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg" + }, + "custom": { + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveCore/Tests/Resources/notification-message.json b/OptimoveCore/Tests/Resources/notification-message.json new file mode 100644 index 00000000..1bc8d635 --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-message.json @@ -0,0 +1,18 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + } + }, + "custom": { + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveCore/Tests/Resources/notification-url.json b/OptimoveCore/Tests/Resources/notification-url.json new file mode 100644 index 00000000..e57f0c19 --- /dev/null +++ b/OptimoveCore/Tests/Resources/notification-url.json @@ -0,0 +1,19 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + } + }, + "custom": { + "u": "https://www.optimove.com", + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveCore/Tests/Sources/MediaHelperTests.swift b/OptimoveCore/Tests/Sources/MediaHelperTests.swift new file mode 100644 index 00000000..ba33d984 --- /dev/null +++ b/OptimoveCore/Tests/Sources/MediaHelperTests.swift @@ -0,0 +1,29 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import OptimoveCore +import OptimoveTest +import XCTest + +final class MediaHelperTests: XCTestCase { + var mediaHelper: MediaHelper! + var storage: KeyValueStorage! + + override func setUp() { + storage = MockOptimoveStorage() + mediaHelper = MediaHelper(storage: storage) + } + + func test_getCompletePictureUrl() throws { + let pictureUrlString = "https://www.optimove.com/wp-content/uploads/2018/12/optimove-logo.png" + let url = try mediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) + XCTAssertEqual(url.absoluteString, pictureUrlString) + } + + func test_getCompletePictureUrl_withMediaUrl() throws { + let pictureUrlString = "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg" + let mediaUrl = "https://www.optimove.com" + storage.set(value: mediaUrl, key: .mediaURL) + let url = try mediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) + XCTAssertEqual(url.absoluteString, "\(mediaUrl)/100x/\(pictureUrlString)") + } +} diff --git a/OptimoveCore/Tests/Sources/PushNotificationTests.swift b/OptimoveCore/Tests/Sources/PushNotificationTests.swift new file mode 100644 index 00000000..fbea0fa0 --- /dev/null +++ b/OptimoveCore/Tests/Sources/PushNotificationTests.swift @@ -0,0 +1,75 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import OptimoveCore +import OptimoveTest +import XCTest + +final class PushNotificationTests: XCTestCase, FileAccessible { + var fileName: String = "" + + func test_decode_message() throws { + fileName = "notification-message.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + XCTAssertEqual(notification.message.id, 1) + } + + func test_decode_badge() throws { + fileName = "notification-badge.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + XCTAssertEqual(notification.badgeIncrement, 42) + } + + func test_decode_buttons() throws { + fileName = "notification-buttons.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.buttons?.count, 3) + XCTAssertEqual(notification.buttons?[0].id, "1") + XCTAssertEqual(notification.buttons?[0].text, "action_1") + + XCTAssertEqual(notification.buttons?[1].id, "2") + XCTAssertEqual(notification.buttons?[1].text, "action_2") + XCTAssertEqual(notification.buttons?[1].icon?.id, "sys_icon_id") + XCTAssertEqual(notification.buttons?[1].icon?.type, .system) + + XCTAssertEqual(notification.buttons?[2].id, "3") + XCTAssertEqual(notification.buttons?[2].text, "action_3") + XCTAssertEqual(notification.buttons?[2].icon?.id, "custom_icon_id") + XCTAssertEqual(notification.buttons?[2].icon?.type, .custom) + } + + func test_decode_image() throws { + fileName = "notification-image.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.attachment?.pictureUrl, "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg") + } + + func test_decode_background() throws { + fileName = "notification-background.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.aps.isBackground, true) + } + + func test_decode_url() throws { + fileName = "notification-url.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.url?.absoluteString, "https://www.optimove.com") + } + + func test_decode_deeplink() throws { + fileName = "notification-deeplink.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.deeplink?.id, 1) + } +} diff --git a/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift b/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift index def072a2..995a145e 100644 --- a/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift +++ b/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift @@ -1,92 +1,56 @@ // Copyright © 2022 Optimove. All rights reserved. -import Foundation +import OptimoveCore import UserNotifications -let MAX_DYNAMIC_CATEGORIES = 128 -let DYNAMIC_CATEGORY_IDENTIFIER = "__kumulos_category_%d__" - -@available(iOS 10.0, *) -class CategoryManager { - let categoryReadLock = DispatchSemaphore(value: 0) - let dynamicCategoryLock = DispatchSemaphore(value: 1) - - fileprivate static var instance: CategoryManager? - - static var sharedInstance: CategoryManager { - if instance == nil { - instance = CategoryManager() - } - - return instance! +enum CategoryManager { + enum Constants { + static let MAX_DYNAMIC_CATEGORIES = 128 + static let DYNAMIC_CATEGORY = OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue } - static func getCategoryIdForMessageId(messageId: Int) -> String { - return String(format: DYNAMIC_CATEGORY_IDENTIFIER, messageId) + static func getCategoryId(messageId: Int) -> String { + return "__kumulos_category_\(messageId)__" } - static func registerCategory(category: UNNotificationCategory) { - var categorySet = sharedInstance.getExistingCategories() - var storedDynamicCategories = sharedInstance.getExistingDynamicCategoriesList() + static func registerCategory(_ category: UNNotificationCategory) async { + var systemCategories = await UNUserNotificationCenter.current().notificationCategories() + var storedCategoryIds = readCategoryIds() + + systemCategories.insert(category) + storedCategoryIds.insert(category.identifier) - categorySet.insert(category) - storedDynamicCategories.append(category.identifier) + let (categories, categoryIds) = maybePruneCategories(categories: systemCategories, categoryIds: storedCategoryIds) - sharedInstance.pruneCategoriesAndSave(categories: categorySet, dynamicCategories: storedDynamicCategories) + UNUserNotificationCenter.current().setNotificationCategories(categories) + writeCategoryIds(categoryIds) // Force a reload of the categories - _ = sharedInstance.getExistingCategories() + await UNUserNotificationCenter.current().notificationCategories() } - private func getExistingCategories() -> Set { - var returnedCategories = Set() - - UNUserNotificationCenter.current().getNotificationCategories { (categories: Set) in - returnedCategories = Set(categories) - - self.categoryReadLock.signal() - } - - _ = categoryReadLock.wait(timeout: DispatchTime.now() + DispatchTimeInterval.seconds(5)) - - return returnedCategories + static func readCategoryIds() -> Set { + let array = UserDefaults.standard.object(forKey: Constants.DYNAMIC_CATEGORY) as? [String] ?? [] + return Set(array) } - private func getExistingDynamicCategoriesList() -> [String] { - dynamicCategoryLock.wait() - defer { - dynamicCategoryLock.signal() - } - - if let existingArray = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue) { - return existingArray as! [String] - } - - let newArray = [String]() - - UserDefaults.standard.set(newArray, forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue) - - return newArray + static func writeCategoryIds(_ ids: Set) { + UserDefaults.standard.set(Array(ids), forKey: Constants.DYNAMIC_CATEGORY) } - private func pruneCategoriesAndSave(categories: Set, dynamicCategories: [String]) { - if dynamicCategories.count <= MAX_DYNAMIC_CATEGORIES { - UNUserNotificationCenter.current().setNotificationCategories(categories) - UserDefaults.standard.set(dynamicCategories, forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue) - return + static func maybePruneCategories( + categories: Set, + categoryIds: Set, + limit: Int = Constants.MAX_DYNAMIC_CATEGORIES + ) -> (categories: Set, categoryIds: Set) { + if categoryIds.count <= limit, categories.count <= limit { + return (categories: categories, categoryIds: categoryIds) } - let categoriesToRemove = dynamicCategories.prefix(dynamicCategories.count - MAX_DYNAMIC_CATEGORIES) - - let prunedCategories = categories.filter { category -> Bool in - categoriesToRemove.firstIndex(of: category.identifier) == nil - } - - let prunedDynamicCategories = dynamicCategories.filter { cat -> Bool in - categoriesToRemove.firstIndex(of: cat) == nil - } + let categoriesToRemove = categoryIds.prefix(categoryIds.count - limit) + let prunedCategories = categories.filter { !categoriesToRemove.contains($0.identifier) } + let prunedCategoryIds = categoryIds.subtracting(categoriesToRemove) - UNUserNotificationCenter.current().setNotificationCategories(prunedCategories) - UserDefaults.standard.set(prunedDynamicCategories, forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue) + return (categories: prunedCategories, categoryIds: prunedCategoryIds) } } diff --git a/OptimoveNotificationServiceExtension/Sources/OptimobileShared b/OptimoveNotificationServiceExtension/Sources/OptimobileShared deleted file mode 120000 index 75755936..00000000 --- a/OptimoveNotificationServiceExtension/Sources/OptimobileShared +++ /dev/null @@ -1 +0,0 @@ -../../OptimobileShared \ No newline at end of file diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 22a29e03..0140c4d7 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -1,217 +1,133 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore import UIKit import UserNotifications -public class OptimoveNotificationService { - private static let syncBarrier = DispatchSemaphore(value: 0) - - public class func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { - let bestAttemptContent = (request.content.mutableCopy() as! UNMutableNotificationContent) - let userInfo = request.content.userInfo +public enum OptimoveNotificationService { + enum Error: String, LocalizedError { + case noBestAttemptContent + case userInfoNotValid + } - if !validateUserInfo(userInfo: userInfo) { - return + public static func didReceive( + _ request: UNNotificationRequest, + withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void + ) { + Task { + do { + let bestAttemptContent = try await didReceive(request) + contentHandler(bestAttemptContent) + } catch { + assertionFailure(error.localizedDescription) + contentHandler(request.content) + } } + } - let custom = userInfo["custom"] as! [AnyHashable: Any] - let data = custom["a"] as! [AnyHashable: Any] - - let msg = data["k.message"] as! [AnyHashable: Any] - let msgData = msg["data"] as! [AnyHashable: Any] - let id = msgData["id"] as! Int - - if bestAttemptContent.categoryIdentifier == "" { - let actionButtons = getButtons(userInfo: userInfo, bestAttemptContent: bestAttemptContent) - - addCategory(bestAttemptContent: bestAttemptContent, actionArray: actionButtons, id: id) + static func didReceive(_ request: UNNotificationRequest) async throws -> UNNotificationContent { + guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else { + throw Error.noBestAttemptContent } - - let dispatchGroup = DispatchGroup() - - maybeAddImageAttachment(dispatchGroup: dispatchGroup, userInfo: userInfo, bestAttemptContent: bestAttemptContent) - - if AppGroupsHelper.isKumulosAppGroupDefined() { - maybeSetBadge(bestAttemptContent: bestAttemptContent, userInfo: userInfo) - PendingNotificationHelper.add(notification: PendingNotification(id: id, deliveredAt: Date(), identifier: request.identifier)) + let userInfo = request.content.userInfo + guard JSONSerialization.isValidJSONObject(userInfo) else { + throw Error.userInfoNotValid } - - dispatchGroup.notify(queue: .main) { - contentHandler(bestAttemptContent) + let data = try JSONSerialization.data(withJSONObject: userInfo) + let notification = try JSONDecoder().decode(PushNotification.self, from: data) + if bestAttemptContent.categoryIdentifier.isEmpty { + bestAttemptContent.categoryIdentifier = await registerCategory(notification: notification) } - } - - private class func validateUserInfo(userInfo: [AnyHashable: Any]) -> Bool { - var dict: [AnyHashable: Any] = userInfo - let keysInOrder = ["custom", "a", "k.message", "data"] - - for key in keysInOrder { - if dict[key] == nil { - return false + if let storage = try? UserDefaults.optimoveAppGroup() { + let mediaHelper = MediaHelper(storage: storage) + if let attachment = try await maybeGetAttachment( + notification: notification, + mediaHelper: mediaHelper + ) { + bestAttemptContent.attachments = [attachment] } - - dict = dict[key] as! [AnyHashable: Any] - } - - if dict["id"] == nil { - return false + let optimobileHelper = OptimobileHelper(storage: storage) + if let badge = optimobileHelper.getBadge(notification: notification) { + storage.set(value: badge, key: .badgeCount) + bestAttemptContent.badge = NSNumber(integerLiteral: badge) + } + let pendingNoticationHelper = PendingNotificationHelper(storage: storage) + pendingNoticationHelper.add( + notification: PendingNotification( + id: notification.message.id, + identifier: request.identifier + ) + ) } - return true + return bestAttemptContent } - private class func getButtons(userInfo: [AnyHashable: Any], bestAttemptContent _: UNMutableNotificationContent) -> NSMutableArray { - let actionArray = NSMutableArray() - - let custom = userInfo["custom"] as! [AnyHashable: Any] - let data = custom["a"] as! [AnyHashable: Any] - - let buttons = data["k.buttons"] as? NSArray - - if buttons == nil || buttons!.count == 0 { - return actionArray - } - - for button in buttons! { - let buttonDict = button as! [AnyHashable: Any] - - let id = buttonDict["id"] as! String - let text = buttonDict["text"] as! String - + static func buildActions(notification: PushNotification) -> [UNNotificationAction] { + return notification.buttons?.map { button in if #available(iOS 15.0, *) { - let icon = getButtonIcon(button: buttonDict) - let action = UNNotificationAction(identifier: id, title: text, options: .foreground, icon: icon) - actionArray.add(action) + return UNNotificationAction( + identifier: button.id, + title: button.text, + options: .foreground, + icon: buildIcon(button: button) + ) } else { - let action = UNNotificationAction(identifier: id, title: text, options: .foreground) - actionArray.add(action) + return UNNotificationAction( + identifier: button.id, + title: button.text, + options: .foreground + ) } - } - - return actionArray + } ?? [] } @available(iOS 15.0, *) - private class func getButtonIcon(button: [AnyHashable: Any]) -> UNNotificationActionIcon? { - guard let icon = button["icon"] as? [String: String], let iconType = icon["type"], let iconId = icon["id"] else { - return nil - } - - if iconType == "custom" { - // TODO: - What if this doesnt exist? Catch exception -> return nil? - return UNNotificationActionIcon(templateImageName: iconId) - } - - return UNNotificationActionIcon(systemImageName: iconId) - } - - private class func addCategory(bestAttemptContent: UNMutableNotificationContent, actionArray: NSMutableArray, id: Int) { - let categoryIdentifier = CategoryManager.getCategoryIdForMessageId(messageId: id) - - let category = UNNotificationCategory(identifier: categoryIdentifier, actions: actionArray as! [UNNotificationAction], intentIdentifiers: [], options: .customDismissAction) - - CategoryManager.registerCategory(category: category) - - bestAttemptContent.categoryIdentifier = categoryIdentifier - } - - private class func maybeAddImageAttachment(dispatchGroup: DispatchGroup, userInfo: [AnyHashable: Any], bestAttemptContent: UNMutableNotificationContent) { - let attachments = userInfo["attachments"] as? [AnyHashable: Any] - let pictureUrl = attachments?["pictureUrl"] as? String - - guard let picUrlNonNull = pictureUrl else { return } - - let picExtension = getPictureExtension(picUrlNonNull) - let url = MediaHelper.getCompletePictureUrl(pictureUrl: picUrlNonNull as String, width: UInt(floor(UIScreen.main.bounds.size.width))) - - dispatchGroup.enter() - - loadAttachment(url!, withExtension: picExtension, completionHandler: { attachment in - if attachment != nil { - bestAttemptContent.attachments = [attachment!] - } - dispatchGroup.leave() - }) - } - - private class func getPictureExtension(_ pictureUrl: String?) -> String? { - if pictureUrl == nil { - return nil + static func buildIcon(button: PushNotification.Button) -> UNNotificationActionIcon? { + guard let icon = button.icon else { return nil } + switch icon.type { + case .custom: + return UNNotificationActionIcon(templateImageName: icon.id) + case .system: + return UNNotificationActionIcon(systemImageName: icon.id) } - let pictureExtension = URL(fileURLWithPath: pictureUrl!).pathExtension - if pictureExtension == "" { - return nil - } - - return "." + pictureExtension } - private class func loadAttachment(_ url: URL, withExtension pictureExtension: String?, completionHandler: @escaping (UNNotificationAttachment?) -> Void) { - let session = URLSession(configuration: URLSessionConfiguration.default) - - (session.downloadTask(with: url, completionHandler: { temporaryFileLocation, response, error in - if error != nil { - print("NotificationServiceExtension: \(error!.localizedDescription)") - completionHandler(nil) - return - } - - var finalExt = pictureExtension - if finalExt == nil { - finalExt = self.getPictureExtension(response?.suggestedFilename) - if finalExt == nil { - completionHandler(nil) - return - } - } - - if temporaryFileLocation == nil { - completionHandler(nil) - return - } - - let fileManager = FileManager.default - let localURL = URL(fileURLWithPath: temporaryFileLocation!.path + finalExt!) - do { - try fileManager.moveItem(at: temporaryFileLocation!, to: localURL) - } catch { - completionHandler(nil) - return - } - - var attachment: UNNotificationAttachment? - do { - attachment = try UNNotificationAttachment(identifier: "", url: localURL, options: nil) - } catch { - print("NotificationServiceExtension: attachment error: \(error.localizedDescription)") - } - - completionHandler(attachment) - })).resume() + static func registerCategory(notification: PushNotification) async -> String { + let categoryIdentifier = CategoryManager.getCategoryId(messageId: notification.message.id) + let category = UNNotificationCategory( + identifier: categoryIdentifier, + actions: buildActions(notification: notification), + intentIdentifiers: [], + options: .customDismissAction + ) + await CategoryManager.registerCategory(category) + + return categoryIdentifier } - private class func maybeSetBadge(bestAttemptContent: UNMutableNotificationContent, userInfo: [AnyHashable: Any]) { - let aps = userInfo["aps"] as! [AnyHashable: Any] - if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 { - return - } + static func maybeGetAttachment(notification: PushNotification, mediaHelper: MediaHelper) async throws -> UNNotificationAttachment? { + guard let picturePath = notification.attachment?.pictureUrl else { return nil } - let newBadge: NSNumber? = OptimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) - if newBadge == nil { - return - } + let url = try await mediaHelper.getCompletePictureUrl( + pictureUrlString: picturePath, + width: UInt(floor(UIScreen.main.bounds.size.width)) + ) - bestAttemptContent.badge = newBadge - KeyValPersistenceHelper.set(newBadge, forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) + return try await downloadAttachment(url: url) } - private class func isBackgroundPush(userInfo: [AnyHashable: Any]) -> Bool { - let aps = userInfo["aps"] as! [AnyHashable: Any] - if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 { - return true - } - - return false + static func downloadAttachment(url: URL) async throws -> UNNotificationAttachment { + let (data, response) = try await URLSession.shared.data(from: url) + let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent(UUID().uuidString) + .appendingPathExtension((response.url ?? url).pathExtension) + try data.write(to: tempURL) + + return try UNNotificationAttachment( + identifier: "", + url: tempURL + ) } } diff --git a/OptimoveNotificationServiceExtension/Tests/Sources/CategoryManagerTests.swift b/OptimoveNotificationServiceExtension/Tests/Sources/CategoryManagerTests.swift new file mode 100644 index 00000000..6d7e543a --- /dev/null +++ b/OptimoveNotificationServiceExtension/Tests/Sources/CategoryManagerTests.swift @@ -0,0 +1,65 @@ +// Copyright © 2023 Optimove. All rights reserved. + +@testable import OptimoveNotificationServiceExtension +import XCTest + +final class CategoryManagerTests: XCTestCase { + override func tearDown() async throws { + UserDefaults.standard.removeObject(forKey: CategoryManager.Constants.DYNAMIC_CATEGORY) + } + + func test_category_id() { + let id = CategoryManager.getCategoryId(messageId: 123) + XCTAssertEqual(id, "__kumulos_category_123__") + } + + func test_read_dynamic_categories() async throws { + let categories = CategoryManager.readCategoryIds() + XCTAssertEqual(categories.count, 0) + } + + func test_write_dynamic_categories() async throws { + let categories = CategoryManager.readCategoryIds() + XCTAssertEqual(categories.count, 0) + + CategoryManager.writeCategoryIds(["category1", "category2"]) + + let newCategories = CategoryManager.readCategoryIds() + XCTAssertEqual(newCategories.count, 2) + } + + func test_filter_pruned_categories() async throws { + let categories: Set = [ + UNNotificationCategory(identifier: "category1", actions: [], intentIdentifiers: [], options: []), + UNNotificationCategory(identifier: "category2", actions: [], intentIdentifiers: [], options: []), + UNNotificationCategory(identifier: "category3", actions: [], intentIdentifiers: [], options: []), + ] + let categoryIds = Set(["category1", "category2", "category3"]) + + let (prunedCategories, prunedCategoryIds) = CategoryManager.maybePruneCategories( + categories: categories, + categoryIds: categoryIds + ) + + XCTAssertEqual(prunedCategories.count, 3) + XCTAssertEqual(prunedCategoryIds.count, 3) + } + + func test_filter_pruned_categories_with_limit() async throws { + let categories: Set = [ + UNNotificationCategory(identifier: "category1", actions: [], intentIdentifiers: [], options: []), + UNNotificationCategory(identifier: "category2", actions: [], intentIdentifiers: [], options: []), + UNNotificationCategory(identifier: "category3", actions: [], intentIdentifiers: [], options: []), + ] + let categoryIds = Set(["category1", "category2", "category3"]) + + let (prunedCategories, prunedCategoryIds) = CategoryManager.maybePruneCategories( + categories: categories, + categoryIds: categoryIds, + limit: 2 + ) + + XCTAssertEqual(prunedCategories.count, 2) + XCTAssertEqual(prunedCategoryIds.count, 2) + } +} diff --git a/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift b/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift index ca1d81cc..fb3d7d02 100644 --- a/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift +++ b/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift @@ -1,28 +1,38 @@ // Copyright © 2023 Optimove. All rights reserved. +@testable import OptimoveNotificationServiceExtension import XCTest final class OptimoveNotificationServiceExtensionTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + func test_download_attachment() async throws { + let url = URL(string: "https://picsum.photos/200")! + let attachment = try await OptimoveNotificationService.downloadAttachment(url: url) + XCTAssertNotNil(attachment) + XCTAssertTrue( + FileManager.default.fileExists(atPath: attachment.url.path), + "File should exist at path: \(attachment.url.path)" + ) } - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + func test_download_attachment_in_the_process() async throws { + let url = URL(string: "https://picsum.photos/200")! + for _ in 0 ... 10 { + let attachment = try await OptimoveNotificationService.downloadAttachment(url: url) + XCTAssertNotNil(attachment) + XCTAssertTrue( + FileManager.default.fileExists(atPath: attachment.url.path), + "File should exist at path: \(attachment.url.path)" + ) + } } - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + func test_download_attachment_withInvalidUrl() async throws { + let url = URL(string: "https://picsum.photos/invalid")! + do { + _ = try await OptimoveNotificationService.downloadAttachment(url: url) + XCTFail("Should throw an error") + } catch { + XCTAssertNotNil(error) } } } diff --git a/OptimoveSDK/Sources/Assets/.gitkeep b/OptimoveSDK/Sources/Assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/OptimoveCore/Sources/Classes/Coding/RuntimeCodingKey.swift b/OptimoveSDK/Sources/Classes/Coding/RuntimeCodingKey.swift similarity index 51% rename from OptimoveCore/Sources/Classes/Coding/RuntimeCodingKey.swift rename to OptimoveSDK/Sources/Classes/Coding/RuntimeCodingKey.swift index 95a87773..5657cab5 100644 --- a/OptimoveCore/Sources/Classes/Coding/RuntimeCodingKey.swift +++ b/OptimoveSDK/Sources/Classes/Coding/RuntimeCodingKey.swift @@ -2,16 +2,16 @@ import Foundation -public final class RuntimeCodingKey: CodingKey { - public var stringValue: String +final class RuntimeCodingKey: CodingKey { + var stringValue: String - public init?(stringValue: String) { + init?(stringValue: String) { self.stringValue = stringValue } - public var intValue: Int? + var intValue: Int? - public init?(intValue: Int) { + init?(intValue: Int) { self.intValue = intValue stringValue = String(intValue) } diff --git a/OptimoveSDK/Sources/Classes/Components/OptiTrack/OptiTrack.swift b/OptimoveSDK/Sources/Classes/Components/OptiTrack/OptiTrack.swift index d470555a..ad683523 100644 --- a/OptimoveSDK/Sources/Classes/Components/OptiTrack/OptiTrack.swift +++ b/OptimoveSDK/Sources/Classes/Components/OptiTrack/OptiTrack.swift @@ -4,10 +4,6 @@ import Foundation import OptimoveCore import UIKit -typealias OptistreamEvent = OptimoveCore.OptistreamEvent -typealias OptistreamEventBuilder = OptimoveCore.OptistreamEventBuilder -typealias OptistreamNetworking = OptimoveCore.OptistreamNetworking - final class OptiTrack { enum Constants { static let eventBatchLimit = 50 diff --git a/OptimoveCore/Sources/Classes/Configurations/Configuration.swift b/OptimoveSDK/Sources/Classes/Configuration/Configuration.swift similarity index 60% rename from OptimoveCore/Sources/Classes/Configurations/Configuration.swift rename to OptimoveSDK/Sources/Classes/Configuration/Configuration.swift index eddca072..25c7dea8 100644 --- a/OptimoveCore/Sources/Classes/Configurations/Configuration.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/Configuration.swift @@ -2,16 +2,16 @@ import Foundation -public struct Configuration: Codable, TenantInfo, EventInfo { - public let tenantID: Int - public let logger: LoggerConfig - public let realtime: RealtimeConfig - public let optitrack: OptitrackConfig - public let events: [String: EventsConfig] - public let isEnableRealtime: Bool - public let isSupportedAirship: Bool +struct Configuration: Codable, TenantInfo, EventInfo { + let tenantID: Int + let logger: LoggerConfig + let realtime: RealtimeConfig + let optitrack: OptitrackConfig + let events: [String: EventsConfig] + let isEnableRealtime: Bool + let isSupportedAirship: Bool - public init( + init( tenantID: Int, logger: LoggerConfig, realtime: RealtimeConfig, @@ -30,12 +30,12 @@ public struct Configuration: Codable, TenantInfo, EventInfo { } } -public struct LoggerConfig: Codable, TenantInfo { - public let tenantID: Int - public let logServiceEndpoint: URL - public let isProductionLogsEnabled: Bool +struct LoggerConfig: Codable, TenantInfo { + let tenantID: Int + let logServiceEndpoint: URL + let isProductionLogsEnabled: Bool - public init( + init( tenantID: Int, logServiceEndpoint: URL, isProductionLogsEnabled: Bool @@ -46,13 +46,13 @@ public struct LoggerConfig: Codable, TenantInfo { } } -public struct RealtimeConfig: Codable, TenantInfo, EventInfo { - public let tenantID: Int - public let realtimeGateway: URL - public let events: [String: EventsConfig] - public let isEnableRealtimeThroughOptistream: Bool +struct RealtimeConfig: Codable, TenantInfo, EventInfo { + let tenantID: Int + let realtimeGateway: URL + let events: [String: EventsConfig] + let isEnableRealtimeThroughOptistream: Bool - public init( + init( tenantID: Int, realtimeGateway: URL, events: [String: EventsConfig], @@ -65,15 +65,15 @@ public struct RealtimeConfig: Codable, TenantInfo, EventInfo { } } -public struct OptitrackConfig: Codable, TenantInfo, EventInfo { - public let tenantID: Int - public let optitrackEndpoint: URL - public let enableAdvertisingIdReport: Bool - public let eventCategoryName: String - public let events: [String: EventsConfig] - public let isEnableRealtime: Bool +struct OptitrackConfig: Codable, TenantInfo, EventInfo { + let tenantID: Int + let optitrackEndpoint: URL + let enableAdvertisingIdReport: Bool + let eventCategoryName: String + let events: [String: EventsConfig] + let isEnableRealtime: Bool - public init( + init( tenantID: Int, optitrackEndpoint: URL, enableAdvertisingIdReport: Bool, @@ -90,10 +90,10 @@ public struct OptitrackConfig: Codable, TenantInfo, EventInfo { } } -public protocol TenantInfo { +protocol TenantInfo { var tenantID: Int { get } } -public protocol EventInfo { +protocol EventInfo { var events: [String: EventsConfig] { get } } diff --git a/OptimoveCore/Sources/Classes/Configurations/ConfigurationBuilder.swift b/OptimoveSDK/Sources/Classes/Configuration/ConfigurationBuilder.swift similarity index 84% rename from OptimoveCore/Sources/Classes/Configurations/ConfigurationBuilder.swift rename to OptimoveSDK/Sources/Classes/Configuration/ConfigurationBuilder.swift index ea588f53..8cea0bc9 100644 --- a/OptimoveCore/Sources/Classes/Configurations/ConfigurationBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/ConfigurationBuilder.swift @@ -2,13 +2,13 @@ import Foundation -public final class ConfigurationBuilder { - private let globalConfig: GlobalConfig - private let tenantConfig: TenantConfig - private let events: [String: EventsConfig] +final class ConfigurationBuilder { + let globalConfig: GlobalConfig + let tenantConfig: TenantConfig + let events: [String: EventsConfig] - public init(globalConfig: GlobalConfig, - tenantConfig: TenantConfig) + init(globalConfig: GlobalConfig, + tenantConfig: TenantConfig) { self.globalConfig = globalConfig self.tenantConfig = tenantConfig @@ -18,7 +18,7 @@ public final class ConfigurationBuilder { ) } - public func build() -> Configuration { + func build() -> Configuration { return Configuration( tenantID: tenantConfig.optitrack.siteId, logger: buildLoggerConfig(), @@ -31,7 +31,7 @@ public final class ConfigurationBuilder { } } -private extension ConfigurationBuilder { +extension ConfigurationBuilder { func buildLoggerConfig() -> LoggerConfig { return LoggerConfig( tenantID: tenantConfig.optitrack.siteId, diff --git a/OptimoveCore/Sources/Classes/Configurations/Global/GlobalConfig.swift b/OptimoveSDK/Sources/Classes/Configuration/Global/GlobalConfig.swift similarity index 64% rename from OptimoveCore/Sources/Classes/Configurations/Global/GlobalConfig.swift rename to OptimoveSDK/Sources/Classes/Configuration/Global/GlobalConfig.swift index 96e87ec6..24df81da 100644 --- a/OptimoveCore/Sources/Classes/Configurations/Global/GlobalConfig.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/Global/GlobalConfig.swift @@ -4,14 +4,14 @@ import Foundation // MARK: - GlobalConfig -public struct GlobalConfig: Codable, Equatable { - public let general: GlobalGeneralConfig - public let optitrack: GlobalOptitrackConfig - public let coreEvents: [String: EventsConfig] +struct GlobalConfig: Codable, Equatable { + let general: GlobalGeneralConfig + let optitrack: GlobalOptitrackConfig + let coreEvents: [String: EventsConfig] - public init(general: GlobalGeneralConfig, - optitrack: GlobalOptitrackConfig, - coreEvents: [String: EventsConfig]) + init(general: GlobalGeneralConfig, + optitrack: GlobalOptitrackConfig, + coreEvents: [String: EventsConfig]) { self.general = general self.optitrack = optitrack @@ -24,14 +24,14 @@ public struct GlobalConfig: Codable, Equatable { case coreEvents = "core_events" } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) general = try container.decode(GlobalGeneralConfig.self, forKey: .general) optitrack = try container.decode(GlobalOptitrackConfig.self, forKey: .optitrack) coreEvents = try container.decode([String: EventsConfig].self, forKey: .coreEvents) } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(general, forKey: .general) try container.encode(optitrack, forKey: .optitrack) @@ -41,10 +41,10 @@ public struct GlobalConfig: Codable, Equatable { // MARK: - General -public struct GlobalGeneralConfig: Codable, Equatable { - public let logsServiceEndpoint: URL +struct GlobalGeneralConfig: Codable, Equatable { + let logsServiceEndpoint: URL - public init(logsServiceEndpoint: URL) { + init(logsServiceEndpoint: URL) { self.logsServiceEndpoint = logsServiceEndpoint } @@ -55,10 +55,10 @@ public struct GlobalGeneralConfig: Codable, Equatable { // MARK: - Optitrack -public struct GlobalOptitrackConfig: Codable, Equatable { - public let eventCategoryName: String +struct GlobalOptitrackConfig: Codable, Equatable { + let eventCategoryName: String - public init(eventCategoryName: String) { + init(eventCategoryName: String) { self.eventCategoryName = eventCategoryName } diff --git a/OptimoveSDK/Sources/Classes/Configuration/RemoteConfiguration/RemoteConfigurationRequestBuilder.swift b/OptimoveSDK/Sources/Classes/Configuration/RemoteConfiguration/RemoteConfigurationRequestBuilder.swift index 35b57f68..8ceaaf4c 100644 --- a/OptimoveSDK/Sources/Classes/Configuration/RemoteConfiguration/RemoteConfigurationRequestBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/RemoteConfiguration/RemoteConfigurationRequestBuilder.swift @@ -3,7 +3,7 @@ import Foundation import OptimoveCore -public final class RemoteConfigurationRequestBuilder { +final class RemoteConfigurationRequestBuilder { enum Error: LocalizedError { case failedToCreateTenantConfigurationRequest(Swift.Error) @@ -21,11 +21,11 @@ public final class RemoteConfigurationRequestBuilder { private let storage: OptimoveStorage - public init(storage: OptimoveStorage) { + init(storage: OptimoveStorage) { self.storage = storage } - public func createTenantConfigurationsRequest() throws -> NetworkRequest { + func createTenantConfigurationsRequest() throws -> NetworkRequest { do { let tenantToken = try storage.getTenantToken() let version = try storage.getVersion() @@ -41,7 +41,7 @@ public final class RemoteConfigurationRequestBuilder { } } - public func createGlobalConfigurationsRequest() -> NetworkRequest { + func createGlobalConfigurationsRequest() -> NetworkRequest { let url = Endpoints.Remote.GlobalConfig.url Logger.debug("Connect to \(url.absoluteString) to retreive global file.") return NetworkRequest(method: .get, baseURL: url, timeoutInterval: Constants.timeout) diff --git a/OptimoveCore/Sources/Classes/Configurations/Tenant/EventConfig.swift b/OptimoveSDK/Sources/Classes/Configuration/Tenant/EventConfig.swift similarity index 62% rename from OptimoveCore/Sources/Classes/Configurations/Tenant/EventConfig.swift rename to OptimoveSDK/Sources/Classes/Configuration/Tenant/EventConfig.swift index ff264943..07f4954e 100644 --- a/OptimoveCore/Sources/Classes/Configurations/Tenant/EventConfig.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/Tenant/EventConfig.swift @@ -1,12 +1,12 @@ // Copyright © 2017 Optimove. All rights reserved. -public struct EventsConfig: Codable, Equatable { - public let id: Int - public let supportedOnOptitrack: Bool - public let supportedOnRealTime: Bool - public let parameters: [String: Parameter] +struct EventsConfig: Codable, Equatable { + let id: Int + let supportedOnOptitrack: Bool + let supportedOnRealTime: Bool + let parameters: [String: Parameter] - public init( + init( id: Int, supportedOnOptitrack: Bool, supportedOnRealTime: Bool, diff --git a/OptimoveCore/Sources/Classes/Configurations/Tenant/OptitrackMetaData.swift b/OptimoveSDK/Sources/Classes/Configuration/Tenant/OptitrackMetaData.swift similarity index 61% rename from OptimoveCore/Sources/Classes/Configurations/Tenant/OptitrackMetaData.swift rename to OptimoveSDK/Sources/Classes/Configuration/Tenant/OptitrackMetaData.swift index ae08a7bf..5f2898fd 100644 --- a/OptimoveCore/Sources/Classes/Configurations/Tenant/OptitrackMetaData.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/Tenant/OptitrackMetaData.swift @@ -2,11 +2,11 @@ import Foundation -public struct TenantOptitrackConfig: Codable, Equatable { - public var optitrackEndpoint: URL - public var siteId: Int +struct TenantOptitrackConfig: Codable, Equatable { + var optitrackEndpoint: URL + var siteId: Int - public init( + init( optitrackEndpoint: URL, siteId: Int ) { diff --git a/OptimoveCore/Sources/Classes/Configurations/Tenant/Parameter.swift b/OptimoveSDK/Sources/Classes/Configuration/Tenant/Parameter.swift similarity index 65% rename from OptimoveCore/Sources/Classes/Configurations/Tenant/Parameter.swift rename to OptimoveSDK/Sources/Classes/Configuration/Tenant/Parameter.swift index 5d842c5e..c90f5149 100644 --- a/OptimoveCore/Sources/Classes/Configurations/Tenant/Parameter.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/Tenant/Parameter.swift @@ -1,10 +1,10 @@ // Copyright © 2017 Optimove. All rights reserved. -public struct Parameter: Codable, Equatable { - public let type: String - public let optional: Bool +struct Parameter: Codable, Equatable { + let type: String + let optional: Bool - public init( + init( type: String, optional: Bool ) { @@ -12,7 +12,7 @@ public struct Parameter: Codable, Equatable { self.optional = optional } - public var mandatory: Bool { + var mandatory: Bool { return !optional } diff --git a/OptimoveSDK/Sources/Classes/Configuration/Tenant/RealtimeMetaData.swift b/OptimoveSDK/Sources/Classes/Configuration/Tenant/RealtimeMetaData.swift new file mode 100644 index 00000000..8126d6af --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Configuration/Tenant/RealtimeMetaData.swift @@ -0,0 +1,11 @@ +// Copyright © 2017 Optimove. All rights reserved. + +import Foundation + +struct TenantRealtimeConfig: Codable, Equatable { + var realtimeGateway: URL + + init(realtimeGateway: URL) { + self.realtimeGateway = realtimeGateway + } +} diff --git a/OptimoveCore/Sources/Classes/Configurations/Tenant/TenantConfig.swift b/OptimoveSDK/Sources/Classes/Configuration/Tenant/TenantConfig.swift similarity index 83% rename from OptimoveCore/Sources/Classes/Configurations/Tenant/TenantConfig.swift rename to OptimoveSDK/Sources/Classes/Configuration/Tenant/TenantConfig.swift index 88b251f8..1cd5a2ec 100644 --- a/OptimoveCore/Sources/Classes/Configurations/Tenant/TenantConfig.swift +++ b/OptimoveSDK/Sources/Classes/Configuration/Tenant/TenantConfig.swift @@ -1,15 +1,15 @@ // Copyright © 2017 Optimove. All rights reserved. -public struct TenantConfig: Codable, Equatable { - public let isSupportedAirship: Bool - public let isEnableRealtime: Bool - public let isEnableRealtimeThroughOptistream: Bool - public let isProductionLogsEnabled: Bool - public let realtime: TenantRealtimeConfig - public var optitrack: TenantOptitrackConfig - public let events: [String: EventsConfig] +struct TenantConfig: Codable, Equatable { + let isSupportedAirship: Bool + let isEnableRealtime: Bool + let isEnableRealtimeThroughOptistream: Bool + let isProductionLogsEnabled: Bool + let realtime: TenantRealtimeConfig + var optitrack: TenantOptitrackConfig + let events: [String: EventsConfig] - public init( + init( realtime: TenantRealtimeConfig, optitrack: TenantOptitrackConfig, events: [String: EventsConfig], @@ -27,7 +27,7 @@ public struct TenantConfig: Codable, Equatable { self.isProductionLogsEnabled = isProductionLogsEnabled } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) isSupportedAirship = try container.decodeIfPresent(Bool.self, forKey: .supportAirship) ?? false isEnableRealtime = try container.decode(Bool.self, forKey: .enableRealtime) @@ -38,7 +38,7 @@ public struct TenantConfig: Codable, Equatable { events = try container.decode([String: EventsConfig].self, forKey: .events) } - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encodeIfPresent(isSupportedAirship, forKey: .supportAirship) try container.encodeIfPresent(isProductionLogsEnabled, forKey: .prodLogsEnabled) diff --git a/OptimoveSDK/Sources/Classes/DI/Assembly.swift b/OptimoveSDK/Sources/Classes/DI/Assembly.swift index 79ea3757..d754a44b 100644 --- a/OptimoveSDK/Sources/Classes/DI/Assembly.swift +++ b/OptimoveSDK/Sources/Classes/DI/Assembly.swift @@ -12,14 +12,14 @@ final class Assembly { /// A special storage migration, before an actual storage going to be in use. migrate() do { - let keyValureStorage = try UserDefaults.optimove() let fileStorage = try FileStorageImpl( persistentStorageURL: FileManager.optimoveURL(), temporaryStorageURL: FileManager.temporaryURL() ) - return ServiceLocator( + return try ServiceLocator( storageFacade: StorageFacade( - persistantStorage: keyValureStorage, + standardStorage: UserDefaults.optimove(), + appGroupStorage: UserDefaults.optimoveAppGroup(), inMemoryStorage: InMemoryStorage(), fileStorage: fileStorage ) @@ -33,6 +33,7 @@ final class Assembly { private func migrate() { let migrations: [MigrationWork] = [ MigrationWork_3_3_0(), + MigrationWork_6_0_0(), MigrationWork_5_9_0() ] migrations diff --git a/OptimoveSDK/Sources/Classes/DataStructs/RingBuffer.swift b/OptimoveSDK/Sources/Classes/DataStructs/RingBuffer.swift index 530c7241..55ad7e7f 100644 --- a/OptimoveSDK/Sources/Classes/DataStructs/RingBuffer.swift +++ b/OptimoveSDK/Sources/Classes/DataStructs/RingBuffer.swift @@ -2,18 +2,18 @@ import Foundation -public struct RingBuffer { +struct RingBuffer { private var array: [T?] private var readIndex = 0 private var writeIndex = 0 - public init(count: Int) { + init(count: Int) { array = [T?](repeating: nil, count: count) } /* Returns false if out of space. */ @discardableResult - public mutating func write(_ element: T) -> Bool { + mutating func write(_ element: T) -> Bool { guard !isFull else { return false } defer { writeIndex += 1 @@ -23,7 +23,7 @@ public struct RingBuffer { } /* Returns nil if the buffer is empty. */ - public mutating func read() -> T? { + mutating func read() -> T? { guard !isEmpty else { return nil } defer { array[wrapped: readIndex] = nil @@ -36,7 +36,7 @@ public struct RingBuffer { return writeIndex - readIndex } - public var isEmpty: Bool { + var isEmpty: Bool { return availableSpaceForReading == 0 } @@ -44,13 +44,13 @@ public struct RingBuffer { return array.count - availableSpaceForReading } - public var isFull: Bool { + var isFull: Bool { return availableSpaceForWriting == 0 } } extension RingBuffer: Sequence { - public func makeIterator() -> AnyIterator { + func makeIterator() -> AnyIterator { var index = readIndex return AnyIterator { guard index < self.writeIndex else { return nil } diff --git a/OptimoveCore/Sources/Classes/Events/Event.swift b/OptimoveSDK/Sources/Classes/Events/Event.swift similarity index 66% rename from OptimoveCore/Sources/Classes/Events/Event.swift rename to OptimoveSDK/Sources/Classes/Events/Event.swift index cfb11bd2..dfa0ce9a 100644 --- a/OptimoveCore/Sources/Classes/Events/Event.swift +++ b/OptimoveSDK/Sources/Classes/Events/Event.swift @@ -2,18 +2,18 @@ import Foundation -open class Event { - public static let category = "track" +class Event { + static let category = "track" - public let eventId: UUID - public let requestId: String - public let name: String - public let timestamp: Date - public let category: String - public var context: [String: Any] - public var isRealtime: Bool + let eventId: UUID + let requestId: String + let name: String + let timestamp: Date + let category: String + var context: [String: Any] + var isRealtime: Bool - public init( + init( eventId: UUID? = nil, requestId: String? = nil, name: String, diff --git a/OptimoveCore/Sources/Classes/Extension/Bundle+AppVersion.swift b/OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift similarity index 59% rename from OptimoveCore/Sources/Classes/Extension/Bundle+AppVersion.swift rename to OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift index 199b21ef..8fefc29f 100644 --- a/OptimoveCore/Sources/Classes/Extension/Bundle+AppVersion.swift +++ b/OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift @@ -2,8 +2,12 @@ import Foundation -public extension Bundle { +extension Bundle { var appVersion: String { return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "undefined" } + + var buildVersion: String { + return Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "undefined" + } } diff --git a/OptimoveSDK/Sources/Classes/Extensions/Notification.swift b/OptimoveSDK/Sources/Classes/Extensions/Notification.swift index 3e4ff19f..10adc460 100644 --- a/OptimoveSDK/Sources/Classes/Extensions/Notification.swift +++ b/OptimoveSDK/Sources/Classes/Extensions/Notification.swift @@ -2,6 +2,7 @@ import Foundation -public extension Notification.Name { +extension Notification.Name { static let internetStatusChanged = Notification.Name("internetStatusChanged") + static let optimobileInializationFinished = Notification.Name("optimobileInializationFinished") } diff --git a/OptimoveSDK/Sources/Classes/Factories/MainFactory.swift b/OptimoveSDK/Sources/Classes/Factories/MainFactory.swift index 273503c1..3d62ff87 100644 --- a/OptimoveSDK/Sources/Classes/Factories/MainFactory.swift +++ b/OptimoveSDK/Sources/Classes/Factories/MainFactory.swift @@ -3,9 +3,6 @@ import Foundation import OptimoveCore -typealias NetworkClient = OptimoveCore.NetworkClient -typealias NetworkClientImpl = OptimoveCore.NetworkClientImpl - final class MainFactory { private let serviceLocator: ServiceLocator diff --git a/OptimoveCore/Sources/Classes/JSON/Initialization.swift b/OptimoveSDK/Sources/Classes/JSON/Initialization.swift similarity index 91% rename from OptimoveCore/Sources/Classes/JSON/Initialization.swift rename to OptimoveSDK/Sources/Classes/JSON/Initialization.swift index 653ab1ae..ed11c86c 100644 --- a/OptimoveCore/Sources/Classes/JSON/Initialization.swift +++ b/OptimoveSDK/Sources/Classes/JSON/Initialization.swift @@ -26,7 +26,7 @@ import Foundation private struct InitializationError: Error {} -public extension JSON { +extension JSON { /// Create a JSON value from anything. /// /// Argument has to be a valid JSON structure: A `Double`, `Int`, `String`, @@ -59,7 +59,7 @@ public extension JSON { } } -public extension JSON { +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 { @@ -69,25 +69,25 @@ public extension JSON { } extension JSON: ExpressibleByBooleanLiteral { - public init(booleanLiteral value: Bool) { + init(booleanLiteral value: Bool) { self = .bool(value) } } extension JSON: ExpressibleByNilLiteral { - public init(nilLiteral _: ()) { + init(nilLiteral _: ()) { self = .null } } extension JSON: ExpressibleByArrayLiteral { - public init(arrayLiteral elements: JSON...) { + init(arrayLiteral elements: JSON...) { self = .array(elements) } } extension JSON: ExpressibleByDictionaryLiteral { - public init(dictionaryLiteral elements: (String, JSON)...) { + init(dictionaryLiteral elements: (String, JSON)...) { var object: [String: JSON] = [:] for (k, v) in elements { object[k] = v @@ -97,19 +97,19 @@ extension JSON: ExpressibleByDictionaryLiteral { } extension JSON: ExpressibleByFloatLiteral { - public init(floatLiteral value: Double) { + init(floatLiteral value: Double) { self = .number(value) } } extension JSON: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { + init(integerLiteral value: Int) { self = .number(Double(value)) } } extension JSON: ExpressibleByStringLiteral { - public init(stringLiteral value: String) { + init(stringLiteral value: String) { self = .string(value) } } diff --git a/OptimoveCore/Sources/Classes/JSON/JSON.swift b/OptimoveSDK/Sources/Classes/JSON/JSON.swift similarity index 94% rename from OptimoveCore/Sources/Classes/JSON/JSON.swift rename to OptimoveSDK/Sources/Classes/JSON/JSON.swift index 9fd3ef33..5cd4fb5b 100644 --- a/OptimoveCore/Sources/Classes/JSON/JSON.swift +++ b/OptimoveSDK/Sources/Classes/JSON/JSON.swift @@ -28,7 +28,7 @@ import Foundation /// 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 public enum JSON: Equatable { +@dynamicMemberLookup enum JSON: Equatable { case string(String) case number(Double) case object([String: JSON]) @@ -38,7 +38,7 @@ import Foundation } extension JSON: Codable { - public func encode(to encoder: Encoder) throws { + func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self { @@ -57,7 +57,7 @@ extension JSON: Codable { } } - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let object = try? container.decode([String: JSON].self) { @@ -81,7 +81,7 @@ extension JSON: Codable { } extension JSON: CustomDebugStringConvertible { - public var debugDescription: String { + var debugDescription: String { switch self { case let .string(str): return str.debugDescription diff --git a/OptimoveCore/Sources/Classes/JSON/Merging.swift b/OptimoveSDK/Sources/Classes/JSON/Merging.swift similarity index 99% rename from OptimoveCore/Sources/Classes/JSON/Merging.swift rename to OptimoveSDK/Sources/Classes/JSON/Merging.swift index 48fa473f..a3ed3c79 100644 --- a/OptimoveCore/Sources/Classes/JSON/Merging.swift +++ b/OptimoveSDK/Sources/Classes/JSON/Merging.swift @@ -24,7 +24,7 @@ import Foundation -public extension JSON { +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 diff --git a/OptimoveCore/Sources/Classes/JSON/Querying.swift b/OptimoveSDK/Sources/Classes/JSON/Querying.swift similarity index 99% rename from OptimoveCore/Sources/Classes/JSON/Querying.swift rename to OptimoveSDK/Sources/Classes/JSON/Querying.swift index 3464dde5..0d6d49d1 100644 --- a/OptimoveCore/Sources/Classes/JSON/Querying.swift +++ b/OptimoveSDK/Sources/Classes/JSON/Querying.swift @@ -24,7 +24,7 @@ import Foundation -public extension JSON { +extension JSON { /// Return the string value if this is a `.string`, otherwise `nil` var stringValue: String? { if case let .string(value) = self { diff --git a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift index cd8e526a..b1d68d4f 100644 --- a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift +++ b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift @@ -8,6 +8,10 @@ protocol MigrationWork { func runMigration() } +private protocol Replacer { + func replace() +} + class MigrationWorker: MigrationWork { let version: Version @@ -109,10 +113,6 @@ final class MigrationWork_3_3_0: MigrationWorker { } } -private protocol Replacer { - func replace() -} - extension MigrationWork_3_3_0 { final class AppGroupReplacer: Replacer { func replace() { @@ -230,6 +230,50 @@ extension MigrationWork_3_3_0 { } } +/// Migration from Kumulos UserDefaults to Optimove UserDefaults. +final class MigrationWork_6_0_0: MigrationWorker { + init() { + super.init(newVersion: .v_6_0_0) + } + + override func isAllowToMiragte(_: String) -> Bool { + guard let storage = try? UserDefaults.optimove() else { + return true + } + let key = StorageKey.migrationVersions + let versions = storage.object(forKey: key.rawValue) as? [String] ?? [] + return !versions.contains(version.rawValue) + } + + override func runMigration() { + Logger.info("Migration from Kumulos UserDefaults to Optimove UserDefaults started") + let keyMapping: [OptimobileUserDefaultsKey: StorageKey] = [ + .REGION: .region, + .MEDIA_BASE_URL: .mediaURL, + .INSTALL_UUID: .installUUID, + .USER_ID: .userID, + .BADGE_COUNT: .badgeCount, + .PENDING_NOTIFICATIONS: .pendingNotifications, + .PENDING_ANALYTICS: .pendingAnaltics, + .IN_APP_LAST_SYNCED_AT: .inAppLastSyncedAt, + .IN_APP_MOST_RECENT_UPDATED_AT: .inAppMostRecentUpdateAt, + .IN_APP_CONSENTED: .inAppConsented, + .DYNAMIC_CATEGORY: .dynamicCategory, + ] + /// Move values from Kumulos UserDefaults to Optimove UserDefaults. + let kumulosStorage = UserDefaults.standard + let optimoveStorage = try! UserDefaults.optimove() + OptimobileUserDefaultsKey.allCases.forEach { key in + if let value = kumulosStorage.object(forKey: key.rawValue), + let storageKey = keyMapping[key] + { + optimoveStorage.set(value: value, key: storageKey) + kumulosStorage.removeObject(forKey: key.rawValue) + } + } + Logger.info("Migration from Kumulos UserDefaults to Optimove UserDefaults completed") +} + final class MigrationWork_5_9_0: MigrationWorker { init() { super.init(newVersion: .v_5_9_0) diff --git a/OptimoveSDK/Sources/Classes/Migration/Version.swift b/OptimoveSDK/Sources/Classes/Migration/Version.swift index 0880b4d1..b622f25a 100644 --- a/OptimoveSDK/Sources/Classes/Migration/Version.swift +++ b/OptimoveSDK/Sources/Classes/Migration/Version.swift @@ -4,5 +4,6 @@ enum Version: String { case v_2_10_0 = "2.10.0" case v_3_0_0 = "3.0.0" case v_3_3_0 = "3.3.0" + case v_6_0_0 = "6.0.0" case v_5_9_0 = "5.9.0" } diff --git a/OptimoveCore/Sources/Classes/NetworkClient/NetworkClient.swift b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkClient.swift similarity index 86% rename from OptimoveCore/Sources/Classes/NetworkClient/NetworkClient.swift rename to OptimoveSDK/Sources/Classes/NetworkClient/NetworkClient.swift index 1448e65c..42ba1fb8 100644 --- a/OptimoveCore/Sources/Classes/NetworkClient/NetworkClient.swift +++ b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkClient.swift @@ -2,22 +2,22 @@ import Foundation -public typealias NetworkServiceCompletion = (Result, NetworkError>) -> Void +typealias NetworkServiceCompletion = (Result, NetworkError>) -> Void -public protocol NetworkClient { +protocol NetworkClient { func perform(_ request: NetworkRequest, _ completion: @escaping NetworkServiceCompletion) } -public struct NetworkClientImpl { +struct NetworkClientImpl { let session: URLSession - public init(configuration: URLSessionConfiguration = URLSessionConfiguration.default) { + init(configuration: URLSessionConfiguration = URLSessionConfiguration.default) { session = URLSession(configuration: configuration) } } extension NetworkClientImpl: NetworkClient { - public func perform(_ request: NetworkRequest, _ completion: @escaping NetworkServiceCompletion) { + func perform(_ request: NetworkRequest, _ completion: @escaping NetworkServiceCompletion) { let baseURL: URL = request.baseURL var urlComponents = URLComponents() diff --git a/OptimoveCore/Sources/Classes/NetworkClient/NetworkError.swift b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkError.swift similarity index 90% rename from OptimoveCore/Sources/Classes/NetworkClient/NetworkError.swift rename to OptimoveSDK/Sources/Classes/NetworkClient/NetworkError.swift index a60e7cee..dec3f5a9 100644 --- a/OptimoveCore/Sources/Classes/NetworkClient/NetworkError.swift +++ b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkError.swift @@ -2,14 +2,14 @@ import Foundation -public enum NetworkError: LocalizedError { +enum NetworkError: LocalizedError { case error(Error) case noData case invalidURL case requestInvalid(Data?) case requestFailed - public var errorDescription: String? { + var errorDescription: String? { return "NetworkError: " + { switch self { case let .error(error): diff --git a/OptimoveCore/Sources/Classes/NetworkClient/NetworkRequest.swift b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift similarity index 77% rename from OptimoveCore/Sources/Classes/NetworkClient/NetworkRequest.swift rename to OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift index 74e80b91..da308c41 100644 --- a/OptimoveCore/Sources/Classes/NetworkClient/NetworkRequest.swift +++ b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift @@ -2,26 +2,26 @@ import Foundation -public final class NetworkRequest { - public enum DefaultValue { - public static let path: String? = nil - public static let headers: [HTTPHeader] = [] - public static let queryItems: [URLQueryItem]? = nil - public static let httpBody: Data? = nil - public static let timeoutInterval: TimeInterval = 60 - public static let keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys +final class NetworkRequest { + enum DefaultValue { + static let path: String? = nil + static let headers: [HTTPHeader] = [] + static let queryItems: [URLQueryItem]? = nil + static let httpBody: Data? = nil + static let timeoutInterval: TimeInterval = 60 + static let keyEncodingStrategy: KeyEncodingStrategy = .useDefaultKeys } - public let method: HTTPMethod - public let baseURL: URL - public let path: String? - public let headers: [HTTPHeader]? - public let queryItems: [URLQueryItem]? - public let httpBody: Data? - public let timeoutInterval: TimeInterval - public let keyEncodingStrategy: KeyEncodingStrategy + let method: HTTPMethod + let baseURL: URL + let path: String? + let headers: [HTTPHeader]? + let queryItems: [URLQueryItem]? + let httpBody: Data? + let timeoutInterval: TimeInterval + let keyEncodingStrategy: KeyEncodingStrategy - public required init( + required init( method: HTTPMethod, baseURL: URL, path: String? = DefaultValue.path, @@ -41,7 +41,7 @@ public final class NetworkRequest { self.keyEncodingStrategy = keyEncodingStrategy } - public convenience init( + convenience init( method: HTTPMethod, baseURL: URL, path: String? = DefaultValue.path, @@ -66,7 +66,7 @@ public final class NetworkRequest { } } -public enum HTTPMethod: String { +enum HTTPMethod: String { case get = "GET" case put = "PUT" case post = "POST" @@ -78,12 +78,12 @@ public enum HTTPMethod: String { case patch = "PATCH" } -public struct HTTPHeader { - public let field: String - public let value: String +struct HTTPHeader { + let field: String + let value: String } -public extension HTTPHeader { +extension HTTPHeader { enum Fields: String { case contentType = "Content-Type" case userAgent = "User-Agent" @@ -109,7 +109,7 @@ public extension HTTPHeader { } } -public extension HTTPHeader { +extension HTTPHeader { init(field: Fields, value: Values) { self.field = field.rawValue self.value = String(describing: value) @@ -117,13 +117,13 @@ public extension HTTPHeader { } extension HTTPHeader: CustomStringConvertible { - public var description: String { + var description: String { return "key: \(field), value: \(value)" } } extension NetworkRequest: CustomStringConvertible { - public var description: String { + var description: String { return """ [Method]: \(method.rawValue) [URL]: \(baseURL.absoluteString) @@ -136,7 +136,7 @@ extension NetworkRequest: CustomStringConvertible { } } -public enum KeyEncodingStrategy { +enum KeyEncodingStrategy { case useDefaultKeys case convertToSnakeCase case custom(([CodingKey]) -> CodingKey) diff --git a/OptimoveCore/Sources/Classes/NetworkClient/NetworkResponse.swift b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkResponse.swift similarity index 89% rename from OptimoveCore/Sources/Classes/NetworkClient/NetworkResponse.swift rename to OptimoveSDK/Sources/Classes/NetworkClient/NetworkResponse.swift index 61e0cb0b..6140e54b 100644 --- a/OptimoveCore/Sources/Classes/NetworkClient/NetworkResponse.swift +++ b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkResponse.swift @@ -2,12 +2,12 @@ import Foundation -public struct NetworkResponse { +struct NetworkResponse { let statusCode: Int let body: Body } -public extension NetworkResponse where Body == Data? { +extension NetworkResponse where Body == Data? { func decode(to _: BodyType.Type) throws -> BodyType { let data = try unwrap() return try JSONDecoder().decode(BodyType.self, from: data) diff --git a/OptimoveCore/Sources/Classes/NetworkClient/NetworkResult.swift b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkResult.swift similarity index 78% rename from OptimoveCore/Sources/Classes/NetworkClient/NetworkResult.swift rename to OptimoveSDK/Sources/Classes/NetworkClient/NetworkResult.swift index 761b8c3f..330f6a27 100644 --- a/OptimoveCore/Sources/Classes/NetworkClient/NetworkResult.swift +++ b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkResult.swift @@ -1,6 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. -public enum NetworkResult { +enum NetworkResult { case success(NetworkResponse) case failure(NetworkError) } diff --git a/OptimoveCore/Sources/Classes/Operations/AsyncOperation.swift b/OptimoveSDK/Sources/Classes/Operations/AsyncOperation.swift similarity index 89% rename from OptimoveCore/Sources/Classes/Operations/AsyncOperation.swift rename to OptimoveSDK/Sources/Classes/Operations/AsyncOperation.swift index 2fdeb3fe..3c845159 100644 --- a/OptimoveCore/Sources/Classes/Operations/AsyncOperation.swift +++ b/OptimoveSDK/Sources/Classes/Operations/AsyncOperation.swift @@ -2,8 +2,8 @@ import Foundation -open class AsyncOperation: Operation { - public enum State: String { +class AsyncOperation: Operation { + enum State: String { case waiting = "isWaiting" case ready = "isReady" case executing = "isExecuting" @@ -11,7 +11,7 @@ open class AsyncOperation: Operation { case cancelled = "isCancelled" } - public var state = State.waiting { + var state = State.waiting { willSet { willChangeValue(forKey: State.ready.rawValue) willChangeValue(forKey: State.executing.rawValue) @@ -79,12 +79,12 @@ open class AsyncOperation: Operation { } } -open class AsyncBlockOperation: AsyncOperation { - public typealias Closure = (AsyncBlockOperation) -> Void +class AsyncBlockOperation: AsyncOperation { + typealias Closure = (AsyncBlockOperation) -> Void - public let closure: Closure + let closure: Closure - public init(closure: @escaping Closure) { + init(closure: @escaping Closure) { self.closure = closure } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift index 4817ca19..b548ee3f 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import OptimoveCore class KSEventModel: NSManagedObject { @NSManaged var uuid: String @@ -15,17 +16,19 @@ typealias SyncCompletedBlock = (Error?) -> Void final class AnalyticsHelper { let eventsHttpClient: KSHttpClient + let optimobileHelper: OptimobileHelper private var analyticsContext: NSManagedObjectContext? private var migrationAnalyticsContext: NSManagedObjectContext? private var finishedInitializationToken: NSObjectProtocol? // MARK: Initialization - init(httpClient: KSHttpClient) { + init(httpClient: KSHttpClient, optimobileHelper: OptimobileHelper) { analyticsContext = nil migrationAnalyticsContext = nil eventsHttpClient = httpClient + self.optimobileHelper = optimobileHelper initContext() @@ -66,7 +69,7 @@ final class AnalyticsHelper { } private func getSharedDbUrl() -> URL? { - let sharedContainerPath: URL? = AppGroupsHelper.getSharedContainerPath() + let sharedContainerPath = try? FileManager.optimoveAppGroupURL() if sharedContainerPath == nil { return nil } @@ -77,7 +80,7 @@ final class AnalyticsHelper { private func initContext() { let appDbUrl = getAppDbUrl() let appDbExists = appDbUrl == nil ? false : FileManager.default.fileExists(atPath: appDbUrl!.path) - let appGroupExists = AppGroupsHelper.isKumulosAppGroupDefined() + let appGroupExists = true let storeUrl = getMainStoreUrl(appGroupExists: appGroupExists) @@ -120,6 +123,7 @@ final class AnalyticsHelper { return } + let currentUserIdentifier = optimobileHelper.currentUserIdentifier() let work = { guard let context = self.analyticsContext else { print("No context, aborting") @@ -136,7 +140,7 @@ final class AnalyticsHelper { event.uuid = UUID().uuidString.lowercased() event.happenedAt = NSNumber(value: Int64(atTime.timeIntervalSince1970 * 1000)) event.eventType = eventType - event.userIdentifier = OptimobileHelper.currentUserIdentifier + event.userIdentifier = currentUserIdentifier if properties != nil { let propsJson = try? JSONSerialization.data(withJSONObject: properties as Any, options: JSONSerialization.WritingOptions(rawValue: 0)) diff --git a/OptimobileShared/Credentials.swift b/OptimoveSDK/Sources/Classes/Optimobile/Credentials.swift similarity index 55% rename from OptimobileShared/Credentials.swift rename to OptimoveSDK/Sources/Classes/Optimobile/Credentials.swift index 4e71a33c..77801e59 100644 --- a/OptimobileShared/Credentials.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Credentials.swift @@ -1,8 +1,11 @@ // Copyright © 2023 Optimove. All rights reserved. -import Foundation - struct OptimobileCredentials: Codable { let apiKey: String let secretKey: String + + init(apiKey: String, secretKey: String) { + self.apiKey = apiKey + self.secretKey = secretKey + } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/CwlSysctl.swift b/OptimoveSDK/Sources/Classes/Optimobile/CwlSysctl.swift index 1f535974..9bca2d77 100755 --- a/OptimoveSDK/Sources/Classes/Optimobile/CwlSysctl.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/CwlSysctl.swift @@ -20,7 +20,7 @@ import Foundation -public enum SysctlError: Error { +enum SysctlError: Error { case unknown case malformedUTF8 case invalidSize @@ -28,7 +28,7 @@ public enum SysctlError: Error { } /// Wrapper around `sysctl` that preflights and allocates an [Int8] for the result and throws a Swift error if anything goes wrong. -public func sysctl(levels: [Int32]) throws -> [Int8] { +func sysctl(levels: [Int32]) throws -> [Int8] { return try levels.withUnsafeBufferPointer { levelsPointer throws -> [Int8] in // Preflight the request to get the required data size var requiredSize = 0 @@ -51,7 +51,7 @@ public func sysctl(levels: [Int32]) throws -> [Int8] { } /// Generate an array of name levels (as can be used with the previous sysctl function) from a sysctl name string. -public func sysctlLevels(fromName: String) throws -> [Int32] { +func sysctlLevels(fromName: String) throws -> [Int32] { var levelsBufferSize = Int(CTL_MAXNAME) var levelsBuffer = [Int32](repeating: 0, count: levelsBufferSize) try levelsBuffer.withUnsafeMutableBufferPointer { (lbp: inout UnsafeMutableBufferPointer) throws in @@ -89,33 +89,33 @@ private func stringFromSysctl(levels: [Int32]) throws -> String { } /// Get an arbitrary sysctl value and interpret the bytes as a UTF8 string -public func sysctlString(levels: Int32...) throws -> String { +func sysctlString(levels: Int32...) throws -> String { return try stringFromSysctl(levels: levels) } /// Get an arbitrary sysctl value and interpret the bytes as a UTF8 string -public func sysctlString(name: String) throws -> String { +func sysctlString(name: String) throws -> String { return try stringFromSysctl(levels: sysctlLevels(fromName: name)) } /// Get an arbitrary sysctl value and cast it to an Int64 -public func sysctlInt(levels: Int32...) throws -> Int64 { +func sysctlInt(levels: Int32...) throws -> Int64 { return try intFromSysctl(levels: levels) } /// Get an arbitrary sysctl value and cast it to an Int64 -public func sysctlInt(name: String) throws -> Int64 { +func sysctlInt(name: String) throws -> Int64 { return try intFromSysctl(levels: sysctlLevels(fromName: name)) } -public enum Sysctl { +enum Sysctl { /// e.g. "MyComputer.local" (from System Preferences -> Sharing -> Computer Name) or /// "My-Name-iPhone" (from Settings -> General -> About -> Name) - public static var hostName: String { return try! sysctlString(levels: CTL_KERN, KERN_HOSTNAME) } + static var hostName: String { return try! sysctlString(levels: CTL_KERN, KERN_HOSTNAME) } /// e.g. "x86_64" or "N71mAP" /// NOTE: this is *corrected* on iOS devices to fetch hw.model - public static var machine: String { + static var machine: String { #if os(iOS) && !arch(x86_64) && !arch(i386) return try! sysctlString(levels: CTL_HW, HW_MODEL) #else @@ -125,7 +125,7 @@ public enum Sysctl { /// e.g. "MacPro4,1" or "iPhone8,1" /// NOTE: this is *corrected* on iOS devices to fetch hw.machine - public static var model: String { + static var model: String { #if os(iOS) && !arch(x86_64) && !arch(i386) return try! sysctlString(levels: CTL_HW, HW_MACHINE) #else @@ -134,29 +134,29 @@ public enum Sysctl { } /// e.g. "8" or "2" - public static var activeCPUs: Int64 { return try! sysctlInt(levels: CTL_HW, HW_AVAILCPU) } + static var activeCPUs: Int64 { return try! sysctlInt(levels: CTL_HW, HW_AVAILCPU) } /// e.g. "15.3.0" or "15.0.0" - public static var osRelease: String { return try! sysctlString(levels: CTL_KERN, KERN_OSRELEASE) } + static var osRelease: String { return try! sysctlString(levels: CTL_KERN, KERN_OSRELEASE) } /// e.g. 199506 or 199506 - public static var osRev: Int64 { return try! sysctlInt(levels: CTL_KERN, KERN_OSREV) } + static var osRev: Int64 { return try! sysctlInt(levels: CTL_KERN, KERN_OSREV) } /// e.g. "Darwin" or "Darwin" - public static var osType: String { return try! sysctlString(levels: CTL_KERN, KERN_OSTYPE) } + static var osType: String { return try! sysctlString(levels: CTL_KERN, KERN_OSTYPE) } /// e.g. "15D21" or "13D20" - public static var osVersion: String { return try! sysctlString(levels: CTL_KERN, KERN_OSVERSION) } + static var osVersion: String { return try! sysctlString(levels: CTL_KERN, KERN_OSVERSION) } /// e.g. "Darwin Kernel Version 15.3.0: Thu Dec 10 18:40:58 PST 2015; root:xnu-3248.30.4~1/RELEASE_X86_64" or /// "Darwin Kernel Version 15.0.0: Wed Dec 9 22:19:38 PST 2015; root:xnu-3248.31.3~2/RELEASE_ARM64_S8000" - public static var version: String { return try! sysctlString(levels: CTL_KERN, KERN_VERSION) } + static var version: String { return try! sysctlString(levels: CTL_KERN, KERN_VERSION) } #if os(OSX) /// e.g. 2659000000 (not available on iOS) - public static var cpuFreq: Int64 { return try! sysctlInt(name: "hw.cpufrequency") } + static var cpuFreq: Int64 { return try! sysctlInt(name: "hw.cpufrequency") } /// e.g. 25769803776 (not available on iOS) - public static var memSize: Int64 { return try! sysctlInt(levels: CTL_HW, HW_MEMSIZE) } + static var memSize: Int64 { return try! sysctlInt(levels: CTL_HW, HW_MEMSIZE) } #endif } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift index ee8d1c5e..9085e668 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 OptimoveCore import UIKit public enum InAppMessagePresentationResult: String { @@ -18,7 +19,10 @@ private var ks_existingBackgroundFetchDelegate: IMP? typealias InAppSyncCompletionHandler = (_ result: Int) -> Void class InAppManager { - private let httpClient: KSHttpClient + let httpClient: KSHttpClient + let storage: OptimoveStorage + let pendingNoticationHelper: PendingNotificationHelper + let optimobileHelper: OptimobileHelper private(set) var presenter: InAppPresenter private var pendingTickleIds = NSMutableOrderedSet(capacity: 1) @@ -34,9 +38,23 @@ class InAppManager { // MARK: Initialization - init(_ config: OptimobileConfig, httpClient: KSHttpClient, urlBuilder: UrlBuilder) { + init( + _ config: OptimobileConfig, + httpClient: KSHttpClient, + urlBuilder: UrlBuilder, + storage: OptimoveStorage, + pendingNoticationHelper: PendingNotificationHelper, + optimobileHelper: OptimobileHelper + ) { self.httpClient = httpClient - presenter = InAppPresenter(displayMode: config.inAppDefaultDisplayMode, urlBuilder: urlBuilder) + self.storage = storage + self.pendingNoticationHelper = pendingNoticationHelper + self.optimobileHelper = optimobileHelper + presenter = InAppPresenter( + displayMode: config.inAppDefaultDisplayMode, + urlBuilder: urlBuilder, + pendingNoticationHelper: pendingNoticationHelper + ) syncQueue = DispatchQueue(label: "com.optimove.inapp.sync") finishedInitializationToken = NotificationCenter.default @@ -155,7 +173,7 @@ class InAppManager { func userConsented() -> Bool { // Note if this implementation is changed there is a usage in the main Optimobile initialisation path // that should be considered. - return UserDefaults.standard.bool(forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) + return storage[.inAppConsented] ?? false } func updateUserConsent(consentGiven: Bool) { @@ -164,7 +182,7 @@ class InAppManager { Optimobile.trackEventImmediately(eventType: OptimobileEvent.IN_APP_CONSENT_CHANGED.rawValue, properties: props) if consentGiven { - UserDefaults.standard.set(consentGiven, forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) + storage.set(value: consentGiven, key: .inAppConsented) handleEnrollmentAndSyncSetup() } else { DispatchQueue.global(qos: .default).async { @@ -219,9 +237,9 @@ class InAppManager { } NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) + storage.set(value: nil, key: .inAppConsented) + storage.set(value: nil, key: .inAppLastSyncedAt) + storage.set(value: nil, key: .inAppMostRecentUpdateAt) context.performAndWait { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Message") @@ -249,22 +267,23 @@ class InAppManager { // MARK: Message management func syncDebounced(_ onComplete: InAppSyncCompletionHandler? = nil) { - syncQueue.async { - let lastSyncedAt = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) as? Date ?? Date(timeIntervalSince1970: 0) - - if lastSyncedAt.timeIntervalSinceNow < self.SYNC_DEBOUNCE_SECONDS { + syncQueue.async { [unowned self] in + let lastSyncedAt = storage.value(for: .inAppLastSyncedAt) as? Date ?? Date(timeIntervalSince1970: 0) + if lastSyncedAt.timeIntervalSinceNow < SYNC_DEBOUNCE_SECONDS { return } - self.sync(onComplete) + sync(onComplete) } } func sync(_ onComplete: InAppSyncCompletionHandler? = nil) { - syncQueue.async { + let currentUserIdentifier = optimobileHelper.currentUserIdentifier() + syncQueue.async { [unowned self] in let syncBarrier = DispatchSemaphore(value: 0) - let mostRecentUpdate = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) as? NSDate + let mostRecentUpdate = storage.value(for: .inAppMostRecentUpdateAt) as? Date + var after = "" if let mostRecentUpdate = mostRecentUpdate { @@ -276,12 +295,12 @@ class InAppManager { after = "?after=\(KSHttpUtil.urlEncode(formatter.string(from: mostRecentUpdate as Date))!)" } - let encodedIdentifier = KSHttpUtil.urlEncode(OptimobileHelper.currentUserIdentifier) + let encodedIdentifier = KSHttpUtil.urlEncode(currentUserIdentifier) let path = "/v1/users/\(encodedIdentifier!)/messages\(after)" - self.httpClient.sendRequest(.GET, toPath: path, data: nil, onSuccess: { _, decodedBody in + httpClient.sendRequest(.GET, toPath: path, data: nil, onSuccess: { [weak self] _, decodedBody in defer { - UserDefaults.standard.set(Date(), forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) + self?.storage.set(value: Date(), key: .inAppLastSyncedAt) syncBarrier.signal() } @@ -291,7 +310,7 @@ class InAppManager { return } - self.persistInAppMessages(messages: messagesToPersist!) + self?.persistInAppMessages(messages: messagesToPersist!) onComplete?(1) DispatchQueue.main.async { @@ -299,9 +318,15 @@ class InAppManager { return } - DispatchQueue.global(qos: .default).async { - let messagesToPresent = self.getMessagesToPresent([InAppPresented.IMMEDIATELY.rawValue]) - self.presenter.queueMessagesForPresentation(messages: messagesToPresent, tickleIds: self.pendingTickleIds) + DispatchQueue.global(qos: .default).async { [weak self] in + if let messagesToPresent = self?.getMessagesToPresent([InAppPresented.IMMEDIATELY.rawValue]), + let pendingTickleIds = self?.pendingTickleIds + { + self?.presenter.queueMessagesForPresentation( + messages: messagesToPresent, + tickleIds: pendingTickleIds + ) + } } } }, onFailure: { _, _, _ in @@ -428,8 +453,7 @@ class InAppManager { removeNotificationTickle(id: idEvicted) } - UserDefaults.standard.set(mostRecentUpdate, forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) - + storage.set(value: mostRecentUpdate, key: .inAppMostRecentUpdateAt) trackMessageDelivery(messages: messages) let inboxUpdated = fetchedWithInbox || evictedWithInbox || evictedExceedersWithInbox @@ -445,7 +469,7 @@ class InAppManager { if #available(iOS 10, *) { let tickleNotificationId = "k-in-app-message:\(id)" UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [tickleNotificationId]) - PendingNotificationHelper.remove(identifier: tickleNotificationId) + pendingNoticationHelper.remove(identifier: tickleNotificationId) } } @@ -670,24 +694,20 @@ class InAppManager { } func handlePushOpen(notification: PushNotification) { - let deepLink: [AnyHashable: Any]? = notification.inAppDeepLink() - if !inAppEnabled() || deepLink == nil { + guard let deepLink = notification.deeplink, !inAppEnabled() else { return } DispatchQueue.global(qos: .default).async { - let data = deepLink!["data"] as! [AnyHashable: Any] - let inAppPartId: Int = data["id"] as! Int - objc_sync_enter(self.pendingTickleIds) defer { objc_sync_exit(self.pendingTickleIds) } - self.pendingTickleIds.add(inAppPartId) + self.pendingTickleIds.add(deepLink.id) let messagesToPresent = self.getMessagesToPresent([]) let tickleMessageFound = messagesToPresent.contains(where: { message -> Bool in - message.id == inAppPartId + message.id == deepLink.id }) if !tickleMessageFound { @@ -811,7 +831,7 @@ class InAppManager { func markAllInboxItemsAsRead() -> Bool { var result = true - let inboxItems = OptimoveInApp.getInboxItems() + let inboxItems = OptimoveInApp.getInboxItems(storage: storage) var inboxNeedsUpdate = false for item in inboxItems { if item.isRead() { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift index a2df1185..ec1525d8 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppModels.swift @@ -3,13 +3,13 @@ import CoreData import Foundation -public enum InAppPresented: String { +enum InAppPresented: String { case IMMEDIATELY = "immediately" case NEXT_OPEN = "next-open" case NEVER = "never" } -class InAppMessageEntity: NSManagedObject { +final class InAppMessageEntity: NSManagedObject { @NSManaged var id: Int64 @NSManaged var updatedAt: NSDate @NSManaged var presentedWhen: String @@ -38,16 +38,16 @@ class InAppMessageEntity: NSManagedObject { } } -class InAppMessage: NSObject { - public internal(set) var id: Int64 - public internal(set) var updatedAt: NSDate - public internal(set) var content: NSDictionary - public internal(set) var data: NSDictionary? - public internal(set) var badgeConfig: NSDictionary? - public internal(set) var inboxConfig: NSDictionary? - public internal(set) var dismissedAt: NSDate? - public internal(set) var readAt: NSDate? - public internal(set) var sentAt: NSDate? +final class InAppMessage: NSObject { + 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? init(entity: InAppMessageEntity) { id = Int64(entity.id) diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift index ff18a6cf..21926bac 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 OptimoveCore import StoreKit import UIKit import UserNotifications @@ -31,9 +32,16 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega private var displayMode: InAppDisplayMode private var currentMessage: InAppMessage? + let pendingNoticationHelper: PendingNotificationHelper + let urlBuilder: UrlBuilder - init(displayMode: InAppDisplayMode, urlBuilder: UrlBuilder) { + init( + displayMode: InAppDisplayMode, + urlBuilder: UrlBuilder, + pendingNoticationHelper: PendingNotificationHelper + ) { + self.pendingNoticationHelper = pendingNoticationHelper messageQueue = NSMutableOrderedSet(capacity: 5) pendingTickleIds = NSMutableOrderedSet(capacity: 2) currentMessage = nil @@ -175,7 +183,7 @@ final class InAppPresenter: NSObject, WKScriptMessageHandler, WKNavigationDelega let tickleNotificationId = "k-in-app-message:\(message.id)" UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [tickleNotificationId]) - PendingNotificationHelper.remove(identifier: tickleNotificationId) + pendingNoticationHelper.remove(identifier: tickleNotificationId) } messageQueueLock.wait() diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift b/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift index c3fbbae2..f3873bb1 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Optimove. All rights reserved. import Foundation +import OptimoveCore enum AuthorizationStrategy { case basic diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift b/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift index 503577a3..c5554d0d 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore public class UrlBuilder { enum Error: LocalizedError { @@ -28,7 +29,7 @@ public class UrlBuilder { public typealias ServiceUrlMap = [Service: String] - let storage: KeyValPersistenceHelper.Type + let storage: KeyValueStorage // Overrided urls var runtimeUrlsMap: ServiceUrlMap? @@ -45,14 +46,14 @@ public class UrlBuilder { var region: String { get throws { - if let regionString = storage.object(forKey: OptimobileUserDefaultsKey.REGION.rawValue) as? String { + if let regionString: String = storage[.region] { return regionString } throw Error.regionNotSet } } - required init(storage: KeyValPersistenceHelper.Type, runtimeUrlsMap: ServiceUrlMap? = nil) { + required init(storage: KeyValueStorage, runtimeUrlsMap: ServiceUrlMap? = nil) { self.storage = storage if let runtimeUrlsMap = runtimeUrlsMap, isValidateUrlMap(urlsMap: runtimeUrlsMap) { self.runtimeUrlsMap = runtimeUrlsMap diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift index 9a78b74d..b46cb719 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore extension Optimobile { static func trackEvent(eventType: OptimobileEvent, properties: [String: Any]?, immediateFlush: Bool = false) { @@ -39,8 +40,15 @@ extension Optimobile { Parameters: - userIdentifier: Unique identifier for the current user */ - static func associateUserWithInstall(userIdentifier: String) { - associateUserWithInstallImpl(userIdentifier: userIdentifier, attributes: nil) + static func associateUserWithInstall( + userIdentifier: String, + storage: OptimoveStorage + ) { + associateUserWithInstallImpl( + userIdentifier: userIdentifier, + attributes: nil, + storage: storage + ) } /** @@ -50,16 +58,16 @@ extension Optimobile { - userIdentifier: Unique identifier for the current user - attributes: JSON encodable dictionary of attributes to store for the user */ - static func associateUserWithInstall(userIdentifier: String, attributes: [String: AnyObject]) { - associateUserWithInstallImpl(userIdentifier: userIdentifier, attributes: attributes) - } - - /** - Returns the identifier for the user currently associated with the Kumulos installation record - If no user is associated, it returns the Kumulos installation ID - */ - static var currentUserIdentifier: String { - return OptimobileHelper.currentUserIdentifier + static func associateUserWithInstall( + userIdentifier: String, + attributes: [String: AnyObject], + storage: OptimoveStorage + ) { + associateUserWithInstallImpl( + userIdentifier: userIdentifier, + attributes: attributes, + storage: storage + ) } /** @@ -67,23 +75,27 @@ extension Optimobile { See associateUserWithInstall and currentUserIdentifier for further information. */ - static func clearUserAssociation() { + static func clearUserAssociation(storage: OptimoveStorage) { OptimobileHelper.userIdLock.wait() - let currentUserId = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) as! String? + let currentUserId: String? = storage[.userID] OptimobileHelper.userIdLock.signal() Optimobile.trackEvent(eventType: OptimobileEvent.STATS_USER_ASSOCIATION_CLEARED, properties: ["oldUserIdentifier": currentUserId ?? NSNull()]) OptimobileHelper.userIdLock.wait() - KeyValPersistenceHelper.removeObject(forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) + storage.set(value: nil, key: .userID) OptimobileHelper.userIdLock.signal() - if currentUserId != nil, currentUserId != Optimobile.installId { + if currentUserId != nil, currentUserId != Optimobile.sharedInstance.installId() { getInstance().inAppManager.handleAssociatedUserChange() } } - fileprivate static func associateUserWithInstallImpl(userIdentifier: String, attributes: [String: AnyObject]?) { + fileprivate static func associateUserWithInstallImpl( + userIdentifier: String, + attributes: [String: AnyObject]?, + storage: OptimoveStorage + ) { if userIdentifier == "" { print("User identifier cannot be empty, aborting!") return @@ -97,8 +109,8 @@ extension Optimobile { } OptimobileHelper.userIdLock.wait() - let currentUserId = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) as! String? - KeyValPersistenceHelper.set(userIdentifier, forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) + let currentUserId: String? = storage[.userID] + storage.set(value: userIdentifier, key: .userID) OptimobileHelper.userIdLock.signal() Optimobile.trackEvent(eventType: OptimobileEvent.STATS_ASSOCIATE_USER, properties: params, immediateFlush: true) diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift index 2a22fcc0..f55a0e68 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift @@ -1,11 +1,12 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore import UIKit public struct DeepLinkContent { - public let title: String? - public let description: String? + let title: String? + let description: String? } public struct DeepLink { @@ -42,16 +43,18 @@ final class DeepLinkHelper { let wasDeferred: Bool } - fileprivate static let deferredLinkCheckedKey = "KUMULOS_DDL_CHECKED" - let config: OptimobileConfig let httpClient: KSHttpClient + let storage: OptimoveStorage + var anyContinuationHandled: Bool var cachedLink: CachedLink? var finishedInitializationToken: NSObjectProtocol? - init(_ config: OptimobileConfig, httpClient: KSHttpClient) { + init(_ config: OptimobileConfig, httpClient: KSHttpClient, storage: OptimoveStorage) { self.config = config self.httpClient = httpClient + self.storage = storage + anyContinuationHandled = false finishedInitializationToken = NotificationCenter.default .addObserver(forName: .optimobileInializationFinished, object: nil, queue: nil) { [weak self] notification in @@ -82,7 +85,7 @@ final class DeepLinkHelper { private func checkForDeferredLinkOnClipboard() -> Bool { var handled = false - if let checked = KeyValPersistenceHelper.object(forKey: DeepLinkHelper.deferredLinkCheckedKey) as? Bool, checked == true { + if let checked: Bool? = storage[.deferredLinkChecked], checked == true { return handled } @@ -99,7 +102,7 @@ final class DeepLinkHelper { handled = true } - KeyValPersistenceHelper.set(true, forKey: DeepLinkHelper.deferredLinkCheckedKey) + storage.set(value: true, key: .deferredLinkChecked) return handled } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift index 5f26cf3d..ae60fd29 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift @@ -1,5 +1,6 @@ import CoreLocation import Foundation +import OptimoveCore extension Optimobile { static func sendLocationUpdate(location: CLLocation) { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index d5279f64..c5eea925 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -2,85 +2,15 @@ import Foundation import ObjectiveC.runtime +import OptimoveCore import UIKit import UserNotifications -public class PushNotification: NSObject { - static let DeepLinkTypeInApp: Int = 1 - - public internal(set) var id: Int - public internal(set) var aps: [AnyHashable: Any] - public internal(set) var data: [AnyHashable: Any] - public internal(set) var url: URL? - public internal(set) var actionIdentifier: String? - - init(userInfo: [AnyHashable: Any]?) { - id = 0 - self.aps = [:] - self.data = [:] - - guard let userInfo = userInfo else { - return - } - - guard let aps = userInfo["aps"] as? [AnyHashable: Any] else { - return - } - - self.aps = aps - - guard let custom = userInfo["custom"] as? [AnyHashable: Any] else { - return - } - - guard let data = custom["a"] as? [AnyHashable: Any] else { - return - } - - self.data = data - - guard let msg = data["k.message"] as? [AnyHashable: Any] else { - return - } - - let msgData = msg["data"] as! [AnyHashable: Any] - - id = msgData["id"] as! Int - - if let urlStr = custom["u"] as? String { - url = URL(string: urlStr) - } else { - url = nil - } - } - - @available(iOS 10.0, *) - convenience init(userInfo: [AnyHashable: Any]?, response: UNNotificationResponse?) { - self.init(userInfo: userInfo) - - if let notificationResponse = response { - if notificationResponse.actionIdentifier != UNNotificationDefaultActionIdentifier { - actionIdentifier = notificationResponse.actionIdentifier - } - } - } - - public func inAppDeepLink() -> [AnyHashable: Any]? { - guard let deepLink = data["k.deepLink"] as? [AnyHashable: Any] else { - return nil - } - - if deepLink["type"] as? Int != PushNotification.DeepLinkTypeInApp { - return nil - } - - return deepLink - } -} - @available(iOS 10.0, *) public typealias OptimoveUNAuthorizationCheckedHandler = (UNAuthorizationStatus, Error?) -> Void +let KS_MESSAGE_TYPE_PUSH = 1 + extension Optimobile { /** Helper method for requesting the device token with alert, badge and sound permissions. @@ -208,35 +138,44 @@ extension Optimobile { - notification: The notification which triggered the action */ static func pushTrackOpen(notification: PushNotification) { - if notification.id == 0 { - Logger.warn(""" - Ignoring push notification open. - Reason: Invalid notification id (== 0). - Payload: \(notification). - """) - } - let params = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.id] + let params = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.message.id] Optimobile.trackEvent(eventType: OptimobileEvent.MESSAGE_OPENED, properties: params) } static func pushTrackOpen(userInfo: [AnyHashable: Any]) { - let notification = PushNotification(userInfo: userInfo) - Optimobile.pushTrackOpen(notification: notification) + do { + let notification = try PushNotification(userInfo: userInfo) + Optimobile.pushTrackOpen(notification: notification) + } catch { + Logger.error( + """ + Ignoring push notification open. + Reason: Invalid notification payload. + Payload: \(userInfo). + Error: \(error.localizedDescription). + """ + ) + } } @available(iOS 10.0, *) - func pushHandleOpen(withUserInfo: [AnyHashable: Any]?, response: UNNotificationResponse?) -> Bool { - let notification = PushNotification(userInfo: withUserInfo, response: response) - - if notification.id == 0 { + func pushHandleOpen(withUserInfo userInfo: [AnyHashable: Any]) -> Bool { + do { + let notification = try PushNotification(userInfo: userInfo) + pushHandleOpen(notification: notification) + pendingNoticationHelper.remove(id: notification.message.id) + return true + } catch { + Logger.error( + """ + Ignoring push notification open. + Reason: Invalid notification payload. + Payload: \(userInfo). + Error: \(error.localizedDescription). + """ + ) return false } - - pushHandleOpen(notification: notification) - - PendingNotificationHelper.remove(id: notification.id) - - return true } private func pushHandleOpen(notification: PushNotification) { @@ -268,21 +207,29 @@ extension Optimobile { // MARK: Dismissed handling @available(iOS 10.0, *) - func pushHandleDismissed(withUserInfo: [AnyHashable: Any]?, response: UNNotificationResponse?) -> Bool { - let notification = PushNotification(userInfo: withUserInfo, response: response) - - if notification.id == 0 { + func pushHandleDismissed(withUserInfo userInfo: [AnyHashable: Any]) -> Bool { + do { + let data = try JSONSerialization.data(withJSONObject: userInfo) + let notification = try JSONDecoder().decode(PushNotification.self, from: data) + pushHandleDismissed(notificationId: notification.message.id) + pendingNoticationHelper.remove(id: notification.message.id) + return true + } catch { + Logger.error( + """ + Ignoring push notification dismissed. + Reason: Invalid notification payload. + Payload: \(userInfo). + Error: \(error.localizedDescription). + """ + ) return false } - - pushHandleDismissed(notificationId: notification.id) - - return true } @available(iOS 10.0, *) private func pushHandleDismissed(notificationId: Int, dismissedAt: Date? = nil) { - PendingNotificationHelper.remove(id: notificationId) + pendingNoticationHelper.remove(id: notificationId) pushTrackDismissed(notificationId: notificationId, dismissedAt: dismissedAt) } @@ -299,26 +246,24 @@ extension Optimobile { @available(iOS 10.0, *) func maybeTrackPushDismissedEvents() { - if !AppGroupsHelper.isKumulosAppGroupDefined() { - return - } - - UNUserNotificationCenter.current().getDeliveredNotifications { (notifications: [UNNotification]) in - var actualPendingNotificationIds: [Int] = [] - for notification in notifications { - let notification = PushNotification(userInfo: notification.request.content.userInfo) - if notification.id == 0 { - continue + Task { + do { + let notifications = await UNUserNotificationCenter.current().deliveredNotifications() + var actualPendingNotificationIds: [Int] = [] + for notification in notifications { + let notification = try PushNotification(userInfo: notification.request.content.userInfo) + + actualPendingNotificationIds.append(notification.message.id) } - actualPendingNotificationIds.append(notification.id) - } + let recordedPendingNotifications = pendingNoticationHelper.readAll() - let recordedPendingNotifications = PendingNotificationHelper.readAll() - - let deletions = recordedPendingNotifications.filter { !actualPendingNotificationIds.contains($0.id) } - for deletion in deletions { - self.pushHandleDismissed(notificationId: deletion.id, dismissedAt: deletion.deliveredAt) + let deletions = recordedPendingNotifications.filter { !actualPendingNotificationIds.contains($0.id) } + for deletion in deletions { + pushHandleDismissed(notificationId: deletion.id, dismissedAt: deletion.deliveredAt) + } + } catch { + Logger.error("Failed to track push dismissed events: \(error.localizedDescription)") } } } @@ -399,58 +344,63 @@ class PushHelper { let didReceiveSelector = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:)) let receiveType = NSString(string: "v@:@@@?").utf8String let didReceive: didReceiveBlock = { (obj: Any, _ application: UIApplication, userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) in - let notification = PushNotification(userInfo: userInfo) - let hasInApp = notification.inAppDeepLink() != nil + do { + let notification = try PushNotification(userInfo: userInfo) + let hasInApp = notification.deeplink != nil - self.setBadge(userInfo: userInfo) - self.trackPushDelivery(notification: notification) + self.setBadge(notification: notification) + self.trackPushDelivery(notification: notification) - if existingDidReceive == nil, !hasInApp { - // Nothing to do - completionHandler(.noData) - return - } else if existingDidReceive != nil, !hasInApp { - // Only existing delegate work to do - unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, completionHandler) - return - } + if existingDidReceive == nil, !hasInApp { + // Nothing to do + completionHandler(.noData) + return + } else if existingDidReceive != nil, !hasInApp { + // Only existing delegate work to do + unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, completionHandler) + return + } - var fetchResult: UIBackgroundFetchResult = .noData - let group = DispatchGroup() + var fetchResult: UIBackgroundFetchResult = .noData + let group = DispatchGroup() - if existingDidReceive != nil { - group.enter() - DispatchQueue.main.async { - unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, { (result: UIBackgroundFetchResult) in + if existingDidReceive != nil { + group.enter() + DispatchQueue.main.async { + unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, { (result: UIBackgroundFetchResult) in + DispatchQueue.main.async { + if fetchResult == .noData { + fetchResult = result + } + + group.leave() + } + }) + } + } + + if hasInApp { + group.enter() + Optimobile.sharedInstance.inAppManager.sync { (result: Int) in DispatchQueue.main.async { - if fetchResult == .noData { - fetchResult = result + if result < 0 { + fetchResult = .failed + } else if result > 0 { + fetchResult = .newData } + // No data case is default, allow override from other handler group.leave() } - }) - } - } - - if hasInApp { - group.enter() - Optimobile.sharedInstance.inAppManager.sync { (result: Int) in - DispatchQueue.main.async { - if result < 0 { - fetchResult = .failed - } else if result > 0 { - fetchResult = .newData - } - // No data case is default, allow override from other handler - - group.leave() } } - } - group.notify(queue: .main) { - completionHandler(fetchResult) + group.notify(queue: .main) { + completionHandler(fetchResult) + } + } catch { + Logger.error("Failed to parse push notification: \(error.localizedDescription)") + completionHandler(.failed) } } let kumulosDidReceive = imp_implementationWithBlock(unsafeBitCast(didReceive, to: AnyObject.self)) @@ -463,19 +413,20 @@ class PushHelper { } }() - private func setBadge(userInfo: [AnyHashable: Any]) { - let badge: NSNumber? = OptimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) - if let newBadge = badge { - UIApplication.shared.applicationIconBadgeNumber = newBadge.intValue - } + let optimobileHelper: OptimobileHelper + + init(optimobileHelper: OptimobileHelper) { + self.optimobileHelper = optimobileHelper } - private func trackPushDelivery(notification: PushNotification) { - if notification.id == 0 { - return + private func setBadge(notification: PushNotification) { + if let badge = optimobileHelper.getBadge(notification: notification) { + UIApplication.shared.applicationIconBadgeNumber = badge } + } - let props: [String: Any] = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.id] + private func trackPushDelivery(notification: PushNotification) { + let props: [String: Any] = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.message.id] Optimobile.trackEvent(eventType: OptimobileEvent.MESSAGE_DELIVERED, properties: props, immediateFlush: true) } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift index 19b0f52f..f17792f2 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift @@ -159,7 +159,7 @@ extension Optimobile { } return [ - "hasGroup": AppGroupsHelper.isKumulosAppGroupDefined(), + "hasGroup": true, "push": push, ] } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift index c15da995..cca11234 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore import UserNotifications public typealias InAppDeepLinkHandlerBlock = (InAppButtonPress) -> Void @@ -20,8 +21,6 @@ public enum InAppDisplayMode: String { case paused } -// MARK: class - final class Optimobile { enum Error: LocalizedError { case alreadyInitialized @@ -42,12 +41,21 @@ final class Optimobile { let pushNotificationDeviceType = 1 let pushNotificationProductionTokenType: Int = 1 - let sdkType: Int = 101 - fileprivate static var instance: Optimobile? - var notificationCenter: Any? + private(set) var config: OptimobileConfig + private(set) var inAppConsentStrategy = InAppConsentStrategy.notEnabled + private(set) var inAppManager: InAppManager + private(set) var analyticsHelper: AnalyticsHelper + private(set) var sessionHelper: SessionHelper + private(set) var badgeObserver: OptimobileBadgeObserver + private var pushHelper: PushHelper + private(set) var deepLinkHelper: DeepLinkHelper? + private let networkFactory: NetworkFactory + private var credentials: OptimobileCredentials? + let pendingNoticationHelper: PendingNotificationHelper + let optimobileHelper: OptimobileHelper static var sharedInstance: Optimobile { if isInitialized() == false { @@ -61,33 +69,17 @@ final class Optimobile { return sharedInstance } - private(set) var config: OptimobileConfig - private(set) var inAppConsentStrategy = InAppConsentStrategy.notEnabled - static var inAppConsentStrategy: InAppConsentStrategy { return sharedInstance.inAppConsentStrategy } - private(set) var inAppManager: InAppManager - - private(set) var analyticsHelper: AnalyticsHelper - private(set) var sessionHelper: SessionHelper - private(set) var badgeObserver: OptimobileBadgeObserver - - private var pushHelper: PushHelper - - private(set) var deepLinkHelper: DeepLinkHelper? - - private let networkFactory: NetworkFactory - private var credentials: OptimobileCredentials? - /** The unique installation Id of the current app - Returns: String - UUID */ - static var installId: String { - return OptimobileHelper.installId + func installId() -> String { + return optimobileHelper.installId() } static func isInitialized() -> Bool { @@ -101,9 +93,12 @@ final class Optimobile { /** Initialize the Optimobile SDK. */ - static func initialize(config optimoveConfig: OptimoveConfig, initialVisitorId: String, initialUserId: String?) throws { + static func initialize( + optimoveConfig: OptimoveConfig, + storage: OptimoveStorage + ) throws { if instance !== nil, optimoveConfig.features.contains(.delayedConfiguration) { - try completeDelayedConfiguration(config: optimoveConfig.optimobileConfig!) + try completeDelayedConfiguration(config: optimoveConfig.optimobileConfig!, storage: storage) return } @@ -115,11 +110,11 @@ final class Optimobile { guard let config = optimoveConfig.optimobileConfig else { throw Error.configurationIsMissing } - - instance = Optimobile(config: config) - - writeDefaultsKeys(config: config, initialVisitorId: initialVisitorId) - + + instance = Optimobile(config: config, storage: storage) + + try writeDefaultsKeys(config: config, storage: storage) + instance!.initializeHelpers() if #available(iOS 10.0, *) { @@ -132,19 +127,19 @@ final class Optimobile { instance!.sendDeviceInformation(config: config) } - maybeAlignUserAssociation(initialUserId: initialUserId) + instance!.maybeAlignUserAssociation(storage: storage) if !optimoveConfig.features.contains(.delayedConfiguration) { NotificationCenter.default.post(name: .optimobileInializationFinished, object: nil) } } - static func completeDelayedConfiguration(config: OptimobileConfig) throws { + static func completeDelayedConfiguration(config: OptimobileConfig, storage: OptimoveStorage) throws { guard let credentials = config.credentials else { throw Error.noCredentialsProvidedForDelayedConfigurationCompletion } Logger.info("Completing delayed configuration with credentials: \(credentials)") - updateStorageValues(config) + updateStorageValues(config, storage: storage) setCredentials(credentials) NotificationCenter.default.post(name: .optimobileInializationFinished, object: nil) } @@ -153,16 +148,18 @@ final class Optimobile { Optimobile.instance?.credentials = credentials } - static func updateStorageValues(_ config: OptimobileConfig) { - KeyValPersistenceHelper.set(config.region.rawValue, forKey: OptimobileUserDefaultsKey.REGION.rawValue) + static func updateStorageValues(_ config: OptimobileConfig, storage: OptimoveStorage) { + storage.set(value: config.region.rawValue, key: .region) let baseUrlMap = UrlBuilder.defaultMapping(for: config.region.rawValue) - KeyValPersistenceHelper.set(baseUrlMap[.media], forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) + storage.set(value: baseUrlMap[.media], key: .mediaURL) } - fileprivate static func writeDefaultsKeys(config: OptimobileConfig, initialVisitorId: String) { - KeyValPersistenceHelper.maybeMigrateUserDefaultsToAppGroups() - - let existingInstallId = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.INSTALL_UUID.rawValue) as? String + fileprivate static func writeDefaultsKeys( + config: OptimobileConfig, + storage: OptimoveStorage + ) throws { + let existingInstallId: String? = storage[.installUUID] + let initialVisitorId = try storage.getInitialVisitorId() // This block handles upgrades from Kumulos SDK users to Optimove SDK users // In the case where a user was auto-enrolled into in-app messaging on the K SDK, they would not become auto-enrolled // on the new Optimove SDK installation. @@ -171,34 +168,34 @@ final class Optimobile { // we're a new install. Note comparing to `nil` isn't enough because we may have a value depending if previous storage used // app groups or not. if existingInstallId != initialVisitorId, - let _ = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) + storage[.inAppConsented] != nil { - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) + storage.set(value: nil, key: .inAppConsented) } - KeyValPersistenceHelper.set(initialVisitorId, forKey: OptimobileUserDefaultsKey.INSTALL_UUID.rawValue) + storage.set(value: initialVisitorId, key: .installUUID) if let credentials = config.credentials { setCredentials(credentials) } - updateStorageValues(config) + updateStorageValues(config, storage: storage) } - fileprivate static func maybeAlignUserAssociation(initialUserId: String?) { - if initialUserId == nil { + fileprivate func maybeAlignUserAssociation(storage: OptimoveStorage) { + guard let initialUserId: String = storage[.customerID] else { return } - let optimobileUserId = OptimobileHelper.currentUserIdentifier + let optimobileUserId = optimobileHelper.currentUserIdentifier() if optimobileUserId == initialUserId { return } - Optimobile.associateUserWithInstall(userIdentifier: initialUserId!) + Optimobile.associateUserWithInstall(userIdentifier: initialUserId, storage: storage) } - private init(config: OptimobileConfig) { + private init(config: OptimobileConfig, storage: OptimoveStorage) { self.config = config - let urlBuilder = UrlBuilder(storage: KeyValPersistenceHelper.self) + let urlBuilder = UrlBuilder(storage: storage) networkFactory = NetworkFactory( urlBuilder: urlBuilder, authorization: AuthorizationMediator(provider: { @@ -206,20 +203,39 @@ final class Optimobile { }) ) inAppConsentStrategy = config.inAppConsentStrategy - + optimobileHelper = OptimobileHelper( + storage: storage + ) analyticsHelper = AnalyticsHelper( - httpClient: networkFactory.build(for: .events) + httpClient: networkFactory.build(for: .events), + optimobileHelper: optimobileHelper ) sessionHelper = SessionHelper(sessionIdleTimeout: config.sessionIdleTimeout) - inAppManager = InAppManager(config, httpClient: networkFactory.build(for: .push), urlBuilder: urlBuilder) - pushHelper = PushHelper() + pendingNoticationHelper = PendingNotificationHelper( + storage: storage + ) + inAppManager = InAppManager( + config, + httpClient: networkFactory.build(for: .push), + urlBuilder: urlBuilder, + storage: storage, + pendingNoticationHelper: pendingNoticationHelper, + optimobileHelper: optimobileHelper + ) + pushHelper = PushHelper( + optimobileHelper: optimobileHelper + ) badgeObserver = OptimobileBadgeObserver(callback: { newBadgeCount in - KeyValPersistenceHelper.set(newBadgeCount, forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) + storage.set(value: newBadgeCount, key: .badgeCount) }) if config.deepLinkHandler != nil { - deepLinkHelper = DeepLinkHelper(config, httpClient: networkFactory.build(for: .ddl)) + deepLinkHelper = DeepLinkHelper( + config, + httpClient: networkFactory.build(for: .ddl), + storage: storage + ) } Logger.debug("Optimobile SDK was initialized with \(config)") diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift index 68f9f556..39fa95f2 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift @@ -2,23 +2,26 @@ import CoreData import Foundation - -public class InAppInboxItem { - public internal(set) var id: Int64 - public internal(set) var title: String - public internal(set) var subtitle: String - public internal(set) var availableFrom: Date? - public internal(set) var availableTo: Date? - public internal(set) var dismissedAt: Date? - public internal(set) var sentAt: Date - public internal(set) var data: NSDictionary? +import OptimoveCore + +public class InAppInboxItem: Identifiable { + public private(set) var id: Int64 + public private(set) var title: String + public private(set) var subtitle: String + public private(set) var availableFrom: Date? + public private(set) var availableTo: Date? + public private(set) var dismissedAt: Date? + public private(set) var sentAt: Date + public private(set) var data: NSDictionary? private var readAt: Date? private var imagePath: String? private static let defaultImageWidth: UInt = 300 + let mediaHelper: MediaHelper - init(entity: InAppMessageEntity) { + init(entity: InAppMessageEntity, mediaHelper: MediaHelper) { id = Int64(entity.id) + self.mediaHelper = mediaHelper let inboxConfig = entity.inboxConfig?.copy() as! [String: Any] @@ -60,7 +63,10 @@ public class InAppInboxItem { public func getImageUrl(width: UInt) -> URL? { if let imagePathNotNil = imagePath { - return MediaHelper.getCompletePictureUrl(pictureUrl: imagePathNotNil, width: width) + return try? mediaHelper.getCompletePictureUrl( + pictureUrlString: imagePathNotNil, + width: width + ) } return nil @@ -97,6 +103,13 @@ public enum OptimoveInApp { } public static func getInboxItems() -> [InAppInboxItem] { + return Optimove.shared.container.resolve { container in + let storage = container.storage() + return getInboxItems(storage: storage) + } ?? [] + } + + static func getInboxItems(storage: OptimoveStorage) -> [InAppInboxItem] { guard let context = Optimobile.sharedInstance.inAppManager.messagesContext else { return [] } @@ -123,7 +136,10 @@ public enum OptimoveInApp { } for item in items { - let inboxItem = InAppInboxItem(entity: item) + let inboxItem = InAppInboxItem( + entity: item, + mediaHelper: MediaHelper(storage: storage) + ) if inboxItem.isAvailable() == false { continue diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift index fb87420f..b091d137 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore import UserNotifications @available(iOS 10.0, *) @@ -12,19 +13,18 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - let push = PushNotification(userInfo: notification.request.content.userInfo, response: nil) + do { + let push = try PushNotification(userInfo: notification.request.content.userInfo) - if push.id == 0 { - chainCenter(center, willPresent: notification, with: completionHandler) - return - } + if Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock == nil { + completionHandler(.alert) + return + } - if Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock == nil { - completionHandler(.alert) - return + Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock?(push, completionHandler) + } catch { + chainCenter(center, willPresent: notification, with: completionHandler) } - - Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock?(push, completionHandler) } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { @@ -36,7 +36,7 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter } if response.actionIdentifier == UNNotificationDismissActionIdentifier { - let handled = Optimobile.sharedInstance.pushHandleDismissed(withUserInfo: userInfo, response: response) + let handled = Optimobile.sharedInstance.pushHandleDismissed(withUserInfo: userInfo) if !handled { chainCenter(center, didReceive: response, with: completionHandler) return @@ -46,7 +46,7 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter return } - let handled = Optimobile.sharedInstance.pushHandleOpen(withUserInfo: userInfo, response: response) + let handled = Optimobile.sharedInstance.pushHandleOpen(withUserInfo: userInfo) if !handled { chainCenter(center, didReceive: response, with: completionHandler) diff --git a/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift index d22fb931..3da83b7d 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore import UIKit class SessionIdleTimer { diff --git a/OptimoveSDK/Sources/Classes/OptimobileShared b/OptimoveSDK/Sources/Classes/OptimobileShared deleted file mode 120000 index f70d4ed1..00000000 --- a/OptimoveSDK/Sources/Classes/OptimobileShared +++ /dev/null @@ -1 +0,0 @@ -../../../OptimobileShared \ No newline at end of file diff --git a/OptimoveSDK/Sources/Classes/Optimove.swift b/OptimoveSDK/Sources/Classes/Optimove.swift index fd6a22b8..235ae38e 100644 --- a/OptimoveSDK/Sources/Classes/Optimove.swift +++ b/OptimoveSDK/Sources/Classes/Optimove.swift @@ -5,7 +5,6 @@ import OptimoveCore import UIKit.UIApplication import UserNotifications -public typealias Event = OptimoveCore.Event typealias Logger = OptimoveCore.Logger /// The Optimove SDK for iOS - a realtime customer data platform. @@ -13,13 +12,14 @@ typealias Logger = OptimoveCore.Logger /// - WARNING: /// To initialize and configure SDK using `Optimove.configure(for:)` first. @objc public final class Optimove: NSObject { + public typealias InApp = OptimoveInApp /// The current OptimoveSDK version string value. public static let version = OptimoveCore.SDKVersion /// The shared instance of Optimove SDK. @objc public static let shared: Optimove = .init() - private let container: Container + let container: Container private var config: OptimoveConfig! override private init() { @@ -53,10 +53,10 @@ typealias Logger = OptimoveCore.Logger if config.isOptimobileConfigured() { shared.container.resolve { serviceLocator in do { - let visitorId = try serviceLocator.storage().getInitialVisitorId() - let userId = try? serviceLocator.storage().getCustomerID() - - try Optimobile.initialize(config: config, initialVisitorId: visitorId, initialUserId: userId) + try Optimobile.initialize( + optimoveConfig: config, + storage: serviceLocator.storage() + ) } catch { throw GuardError.custom("Failed on OptimobileSDK initialization. Reason: \(error.localizedDescription)") } @@ -210,22 +210,24 @@ public extension Optimove { /// - email: The user email. @objc func registerUser(sdkId userID: String, email: String) { if config.isOptimoveConfigured() { - let function: (ServiceLocator) -> Void = { serviceLocator in - tryCatch { - let user = User(userID: userID) - let setUserIdEvent = try self._setUser(user, serviceLocator) - let setUserEmailEvent: Event = try self._setUserEmail(email, serviceLocator) - serviceLocator.pipeline().deliver(.report(events: [setUserIdEvent, setUserEmailEvent])) - if UserValidator(storage: serviceLocator.storage()).validateNewUser(user) == .valid { - serviceLocator.pipeline().deliver(.setInstallation) - } + container.resolve { serviceLocator in + let user = User(userID: userID) + let setUserIdEvent = try self._setUser(user, serviceLocator) + let setUserEmailEvent: Event = try self._setUserEmail(email, serviceLocator) + serviceLocator.pipeline().deliver(.report(events: [setUserIdEvent, setUserEmailEvent])) + if UserValidator(storage: serviceLocator.storage()).validateNewUser(user) == .valid { + serviceLocator.pipeline().deliver(.setInstallation) } } - container.resolve(function) } if config.isOptimobileConfigured() { - Optimobile.associateUserWithInstall(userIdentifier: userID) + container.resolve { serviceLocator in + Optimobile.associateUserWithInstall( + userIdentifier: userID, + storage: serviceLocator.storage() + ) + } } } @@ -243,21 +245,23 @@ public extension Optimove { /// - Parameter userID: The user unique identifier. @objc func setUserId(_ userID: String) { if config.isOptimoveConfigured() { - let function: (ServiceLocator) -> Void = { serviceLocator in - tryCatch { - let user = User(userID: userID) - let event = try self._setUser(user, serviceLocator) - serviceLocator.pipeline().deliver(.report(events: [event])) - if UserValidator(storage: serviceLocator.storage()).validateNewUser(user) == .valid { - serviceLocator.pipeline().deliver(.setInstallation) - } + container.resolve { serviceLocator in + let user = User(userID: userID) + let event = try self._setUser(user, serviceLocator) + serviceLocator.pipeline().deliver(.report(events: [event])) + if UserValidator(storage: serviceLocator.storage()).validateNewUser(user) == .valid { + serviceLocator.pipeline().deliver(.setInstallation) } } - container.resolve(function) } if config.isOptimobileConfigured() { - Optimobile.associateUserWithInstall(userIdentifier: userID) + container.resolve { serviceLocator in + Optimobile.associateUserWithInstall( + userIdentifier: userID, + storage: serviceLocator.storage() + ) + } } } @@ -334,17 +338,40 @@ public extension Optimove { /// Call this function to unset the customerID and revert to an anonymous visitor func signOutUser() { if config.isOptimoveConfigured() { - let function: (ServiceLocator) -> Void = { serviceLocator in - tryCatch { - serviceLocator.storage().set(value: nil, key: StorageKey.customerID) - serviceLocator.storage().set(value: serviceLocator.storage().initialVisitorId, key: StorageKey.visitorID) - } + container.resolve { serviceLocator in + serviceLocator.storage().set(value: nil, key: StorageKey.customerID) + serviceLocator.storage().set(value: serviceLocator.storage().initialVisitorId, key: StorageKey.visitorID) } - container.resolve(function) } if config.isOptimobileConfigured() { - Optimobile.clearUserAssociation() + container.resolve { serviceLocator in + Optimobile.clearUserAssociation( + storage: serviceLocator.storage() + ) + } + } + } + + enum Debug { + enum Constants { + static let undefined = "" + } + + public static var state: SdkState { + return Optimove.shared.container.resolve { locator in + let storage = locator.storage() + return SdkState( + appVersion: "\(Bundle.main.appVersion) build: \(Bundle.main.buildVersion)", + sdkVersion: Optimove.version, + installation: storage.installationID ?? Constants.undefined, + tenant: storage.tenantID?.description ?? Constants.undefined, + initialVisitor: storage.initialVisitorId ?? Constants.undefined, + customer: storage.customerID ?? Constants.undefined, + email: storage.userEmail ?? Constants.undefined, + updateVisitor: storage.visitorID ?? Constants.undefined + ) + } ?? SdkState.empty } } } diff --git a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift index 1abe9cd3..aee90cc0 100644 --- a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift +++ b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimoveCore /// A set of options for configuring the SDK. /// - Note: The SDK can be configured to support multiple features. @@ -63,11 +64,11 @@ public struct OptimoveConfig { } } -@objc public class OptimoveTenantInfo: NSObject { - @objc public var tenantToken: String - @objc public var configName: String +class OptimoveTenantInfo: NSObject { + var tenantToken: String + var configName: String - @objc public init(tenantToken: String, configName: String) { + init(tenantToken: String, configName: String) { self.tenantToken = tenantToken self.configName = configName } @@ -163,9 +164,14 @@ open class OptimoveConfigBuilder: NSObject { } override public required init() { - features = [] - urlBuilder = UrlBuilder(storage: KeyValPersistenceHelper.self) - super.init() + do { + features = [] + // FIXME: Move UrlBuilder out of OptimoveConfigBuilder + urlBuilder = try UrlBuilder(storage: UserDefaults.optimoveAppGroup()) + super.init() + } catch { + fatalError(error.localizedDescription) + } } @discardableResult func setCredentials( diff --git a/OptimoveCore/Sources/Classes/Optistream/OptistreamEvent.swift b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift similarity index 64% rename from OptimoveCore/Sources/Classes/Optistream/OptistreamEvent.swift rename to OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift index 48d8a283..d4bff2f0 100644 --- a/OptimoveCore/Sources/Classes/Optistream/OptistreamEvent.swift +++ b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEvent.swift @@ -1,25 +1,26 @@ // Copyright © 2020 Optimove. All rights reserved. import Foundation +import OptimoveCore -public struct OptistreamEvent: Codable { - public let tenant: Int - public let category: String - public let event: String - public let origin: String - public let customer: String? - public let visitor: String - public let timestamp: String - public let context: JSON - public var metadata: Metadata +struct OptistreamEvent: Codable { + let tenant: Int + let category: String + let event: String + let origin: String + let customer: String? + let visitor: String + let timestamp: String + let context: JSON + var metadata: Metadata - public struct Metadata: Codable, Hashable { - public var realtime: Bool - public var firstVisitorDate: Int64? - public let eventId: String - public let requestId: String - public let platform: String = "ios" - public let version: String = SDKVersion + struct Metadata: Codable, Hashable { + var realtime: Bool + var firstVisitorDate: Int64? + let eventId: String + let requestId: String + let platform: String = "ios" + let version: String = SDKVersion enum CodingKeys: String, CodingKey { case realtime @@ -30,7 +31,7 @@ public struct OptistreamEvent: Codable { case version = "sdk_version" } - public init( + init( realtime: Bool, firstVisitorDate: Int64?, eventId: String, @@ -43,7 +44,7 @@ public struct OptistreamEvent: Codable { } } - public init( + init( tenant: Int, category: String, event: String, @@ -67,7 +68,7 @@ public struct OptistreamEvent: Codable { } extension OptistreamEvent: Equatable { - public static func == (lhs: OptistreamEvent, rhs: OptistreamEvent) -> Bool { + static func == (lhs: OptistreamEvent, rhs: OptistreamEvent) -> Bool { return lhs.metadata.eventId == rhs.metadata.eventId } } diff --git a/OptimoveCore/Sources/Classes/Optistream/OptistreamEventBuilder.swift b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift similarity index 90% rename from OptimoveCore/Sources/Classes/Optistream/OptistreamEventBuilder.swift rename to OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift index f8fefb83..56454ca1 100644 --- a/OptimoveCore/Sources/Classes/Optistream/OptistreamEventBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Optistream/OptistreamEventBuilder.swift @@ -4,7 +4,7 @@ import Foundation /// 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. -public final class OptistreamEventBuilder { +final class OptistreamEventBuilder { enum Constants { enum Values { static let origin = "sdk" @@ -14,7 +14,7 @@ public final class OptistreamEventBuilder { private let tenantID: Int private let storage: OptimoveStorage - public init( + init( tenantID: Int, storage: OptimoveStorage ) { @@ -22,7 +22,7 @@ public final class OptistreamEventBuilder { self.storage = storage } - public func build(event: Event) throws -> OptistreamEvent { + func build(event: Event) throws -> OptistreamEvent { return try OptistreamEvent( tenant: tenantID, category: event.category, diff --git a/OptimoveCore/Sources/Classes/Optistream/OptistreamNetworking.swift b/OptimoveSDK/Sources/Classes/Optistream/OptistreamNetworking.swift similarity index 95% rename from OptimoveCore/Sources/Classes/Optistream/OptistreamNetworking.swift rename to OptimoveSDK/Sources/Classes/Optistream/OptistreamNetworking.swift index 84173a50..b552612e 100644 --- a/OptimoveCore/Sources/Classes/Optistream/OptistreamNetworking.swift +++ b/OptimoveSDK/Sources/Classes/Optistream/OptistreamNetworking.swift @@ -2,7 +2,7 @@ import Foundation -public protocol OptistreamNetworking { +protocol OptistreamNetworking { func send( events: [OptistreamEvent], path: String, @@ -15,11 +15,11 @@ public protocol OptistreamNetworking { ) } -public final class OptistreamNetworkingImpl { +final class OptistreamNetworkingImpl { private let networkClient: NetworkClient private let endpoint: URL - public init( + init( networkClient: NetworkClient, endpoint: URL ) { @@ -49,7 +49,7 @@ public final class OptistreamNetworkingImpl { } extension OptistreamNetworkingImpl: OptistreamNetworking { - public func send( + func send( events: [OptistreamEvent], path: String, completion: @escaping (Result) -> Void @@ -57,7 +57,7 @@ extension OptistreamNetworkingImpl: OptistreamNetworking { _send(events: events, path: path, completion: completion) } - public func send( + func send( events: [OptistreamEvent], completion: @escaping (Result) -> Void ) { diff --git a/OptimoveCore/Sources/Classes/Repository/ConfigurationRepository.swift b/OptimoveSDK/Sources/Classes/Repository/ConfigurationRepository.swift similarity index 77% rename from OptimoveCore/Sources/Classes/Repository/ConfigurationRepository.swift rename to OptimoveSDK/Sources/Classes/Repository/ConfigurationRepository.swift index af951e62..79ebf762 100644 --- a/OptimoveCore/Sources/Classes/Repository/ConfigurationRepository.swift +++ b/OptimoveSDK/Sources/Classes/Repository/ConfigurationRepository.swift @@ -2,7 +2,7 @@ import Foundation -public protocol ConfigurationRepository { +protocol ConfigurationRepository { func getConfiguration() throws -> Configuration func setConfiguration(_: Configuration) throws @@ -13,7 +13,7 @@ public protocol ConfigurationRepository { func saveTenant(_: TenantConfig) throws } -public final class ConfigurationRepositoryImpl { +final class ConfigurationRepositoryImpl { private enum Constants { static let fileExtension = ".json" enum Global { @@ -32,35 +32,35 @@ public final class ConfigurationRepositoryImpl { private let storage: OptimoveStorage - public init(storage: OptimoveStorage) { + init(storage: OptimoveStorage) { self.storage = storage } } extension ConfigurationRepositoryImpl: ConfigurationRepository { - public func getConfiguration() throws -> Configuration { + func getConfiguration() throws -> Configuration { return try storage.load(fileName: Constants.Configuration.fileName, isTemporary: true) } - public func setConfiguration(_ config: Configuration) throws { + func setConfiguration(_ config: Configuration) throws { try storage.save(data: config, toFileName: Constants.Configuration.fileName, isTemporary: true) } - public func getGlobal() throws -> GlobalConfig { + func getGlobal() throws -> GlobalConfig { return try storage.load(fileName: Constants.Global.fileName) } - public func saveGlobal(_ config: GlobalConfig) throws { + func saveGlobal(_ config: GlobalConfig) throws { try storage.save(data: config, toFileName: Constants.Global.fileName) } - public func getTenant() throws -> TenantConfig { + func getTenant() throws -> TenantConfig { let version = try storage.getVersion() let fileName = version + Constants.fileExtension return try storage.load(fileName: fileName, isTemporary: true) } - public func saveTenant(_ config: TenantConfig) throws { + func saveTenant(_ config: TenantConfig) throws { let version = try storage.getVersion() let fileName = version + Constants.fileExtension try storage.save(data: config, toFileName: fileName, isTemporary: true) diff --git a/OptimoveSDK/Sources/Classes/States/SdkState.swift b/OptimoveSDK/Sources/Classes/States/SdkState.swift new file mode 100644 index 00000000..7952588e --- /dev/null +++ b/OptimoveSDK/Sources/Classes/States/SdkState.swift @@ -0,0 +1,25 @@ +// Copyright © 2024 Optimove. All rights reserved. + +import Foundation + +public struct SdkState { + public let appVersion: String + public let sdkVersion: String + public let installation: String + public let tenant: String + public let initialVisitor: String + public let customer: String + public let email: String + public let updateVisitor: String + + static let empty = SdkState( + appVersion: "", + sdkVersion: "", + installation: "", + tenant: "", + initialVisitor: "", + customer: "", + email: "", + updateVisitor: "" + ) +} diff --git a/OptimoveCore/Sources/Classes/Storage/FileManager+Optimove.swift b/OptimoveSDK/Sources/Classes/Storage/FileManager+Optimove.swift similarity index 96% rename from OptimoveCore/Sources/Classes/Storage/FileManager+Optimove.swift rename to OptimoveSDK/Sources/Classes/Storage/FileManager+Optimove.swift index c0f6c3c2..d286f3d8 100644 --- a/OptimoveCore/Sources/Classes/Storage/FileManager+Optimove.swift +++ b/OptimoveSDK/Sources/Classes/Storage/FileManager+Optimove.swift @@ -4,7 +4,7 @@ import Foundation private var temporaryDirectoryURL: URL? -public extension FileManager { +extension FileManager { static func optimoveURL() throws -> URL { return try FileManager.default.url( for: .applicationSupportDirectory, diff --git a/OptimoveCore/Sources/Classes/Storage/FileStorage.swift b/OptimoveSDK/Sources/Classes/Storage/FileStorage.swift similarity index 90% rename from OptimoveCore/Sources/Classes/Storage/FileStorage.swift rename to OptimoveSDK/Sources/Classes/Storage/FileStorage.swift index b615ddb3..8842d701 100644 --- a/OptimoveCore/Sources/Classes/Storage/FileStorage.swift +++ b/OptimoveSDK/Sources/Classes/Storage/FileStorage.swift @@ -1,8 +1,9 @@ // Copyright © 2019 Optimove. All rights reserved. import Foundation +import OptimoveCore -public protocol FileStorage { +protocol FileStorage { /// Check file if exist. /// /// - Parameters: @@ -60,7 +61,7 @@ public protocol FileStorage { func delete(fileName: String, isTemporary: Bool) throws } -public extension FileStorage { +extension FileStorage { func isExist(fileName: String) -> Bool { return isExist(fileName: fileName, isTemporary: false) } @@ -86,7 +87,7 @@ public extension FileStorage { } } -public final class FileStorageImpl { +final class FileStorageImpl { enum FileStorageError: Error { case unableToCreateDirectory case unableToSaveFile @@ -102,7 +103,7 @@ public final class FileStorageImpl { let persistentStorageURL: URL let temporaryStorageURL: URL - public init(persistentStorageURL: URL, temporaryStorageURL: URL) throws { + init(persistentStorageURL: URL, temporaryStorageURL: URL) throws { fileManager = FileManager.default self.persistentStorageURL = persistentStorageURL self.temporaryStorageURL = temporaryStorageURL @@ -124,13 +125,13 @@ public final class FileStorageImpl { } extension FileStorageImpl: FileStorage { - public func isExist(fileName: String, isTemporary: Bool) -> Bool { + func isExist(fileName: String, isTemporary: Bool) -> Bool { let url = getDirectory(isTemporary: isTemporary) let fileUrl = url.appendingPathComponent(fileName) return fileManager.fileExists(atPath: fileUrl.path) } - public func loadData(fileName: String, isTemporary: Bool) throws -> Data { + func loadData(fileName: String, isTemporary: Bool) throws -> Data { let fileUrl = getDirectory(isTemporary: isTemporary).appendingPathComponent(fileName) do { let contents = try unwrap(fileManager.contents(atPath: fileUrl.path)) @@ -142,17 +143,17 @@ extension FileStorageImpl: FileStorage { } } - public func load(fileName: String, isTemporary: Bool) throws -> T where T: Decodable, T: Encodable { + func load(fileName: String, isTemporary: Bool) throws -> T where T: Decodable, T: Encodable { let data = try loadData(fileName: fileName, isTemporary: isTemporary) return try JSONDecoder().decode(T.self, from: data) } - public func save(data: T, toFileName fileName: String, isTemporary: Bool) throws { + func save(data: T, toFileName fileName: String, isTemporary: Bool) throws { let data = try JSONEncoder().encode(data) try saveData(data: data, toFileName: fileName, isTemporary: isTemporary) } - public func saveData(data: Data, toFileName fileName: String, isTemporary: Bool) throws { + func saveData(data: Data, toFileName fileName: String, isTemporary: Bool) throws { do { let url = getDirectory(isTemporary: isTemporary) try fileManager.createDirectory(at: url, withIntermediateDirectories: true) @@ -168,7 +169,7 @@ extension FileStorageImpl: FileStorage { } } - public func delete(fileName: String, isTemporary: Bool) throws { + func delete(fileName: String, isTemporary: Bool) throws { do { let fileUrl = getDirectory(isTemporary: isTemporary).appendingPathComponent(fileName) try fileManager.removeItem(at: fileUrl) diff --git a/OptimoveCore/Sources/Classes/Storage/StorageFacade.swift b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift similarity index 66% rename from OptimoveCore/Sources/Classes/Storage/StorageFacade.swift rename to OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift index ef5752ad..30ef098f 100644 --- a/OptimoveCore/Sources/Classes/Storage/StorageFacade.swift +++ b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift @@ -1,143 +1,15 @@ -// Copyright © 2019 Optimove. All rights reserved. +// Copyright © 2023 Optimove. All rights reserved. import Foundation +import OptimoveCore /// Combined protocol for a convenince access to stored values and files. -public typealias OptimoveStorage = FileStorage & KeyValueStorage & StorageValue - -// MARK: - StorageCase - -// MARK: - StorageKey - -public enum StorageKey: String, CaseIterable { - case installationID - case customerID - case configurationEndPoint - case initialVisitorId - case tenantToken - case visitorID - case version - case deviceResolutionWidth - case deviceResolutionHeight - case advertisingIdentifier - case migrationVersions /// For storing a migration history - case firstRunTimestamp - case pushNotificationChannels - case optitrackEndpoint - case tenantID - case userEmail - case siteID /// Legacy: See tenantID - case settingUserSuccess - case firstVisitTimestamp /// Legacy - - static let inMemoryValues: Set = [.tenantToken, .version] -} - -// MARK: - StorageValue - -/// The protocol used as convenience accessor to storage values. -public protocol StorageValue { - var installationID: String? { get set } - var customerID: String? { get set } - var configurationEndPoint: URL? { get set } - var initialVisitorId: String? { get set } - var tenantToken: String? { get set } - var visitorID: String? { get set } - var version: String? { get set } - var deviceResolutionWidth: Float? { get set } - var deviceResolutionHeight: Float? { get set } - var advertisingIdentifier: String? { get set } - var optitrackEndpoint: URL? { get set } - var tenantID: Int? { get set } - var userEmail: String? { get set } - /// Legacy: See tenantID - var siteID: Int? { get set } - var isSettingUserSuccess: Bool? { get set } - /// Legacy. Use `firstRunTimestamp` instead - var firstVisitTimestamp: Int64? { get set } - - func getConfigurationEndPoint() throws -> URL - func getCustomerID() throws -> String - func getInitialVisitorId() throws -> String - func getTenantToken() throws -> String - func getVisitorID() throws -> String - func getVersion() throws -> String - func getDeviceResolutionWidth() throws -> Float - func getDeviceResolutionHeight() throws -> Float - /// Called when a migration is finished for the version. - mutating func finishedMigration(to version: String) - /// Use for checking if a migration was applied for the version. - func isAlreadyMigrated(to version: String) -> Bool - func getUserEmail() throws -> String - func getSiteID() throws -> Int -} - -/// The protocol used for convenience implementation of any storage technology below this protocol. -public protocol KeyValueStorage { - func set(value: Any?, key: StorageKey) - func value(for: StorageKey) -> Any? - subscript(_: StorageKey) -> T? { get set } -} - -extension UserDefaults: KeyValueStorage { - public func set(value: Any?, key: StorageKey) { - set(value, forKey: key.rawValue) - } - - public func value(for key: StorageKey) -> Any? { - return value(forKey: key.rawValue) - } - - public subscript(key: StorageKey) -> T? { - get { - return value(for: key) as? T - } - set { - set(value: newValue, key: key) - } - } -} +typealias OptimoveStorage = FileStorage & KeyValueStorage & StorageValue -public final class InMemoryStorage: KeyValueStorage { - private var storage = [StorageKey: Any]() - private let queue = DispatchQueue(label: "com.optimove.sdk.inmemorystorage", attributes: .concurrent) - - public init() {} - - public func set(value: Any?, key: StorageKey) { - queue.async(flags: .barrier) { [self] in - storage[key] = value - } - } - - public subscript(key: StorageKey) -> T? { - get { - var result: T? - queue.sync { - result = storage[key] as? T - } - return result - } - set { - queue.async(flags: .barrier) { [self] in - storage[key] = newValue - } - } - } - - public func value(for key: StorageKey) -> Any? { - var result: Any? - queue.sync { - result = storage[key] - } - return result - } -} - -public enum StorageError: LocalizedError { +enum StorageError: LocalizedError { case noValue(StorageKey) - public var errorDescription: String? { + var errorDescription: String? { switch self { case let .noValue(key): return "StorageError: No value for key \(key.rawValue)" @@ -146,32 +18,39 @@ public enum StorageError: LocalizedError { } /// Class implements the Façade pattern for hiding complexity of the OptimoveStorage protocol. -public final class StorageFacade: OptimoveStorage { - private let persistantStorage: KeyValueStorage +final class StorageFacade: OptimoveStorage { + // FIXME: - Split persistance storage to AppGroup and Standart + private let standardStorage: KeyValueStorage + private let appGroupStorage: KeyValueStorage private let inMemoryStorage: KeyValueStorage private let fileStorage: FileStorage - public init( - persistantStorage: KeyValueStorage, + init( + standardStorage: KeyValueStorage, + appGroupStorage: KeyValueStorage, inMemoryStorage: KeyValueStorage, fileStorage: FileStorage ) { self.fileStorage = fileStorage self.inMemoryStorage = inMemoryStorage - self.persistantStorage = persistantStorage + self.appGroupStorage = appGroupStorage + self.standardStorage = standardStorage } func getStorage(for key: StorageKey) -> KeyValueStorage { if StorageKey.inMemoryValues.contains(key) { return inMemoryStorage } - return persistantStorage + if StorageKey.appGroupValues.contains(key) { + return appGroupStorage + } + return standardStorage } } // MARK: - KeyValueStorage -public extension StorageFacade { +extension StorageFacade { /// Use `storage.key` instead. /// Some variable have formatters, implemented in own setters. Set unformatted value could cause an issue. func set(value: Any?, key: StorageKey) { @@ -203,7 +82,7 @@ public extension StorageFacade { // MARK: - FileStorage -public extension StorageFacade { +extension StorageFacade { func isExist(fileName: String, isTemporary: Bool) -> Bool { return fileStorage.isExist(fileName: fileName, isTemporary: isTemporary) } @@ -231,7 +110,7 @@ public extension StorageFacade { // MARK: - StorageValue -public extension KeyValueStorage where Self: StorageValue { +extension KeyValueStorage where Self: StorageValue { var installationID: String? { get { return self[.installationID] diff --git a/OptimoveCore/Sources/Classes/Storage/UserDefaults+Optimove.swift b/OptimoveSDK/Sources/Classes/Storage/UserDefaults+Optimove.swift similarity index 91% rename from OptimoveCore/Sources/Classes/Storage/UserDefaults+Optimove.swift rename to OptimoveSDK/Sources/Classes/Storage/UserDefaults+Optimove.swift index 7cf7fd5a..43e30cf9 100644 --- a/OptimoveCore/Sources/Classes/Storage/UserDefaults+Optimove.swift +++ b/OptimoveSDK/Sources/Classes/Storage/UserDefaults+Optimove.swift @@ -1,8 +1,9 @@ // Copyright © 2019 Optimove. All rights reserved. import Foundation +import OptimoveCore -public extension UserDefaults { +extension UserDefaults { enum Constants { static let suiteName: String = "com.optimove.sdk" } diff --git a/OptimoveCore/Tests/Resources/configs.json b/OptimoveSDK/Tests/Resources/configs.json similarity index 100% rename from OptimoveCore/Tests/Resources/configs.json rename to OptimoveSDK/Tests/Resources/configs.json diff --git a/OptimoveCore/Tests/Resources/dev.tid.107.optipush.json b/OptimoveSDK/Tests/Resources/dev.tid.107.optipush.json similarity index 100% rename from OptimoveCore/Tests/Resources/dev.tid.107.optipush.json rename to OptimoveSDK/Tests/Resources/dev.tid.107.optipush.json diff --git a/OptimoveCore/Tests/Sources/Configurations/ConfigurationBuilderTests.swift b/OptimoveSDK/Tests/Sources/Configurations/ConfigurationBuilderTests.swift similarity index 96% rename from OptimoveCore/Tests/Sources/Configurations/ConfigurationBuilderTests.swift rename to OptimoveSDK/Tests/Sources/Configurations/ConfigurationBuilderTests.swift index 4bce5f2c..9bf0a3a8 100644 --- a/OptimoveCore/Tests/Sources/Configurations/ConfigurationBuilderTests.swift +++ b/OptimoveSDK/Tests/Sources/Configurations/ConfigurationBuilderTests.swift @@ -1,6 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. -@testable import OptimoveCore +@testable import OptimoveSDK import OptimoveTest import XCTest diff --git a/OptimoveCore/Tests/Sources/Configurations/Global/GlobalConfigTests.swift b/OptimoveSDK/Tests/Sources/Configurations/Global/GlobalConfigTests.swift similarity index 97% rename from OptimoveCore/Tests/Sources/Configurations/Global/GlobalConfigTests.swift rename to OptimoveSDK/Tests/Sources/Configurations/Global/GlobalConfigTests.swift index b406aa7f..a9d31e8d 100644 --- a/OptimoveCore/Tests/Sources/Configurations/Global/GlobalConfigTests.swift +++ b/OptimoveSDK/Tests/Sources/Configurations/Global/GlobalConfigTests.swift @@ -1,6 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. -@testable import OptimoveCore +@testable import OptimoveSDK import OptimoveTest import XCTest diff --git a/OptimoveCore/Tests/Sources/Configurations/Tenant/TenantConfigTests.swift b/OptimoveSDK/Tests/Sources/Configurations/Tenant/TenantConfigTests.swift similarity index 97% rename from OptimoveCore/Tests/Sources/Configurations/Tenant/TenantConfigTests.swift rename to OptimoveSDK/Tests/Sources/Configurations/Tenant/TenantConfigTests.swift index 63831896..987f9dcf 100644 --- a/OptimoveCore/Tests/Sources/Configurations/Tenant/TenantConfigTests.swift +++ b/OptimoveSDK/Tests/Sources/Configurations/Tenant/TenantConfigTests.swift @@ -1,6 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. -@testable import OptimoveCore +@testable import OptimoveSDK import OptimoveTest import XCTest diff --git a/OptimoveSDK/Tests/Sources/Mocks/MockConfigurationRepository.swift b/OptimoveSDK/Tests/Sources/Mocks/MockConfigurationRepository.swift index 6616468d..f70ca926 100644 --- a/OptimoveSDK/Tests/Sources/Mocks/MockConfigurationRepository.swift +++ b/OptimoveSDK/Tests/Sources/Mocks/MockConfigurationRepository.swift @@ -1,5 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. +@testable import OptimoveSDK import OptimoveCore final class MockConfigurationRepository: ConfigurationRepository { diff --git a/OptimoveCore/Tests/Sources/NetworkClientTests.swift b/OptimoveSDK/Tests/Sources/NetworkClientTests.swift similarity index 98% rename from OptimoveCore/Tests/Sources/NetworkClientTests.swift rename to OptimoveSDK/Tests/Sources/NetworkClientTests.swift index 15ebdd62..423f1f0a 100644 --- a/OptimoveCore/Tests/Sources/NetworkClientTests.swift +++ b/OptimoveSDK/Tests/Sources/NetworkClientTests.swift @@ -1,7 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. import Mocker -@testable import OptimoveCore +@testable import OptimoveSDK import OptimoveTest import XCTest diff --git a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift index 80e418bf..991cc2c2 100644 --- a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift +++ b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift @@ -1,5 +1,6 @@ // Copyright © 2023 Optimove. All rights reserved. +import OptimoveCore @testable import OptimoveSDK import XCTest @@ -41,7 +42,7 @@ final class OptimoveConfigBuilderTests: XCTestCase { } func testBaseUrlMapping() throws { - KeyValPersistenceHelper.set(Region.DEV.rawValue, forKey: OptimobileUserDefaultsKey.REGION.rawValue) + try UserDefaults.optimoveAppGroup().set(value: Region.DEV.rawValue, key: .region) let urlsMap = UrlBuilder.defaultMapping(for: Region.DEV.rawValue) let config = optimoveConfigBuilder.build() try UrlBuilder.Service.allCases.forEach { service in diff --git a/OptimoveCore/Tests/Sources/Storage/KeyValueStorageTests.swift b/OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift similarity index 97% rename from OptimoveCore/Tests/Sources/Storage/KeyValueStorageTests.swift rename to OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift index c9872600..7e4544a6 100644 --- a/OptimoveCore/Tests/Sources/Storage/KeyValueStorageTests.swift +++ b/OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift @@ -1,6 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. -@testable import OptimoveCore +import OptimoveCore +@testable import OptimoveSDK import OptimoveTest import XCTest @@ -61,7 +62,8 @@ class KeyValueStorageTests: XCTestCase { override func setUp() { storage = StorageFacade( - persistantStorage: MockKeyValueStorage(), + standardStorage: MockKeyValueStorage(), + appGroupStorage: MockKeyValueStorage(), inMemoryStorage: MockKeyValueStorage(), fileStorage: MockFileStorage() ) diff --git a/OptimoveCore/Tests/Sources/Storage/OptimoveFileManagerTests.swift b/OptimoveSDK/Tests/Sources/Storage/OptimoveFileManagerTests.swift similarity index 98% rename from OptimoveCore/Tests/Sources/Storage/OptimoveFileManagerTests.swift rename to OptimoveSDK/Tests/Sources/Storage/OptimoveFileManagerTests.swift index 3cff64a3..f41640fe 100644 --- a/OptimoveCore/Tests/Sources/Storage/OptimoveFileManagerTests.swift +++ b/OptimoveSDK/Tests/Sources/Storage/OptimoveFileManagerTests.swift @@ -1,6 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. -@testable import OptimoveCore +@testable import OptimoveSDK import XCTest final class MockedFileManager: FileManager { diff --git a/OptimoveCore/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift b/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift similarity index 90% rename from OptimoveCore/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift rename to OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift index 27a3745a..20406a0c 100644 --- a/OptimoveCore/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift +++ b/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift @@ -1,6 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. -@testable import OptimoveCore +import OptimoveCore +@testable import OptimoveSDK import XCTest class OptimoveStorageFacadeTests: XCTestCase { @@ -8,7 +9,8 @@ class OptimoveStorageFacadeTests: XCTestCase { override func setUp() { storage = StorageFacade( - persistantStorage: MockKeyValueStorage(), + standardStorage: MockKeyValueStorage(), + appGroupStorage: MockKeyValueStorage(), inMemoryStorage: MockKeyValueStorage(), fileStorage: MockFileStorage() ) diff --git a/Package.swift b/Package.swift index 4342fc11..73b21dea 100644 --- a/Package.swift +++ b/Package.swift @@ -1,12 +1,11 @@ // swift-tools-version:5.3 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Optimove", platforms: [ - .iOS(.v10), + .iOS(.v13), .macOS(.v10_14), ], products: [ @@ -40,12 +39,15 @@ let package = Package( ), .target( name: "OptimoveNotificationServiceExtension", + dependencies: [ + "OptimoveCore", + ], path: "OptimoveNotificationServiceExtension/Sources" ), .target( name: "OptimoveTest", dependencies: [ - "OptimoveCore", + "OptimoveSDK", ], path: "Shared", resources: [ @@ -78,7 +80,10 @@ let package = Package( ), .testTarget( name: "OptimoveNotificationServiceExtensionTests", - dependencies: ["OptimoveNotificationServiceExtension"], + dependencies: [ + "OptimoveNotificationServiceExtension", + "OptimoveTest", + ], path: "OptimoveNotificationServiceExtension/Tests" ), ], diff --git a/Shared/Sources/Fixture/ConfigurationFixture.swift b/Shared/Sources/Fixture/ConfigurationFixture.swift index 53b7545c..4e572bb6 100644 --- a/Shared/Sources/Fixture/ConfigurationFixture.swift +++ b/Shared/Sources/Fixture/ConfigurationFixture.swift @@ -1,7 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. import Foundation -@testable import OptimoveCore +@testable import OptimoveSDK public struct Options { public let isEnableRealtime: Bool diff --git a/Shared/Sources/Fixture/GlobalConfigFixture.swift b/Shared/Sources/Fixture/GlobalConfigFixture.swift index 4c1b7a2a..435ea587 100644 --- a/Shared/Sources/Fixture/GlobalConfigFixture.swift +++ b/Shared/Sources/Fixture/GlobalConfigFixture.swift @@ -1,7 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. import Foundation -@testable import OptimoveCore +@testable import OptimoveSDK public final class GlobalConfigFixture: FileAccessible { public let fileName: String = "core_events.json" diff --git a/Shared/Sources/Fixture/TenantConfigFixture.swift b/Shared/Sources/Fixture/TenantConfigFixture.swift index 7a7e0af7..ab2d1b6e 100644 --- a/Shared/Sources/Fixture/TenantConfigFixture.swift +++ b/Shared/Sources/Fixture/TenantConfigFixture.swift @@ -1,7 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. import Foundation -import OptimoveCore +@testable import OptimoveSDK public final class TenantConfigFixture { public init() {} diff --git a/Shared/Sources/Mocks/MockOptimoveStorage.swift b/Shared/Sources/Mocks/MockOptimoveStorage.swift index d98e7cae..d0fa7a71 100644 --- a/Shared/Sources/Mocks/MockOptimoveStorage.swift +++ b/Shared/Sources/Mocks/MockOptimoveStorage.swift @@ -2,6 +2,7 @@ import Foundation import OptimoveCore +@testable import OptimoveSDK public final class MockOptimoveStorage: OptimoveStorage { public init() {} diff --git a/Shared/Sources/Mocks/OptistreamNetworkingMock.swift b/Shared/Sources/Mocks/OptistreamNetworkingMock.swift index 64d1b169..ac76aa88 100644 --- a/Shared/Sources/Mocks/OptistreamNetworkingMock.swift +++ b/Shared/Sources/Mocks/OptistreamNetworkingMock.swift @@ -1,6 +1,6 @@ // Copyright © 2020 Optimove. All rights reserved. -import OptimoveCore +@testable import OptimoveSDK public final class OptistreamNetworkingMock: OptistreamNetworking { public init() {} diff --git a/Shared/Sources/OptimoveTestCase.swift b/Shared/Sources/OptimoveTestCase.swift index c9596e4a..36c93f96 100644 --- a/Shared/Sources/OptimoveTestCase.swift +++ b/Shared/Sources/OptimoveTestCase.swift @@ -1,6 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. import OptimoveCore +@testable import OptimoveSDK import XCTest public let defaultTimeout: TimeInterval = 0.8 diff --git a/Shared/Sources/StubEvent.swift b/Shared/Sources/StubEvent.swift index 446bc530..439d3a5c 100644 --- a/Shared/Sources/StubEvent.swift +++ b/Shared/Sources/StubEvent.swift @@ -1,7 +1,7 @@ // Copyright © 2019 Optimove. All rights reserved. import Foundation -@testable import OptimoveCore +@testable import OptimoveSDK public final class StubEvent: Event { public enum Constnats {