From 9a121c69be51c83449c9f965728c613f7d39a002 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 14:14:39 +0200 Subject: [PATCH 01/12] refactor: split storage facade to files --- .../Classes/Storage/KeyValueStorage.swift | 145 ++++++++++++++++++ .../Classes/Storage/OptimoveStorage.swift | 4 + .../Classes/Storage/StorageFacade.swift | 135 ---------------- 3 files changed, 149 insertions(+), 135 deletions(-) create mode 100644 OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift create mode 100644 OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift diff --git a/OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift b/OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift new file mode 100644 index 00000000..f43528ab --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift @@ -0,0 +1,145 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +/// The enum used as keys for storage values. +enum StorageKey: String, CaseIterable { + case installationID + case customerID + case configurationEndPoint + case initialVisitorId + case tenantToken + case visitorID + case version + case userAgent + 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 + + static let inMemoryValues: Set = [.tenantToken, .version] + static let appGroup: Set = [.mediaURL, .dynamicCategory] +} + +/// The protocol used as convenience accessor to storage values. +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 userAgent: 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 getUserAgent() 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. +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 { + func set(value: Any?, key: StorageKey) { + set(value, forKey: key.rawValue) + } + + func value(for key: StorageKey) -> Any? { + return value(forKey: key.rawValue) + } + + subscript(key: StorageKey) -> T? { + get { + return value(for: key) as? T + } + set { + set(value: newValue, key: key) + } + } +} + +/// ``InMemoryStorage`` uses as in-memory ``KeyValueStorage``. +final class InMemoryStorage: KeyValueStorage { + private var storage = [StorageKey: Any]() + private let queue = DispatchQueue(label: "com.optimove.sdk.inmemorystorage", attributes: .concurrent) + + init() {} + + func set(value: Any?, key: StorageKey) { + queue.async(flags: .barrier) { [self] in + storage[key] = value + } + } + + 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 + } + } + } + + func value(for key: StorageKey) -> Any? { + var result: Any? + queue.sync { + result = storage[key] + } + return result + } +} diff --git a/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift new file mode 100644 index 00000000..51107fcf --- /dev/null +++ b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift @@ -0,0 +1,4 @@ +// Copyright © 2023 Optimove. All rights reserved. + +/// Combined protocol for a convenince access to stored values and files. +typealias OptimoveStorage = FileStorage & KeyValueStorage & StorageValue diff --git a/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift b/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift index ab63d8ff..750c5eea 100644 --- a/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift +++ b/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift @@ -3,141 +3,6 @@ import Foundation import OptimoveCore -/// Combined protocol for a convenince access to stored values and files. -typealias OptimoveStorage = FileStorage & KeyValueStorage & StorageValue - -// MARK: - StorageCase - -// MARK: - StorageKey - -enum StorageKey: String, CaseIterable { - case installationID - case customerID - case configurationEndPoint - case initialVisitorId - case tenantToken - case visitorID - case version - case userAgent - 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. -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 userAgent: 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 getUserAgent() 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. -protocol KeyValueStorage { - func set(value: Any?, key: StorageKey) - func value(for: StorageKey) -> Any? - subscript(_: StorageKey) -> T? { get set } -} - -extension UserDefaults: KeyValueStorage { - func set(value: Any?, key: StorageKey) { - set(value, forKey: key.rawValue) - } - - func value(for key: StorageKey) -> Any? { - return value(forKey: key.rawValue) - } - - subscript(key: StorageKey) -> T? { - get { - return value(for: key) as? T - } - set { - set(value: newValue, key: key) - } - } -} - -final class InMemoryStorage: KeyValueStorage { - private var storage = [StorageKey: Any]() - private let queue = DispatchQueue(label: "com.optimove.sdk.inmemorystorage", attributes: .concurrent) - - init() {} - - func set(value: Any?, key: StorageKey) { - queue.async(flags: .barrier) { [self] in - storage[key] = value - } - } - - 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 - } - } - } - - func value(for key: StorageKey) -> Any? { - var result: Any? - queue.sync { - result = storage[key] - } - return result - } -} - enum StorageError: LocalizedError { case noValue(StorageKey) From 615864e1316760d0fdeecabebc2573ef0cd54457 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 14:14:50 +0200 Subject: [PATCH 02/12] feat: add migration 5.7.0 --- .../Classes/OptimobileUserDefaultsKey.swift | 2 +- OptimoveSDK/Sources/Classes/DI/Assembly.swift | 1 + .../Classes/Migration/MigrationWork.swift | 62 ++++++++++++++++--- .../Sources/Classes/Migration/Version.swift | 1 + 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift index 4c290b0a..3fab605c 100644 --- a/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift +++ b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift @@ -2,7 +2,7 @@ import Foundation -public enum OptimobileUserDefaultsKey: String { +public enum OptimobileUserDefaultsKey: String, CaseIterable { case REGION = "KumulosEventsRegion" case MEDIA_BASE_URL = "KumulosMediaBaseUrl" case INSTALL_UUID = "KumulosUUID" diff --git a/OptimoveSDK/Sources/Classes/DI/Assembly.swift b/OptimoveSDK/Sources/Classes/DI/Assembly.swift index 5b85aa56..777bf0ff 100644 --- a/OptimoveSDK/Sources/Classes/DI/Assembly.swift +++ b/OptimoveSDK/Sources/Classes/DI/Assembly.swift @@ -33,6 +33,7 @@ final class Assembly { private func migrate() { let migrations: [MigrationWork] = [ MigrationWork_3_3_0(), + MigrationWork_5_7_0(), ] migrations .filter { $0.isAllowToMiragte(SDKVersion) } diff --git a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift index 4deddd58..573eafec 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 @@ -57,9 +61,10 @@ class MigrationWorkerWithStorage: MigrationWorker { final class MigrationWork_2_10_0: MigrationWorkerWithStorage { private let synchronizer: Pipeline - init(synchronizer: Pipeline, - storage: OptimoveStorage) - { + init( + synchronizer: Pipeline, + storage: OptimoveStorage + ) { self.synchronizer = synchronizer super.init(storage: storage, newVersion: .v_2_10_0) } @@ -108,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() { @@ -229,3 +230,50 @@ extension MigrationWork_3_3_0 { } } } + +/// Migration from Kumulos UserDefaults to Optimove UserDefaults. +final class MigrationWork_5_7_0: MigrationWorker { + init() { + super.init(newVersion: .v_5_7_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, + // TODO: MIGRATED_TO_GROUPS + ] + /// 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") + super.runMigration() + } +} diff --git a/OptimoveSDK/Sources/Classes/Migration/Version.swift b/OptimoveSDK/Sources/Classes/Migration/Version.swift index e55b4653..c1c172ca 100644 --- a/OptimoveSDK/Sources/Classes/Migration/Version.swift +++ b/OptimoveSDK/Sources/Classes/Migration/Version.swift @@ -4,4 +4,5 @@ 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_5_7_0 = "5.7.0" } From b4f1c8b7c9bf456ccbb43be4c5c5043011cd4287 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 16:51:47 +0200 Subject: [PATCH 03/12] feat: remove kumulos key-value storage --- .../Classes/Extension/Bundle+HostApp.swift | 15 +- .../Classes/KeyValPersistenceHelper.swift | 59 --- .../Sources/Classes/MediaHelper.swift | 12 +- .../Sources/Classes/OptimobileHelper.swift | 38 +- .../Classes/OptimobileUserDefaultsKey.swift | 3 - .../Classes/PendingNotificationHelper.swift | 24 +- .../Storage/FileManager+AppGroup.swift | 18 + .../Classes/Storage/KeyValueStorage.swift | 34 +- .../Storage/UserDefaults+AppGroup.swift | 18 + .../Tests/Sources/MediaHelperTests.swift | 15 +- .../Sources/OptimoveNotificationService.swift | 37 +- .../Classes/Migration/MigrationWork.swift | 1 - .../Classes/Optimobile/AnalyticsHelper.swift | 11 +- .../Optimobile/InApp/InAppManager.swift | 30 +- .../Optimobile/InApp/InAppPresenter.swift | 11 +- .../Optimobile/Network/UrlBuilder.swift | 6 +- .../Optimobile/Optimobile+Analytics.swift | 49 ++- .../Optimobile/Optimobile+DeepLinking.swift | 10 +- .../Classes/Optimobile/Optimobile+Push.swift | 18 +- .../Classes/Optimobile/Optimobile+Stats.swift | 2 +- .../Classes/Optimobile/Optimobile.swift | 121 +++--- .../Classes/Optimobile/OptimoveInApp.swift | 13 +- OptimoveSDK/Sources/Classes/Optimove.swift | 69 +-- .../Sources/Classes/OptimoveConfig.swift | 2 +- .../Classes/Storage/OptimoveStorage.swift | 398 +++++++++++++++++ .../Classes/Storage/StorageFacade.swift | 399 ------------------ .../Sources/OptimoveConfigBuilderTests.swift | 2 +- .../Storage/OptimoveStorageFacadeTests.swift | 1 + 28 files changed, 758 insertions(+), 658 deletions(-) delete mode 100644 OptimoveCore/Sources/Classes/KeyValPersistenceHelper.swift create mode 100644 OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift rename {OptimoveSDK => OptimoveCore}/Sources/Classes/Storage/KeyValueStorage.swift (82%) create mode 100644 OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift delete mode 100644 OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift diff --git a/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift b/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift index 6f0d73de..3f96df9b 100644 --- a/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift +++ b/OptimoveCore/Sources/Classes/Extension/Bundle+HostApp.swift @@ -3,11 +3,12 @@ import Foundation 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 @@ 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/KeyValPersistenceHelper.swift b/OptimoveCore/Sources/Classes/KeyValPersistenceHelper.swift deleted file mode 100644 index e3091138..00000000 --- a/OptimoveCore/Sources/Classes/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) -} - -public enum KeyValPersistenceHelper { - public 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 { - public static func set(_ value: Any?, forKey: String) { - getUserDefaults().set(value, forKey: forKey) - } - - public static func object(forKey: String) -> Any? { - return getUserDefaults().object(forKey: forKey) - } - - public static func removeObject(forKey: String) { - getUserDefaults().removeObject(forKey: forKey) - } -} diff --git a/OptimoveCore/Sources/Classes/MediaHelper.swift b/OptimoveCore/Sources/Classes/MediaHelper.swift index f9a1e575..161ec0cd 100644 --- a/OptimoveCore/Sources/Classes/MediaHelper.swift +++ b/OptimoveCore/Sources/Classes/MediaHelper.swift @@ -2,13 +2,19 @@ import Foundation -public enum MediaHelper { +public struct MediaHelper { enum Error: LocalizedError { case noMediaUrlFound case invalidPictureUrl(String) } - public static func getCompletePictureUrl(pictureUrlString: String, width: UInt) throws -> URL { + 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) @@ -16,7 +22,7 @@ public enum MediaHelper { return url } - guard let mediaUrl = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) as? String else { + guard let mediaUrl: String = storage[.mediaURL] else { throw Error.noMediaUrlFound } diff --git a/OptimoveCore/Sources/Classes/OptimobileHelper.swift b/OptimoveCore/Sources/Classes/OptimobileHelper.swift index 13dc6448..f94a5f6c 100644 --- a/OptimoveCore/Sources/Classes/OptimobileHelper.swift +++ b/OptimoveCore/Sources/Classes/OptimobileHelper.swift @@ -2,23 +2,28 @@ import Foundation -public enum OptimobileHelper { - private static let installIdLock = DispatchSemaphore(value: 1) +public struct OptimobileHelper { + static let installIdLock = DispatchSemaphore(value: 1) public static let userIdLock = DispatchSemaphore(value: 1) - public static var installId: String { - installIdLock.wait() + let storage: KeyValueStorage + + public init(storage: KeyValueStorage) { + self.storage = storage + } + + public func installId() -> String { + OptimobileHelper.installIdLock.wait() defer { - installIdLock.signal() + OptimobileHelper.installIdLock.signal() } - if let existingID = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.INSTALL_UUID.rawValue) { - return existingID as! String + if let existingID: String = storage[.installUUID] { + return existingID } let newID = UUID().uuidString - KeyValPersistenceHelper.set(newID, forKey: OptimobileUserDefaultsKey.INSTALL_UUID.rawValue) - + storage.set(value: newID, key: .installUUID) return newID } @@ -27,17 +32,18 @@ public enum OptimobileHelper { If no user is associated, it returns the Kumulos installation ID */ - public static var currentUserIdentifier: String { - userIdLock.wait() - defer { userIdLock.signal() } - if let userId = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) as! String? { + public func currentUserIdentifier() -> String { + OptimobileHelper.userIdLock.wait() + defer { OptimobileHelper.userIdLock.signal() } + if let userId: String = storage[.userID] { return userId } - return OptimobileHelper.installId + return installId() } - public static func getBadgeFromUserInfo(userInfo: [AnyHashable: Any]) -> NSNumber? { + // FIXME: Use PushNotifcation + public func getBadgeFromUserInfo(userInfo: [AnyHashable: Any]) -> NSNumber? { let custom = userInfo["custom"] as? [AnyHashable: Any] let aps = userInfo["aps"] as? [AnyHashable: Any] @@ -53,7 +59,7 @@ public enum OptimobileHelper { } var newBadge: NSNumber? = badge - if let incrementBy = incrementBy, let currentVal = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) as? NSNumber { + if let incrementBy = incrementBy, let currentVal: NSNumber = storage[.badgeCount] { newBadge = NSNumber(value: currentVal.intValue + incrementBy.intValue) if newBadge!.intValue < 0 { diff --git a/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift index 3fab605c..824bc186 100644 --- a/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift +++ b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift @@ -10,9 +10,6 @@ public enum OptimobileUserDefaultsKey: String, CaseIterable { 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/PendingNotificationHelper.swift b/OptimoveCore/Sources/Classes/PendingNotificationHelper.swift index f55356bf..405de69c 100644 --- a/OptimoveCore/Sources/Classes/PendingNotificationHelper.swift +++ b/OptimoveCore/Sources/Classes/PendingNotificationHelper.swift @@ -2,8 +2,14 @@ import Foundation -public enum PendingNotificationHelper { - public 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 @@ public enum PendingNotificationHelper { } } - public 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 @@ public enum PendingNotificationHelper { } } - public 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 @@ public enum PendingNotificationHelper { return pendingNotifications } - public 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 @@ public enum PendingNotificationHelper { save(pendingNotifications: pendingNotifications) } - 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/Storage/FileManager+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift new file mode 100644 index 00000000..116c9c9d --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift @@ -0,0 +1,18 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public extension FileManager { + static func optimoveAppGroupURL() -> 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 + """ + fatalError(message) + } + return url + } +} diff --git a/OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift similarity index 82% rename from OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift rename to OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift index f43528ab..de9fca90 100644 --- a/OptimoveSDK/Sources/Classes/Storage/KeyValueStorage.swift +++ b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift @@ -3,7 +3,7 @@ import Foundation /// The enum used as keys for storage values. -enum StorageKey: String, CaseIterable { +public enum StorageKey: String, CaseIterable { case installationID case customerID case configurationEndPoint @@ -36,13 +36,21 @@ enum StorageKey: String, CaseIterable { case inAppMostRecentUpdateAt case inAppConsented case dynamicCategory + case deferredLinkChecked = "KUMULOS_DDL_CHECKED" - static let inMemoryValues: Set = [.tenantToken, .version] - static let appGroup: Set = [.mediaURL, .dynamicCategory] + public static let inMemoryValues: Set = [.tenantToken, .version] + public static let appGroup: Set = [ + .badgeCount, + .dynamicCategory, + .installUUID, + .mediaURL, + .pendingNotifications, + .userID, + ] } /// The protocol used as convenience accessor to storage values. -protocol StorageValue { +public protocol StorageValue { var installationID: String? { get set } var customerID: String? { get set } var configurationEndPoint: URL? { get set } @@ -81,7 +89,7 @@ protocol StorageValue { } /// The protocol used for convenience implementation of any storage technology below this protocol. -protocol KeyValueStorage { +public protocol KeyValueStorage { func set(value: Any?, key: StorageKey) func value(for: StorageKey) -> Any? subscript(_: StorageKey) -> T? { get set } @@ -89,15 +97,15 @@ protocol KeyValueStorage { /// ``UserDefaults`` uses as persistent ``KeyValueStorage``. extension UserDefaults: KeyValueStorage { - func set(value: Any?, key: StorageKey) { + public func set(value: Any?, key: StorageKey) { set(value, forKey: key.rawValue) } - func value(for key: StorageKey) -> Any? { + public func value(for key: StorageKey) -> Any? { return value(forKey: key.rawValue) } - subscript(key: StorageKey) -> T? { + public subscript(key: StorageKey) -> T? { get { return value(for: key) as? T } @@ -108,19 +116,19 @@ extension UserDefaults: KeyValueStorage { } /// ``InMemoryStorage`` uses as in-memory ``KeyValueStorage``. -final class InMemoryStorage: KeyValueStorage { +public final class InMemoryStorage: KeyValueStorage { private var storage = [StorageKey: Any]() private let queue = DispatchQueue(label: "com.optimove.sdk.inmemorystorage", attributes: .concurrent) - init() {} + public init() {} - func set(value: Any?, key: StorageKey) { + public func set(value: Any?, key: StorageKey) { queue.async(flags: .barrier) { [self] in storage[key] = value } } - subscript(key: StorageKey) -> T? { + public subscript(key: StorageKey) -> T? { get { var result: T? queue.sync { @@ -135,7 +143,7 @@ final class InMemoryStorage: KeyValueStorage { } } - func value(for key: StorageKey) -> Any? { + public func value(for key: StorageKey) -> Any? { var result: Any? queue.sync { result = storage[key] diff --git a/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift new file mode 100644 index 00000000..74096c35 --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift @@ -0,0 +1,18 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public extension UserDefaults { + static func optimoveAppGroup() -> 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 + """ + fatalError(message) + } + return userDefaults + } +} diff --git a/OptimoveCore/Tests/Sources/MediaHelperTests.swift b/OptimoveCore/Tests/Sources/MediaHelperTests.swift index 580e1006..ba33d984 100644 --- a/OptimoveCore/Tests/Sources/MediaHelperTests.swift +++ b/OptimoveCore/Tests/Sources/MediaHelperTests.swift @@ -1,20 +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) + 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" - KeyValPersistenceHelper.set(mediaUrl, forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) - let url = try MediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) + storage.set(value: mediaUrl, key: .mediaURL) + let url = try mediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) XCTAssertEqual(url.absoluteString, "\(mediaUrl)/100x/\(pictureUrlString)") } } diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 9c79734c..cc77fb91 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -39,12 +39,22 @@ public enum OptimoveNotificationService { if bestAttemptContent.categoryIdentifier.isEmpty { bestAttemptContent.categoryIdentifier = await buildCategory(notification: notification) } - if AppGroupsHelper.isKumulosAppGroupDefined() { - if let attachment = try await maybeGetAttachment(notification: notification) { + if AppGroupsHelper.isAppGroupDefined() { + let storage = UserDefaults.optimoveAppGroup() + let mediaHelper = MediaHelper(storage: storage) + if let attachment = try await maybeGetAttachment( + notification: notification, + mediaHelper: mediaHelper + ) { bestAttemptContent.attachments = [attachment] } - maybeSetBadge(bestAttemptContent: bestAttemptContent, userInfo: userInfo) - PendingNotificationHelper.add( + let optimobileHelper = OptimobileHelper(storage: storage) + if let badge = maybeSetBadge(userInfo: userInfo, optimobileHelper: optimobileHelper) { + storage.set(value: badge, key: .badgeCount) + bestAttemptContent.badge = badge + } + let pendingNoticationHelper = PendingNotificationHelper(storage: storage) + pendingNoticationHelper.add( notification: PendingNotification( id: notification.message.id, identifier: request.identifier @@ -98,10 +108,10 @@ public enum OptimoveNotificationService { return categoryIdentifier } - static func maybeGetAttachment(notification: PushNotification) async throws -> UNNotificationAttachment? { + static func maybeGetAttachment(notification: PushNotification, mediaHelper: MediaHelper) async throws -> UNNotificationAttachment? { guard let picturePath = notification.attachment?.pictureUrl else { return nil } - let url = try await MediaHelper.getCompletePictureUrl( + let url = try await mediaHelper.getCompletePictureUrl( pictureUrlString: picturePath, width: UInt(floor(UIScreen.main.bounds.size.width)) ) @@ -122,18 +132,19 @@ public enum OptimoveNotificationService { ) } - static func maybeSetBadge(bestAttemptContent: UNMutableNotificationContent, userInfo: [AnyHashable: Any]) { + static func maybeSetBadge( + userInfo: [AnyHashable: Any], + optimobileHelper: OptimobileHelper + ) -> NSNumber? { let aps = userInfo["aps"] as! [AnyHashable: Any] if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 { - return + return nil } - let newBadge: NSNumber? = OptimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) + let newBadge: NSNumber? = optimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) if newBadge == nil { - return + return nil } - - bestAttemptContent.badge = newBadge - KeyValPersistenceHelper.set(newBadge, forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) + return newBadge } } diff --git a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift index 573eafec..f58a501a 100644 --- a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift +++ b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift @@ -260,7 +260,6 @@ final class MigrationWork_5_7_0: MigrationWorker { .IN_APP_MOST_RECENT_UPDATED_AT: .inAppMostRecentUpdateAt, .IN_APP_CONSENTED: .inAppConsented, .DYNAMIC_CATEGORY: .dynamicCategory, - // TODO: MIGRATED_TO_GROUPS ] /// Move values from Kumulos UserDefaults to Optimove UserDefaults. let kumulosStorage = UserDefaults.standard diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift index 3c34e005..99c72b05 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift @@ -16,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() @@ -67,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 } @@ -78,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 = AppGroupsHelper.isAppGroupDefined() let storeUrl = getMainStoreUrl(appGroupExists: appGroupExists) @@ -121,6 +123,7 @@ final class AnalyticsHelper { return } + let currentUserIdentifier = optimobileHelper.currentUserIdentifier() let work = { guard let context = self.analyticsContext else { print("No context, aborting") @@ -137,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/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift index db390962..19d748c5 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift @@ -19,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) @@ -35,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 @@ -262,6 +279,7 @@ class InAppManager { } func sync(_ onComplete: InAppSyncCompletionHandler? = nil) { + let currentUserIdentifier = optimobileHelper.currentUserIdentifier() syncQueue.async { let syncBarrier = DispatchSemaphore(value: 0) @@ -277,7 +295,7 @@ 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 @@ -446,7 +464,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) } } @@ -808,7 +826,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/InAppPresenter.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift index ffac42dd..21926bac 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift @@ -32,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 @@ -176,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/UrlBuilder.swift b/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift index 84bcd4dd..c5554d0d 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift @@ -29,7 +29,7 @@ public class UrlBuilder { public typealias ServiceUrlMap = [Service: String] - let storage: KeyValPersistenceHelper.Type + let storage: KeyValueStorage // Overrided urls var runtimeUrlsMap: ServiceUrlMap? @@ -46,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 72b184f5..b46cb719 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift @@ -40,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 + ) } /** @@ -51,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 + ) } /** @@ -68,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 @@ -98,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 4b2ee5a4..25344eb4 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift @@ -44,18 +44,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 cachedFingerprintComponents: [String: String]? 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 @@ -97,7 +97,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 } @@ -114,7 +114,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+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index 15f124b7..6bf0336e 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -163,7 +163,7 @@ extension Optimobile { do { let notification = try PushNotification(userInfo: userInfo) pushHandleOpen(notification: notification) - PendingNotificationHelper.remove(id: notification.message.id) + pendingNoticationHelper.remove(id: notification.message.id) return true } catch { Logger.error( @@ -212,7 +212,7 @@ extension Optimobile { let data = try JSONSerialization.data(withJSONObject: userInfo) let notification = try JSONDecoder().decode(PushNotification.self, from: data) pushHandleDismissed(notificationId: notification.message.id) - PendingNotificationHelper.remove(id: notification.message.id) + pendingNoticationHelper.remove(id: notification.message.id) return true } catch { Logger.error( @@ -229,7 +229,7 @@ extension Optimobile { @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) } @@ -246,7 +246,7 @@ extension Optimobile { @available(iOS 10.0, *) func maybeTrackPushDismissedEvents() { - if !AppGroupsHelper.isKumulosAppGroupDefined() { + if !AppGroupsHelper.isAppGroupDefined() { return } Task { @@ -259,7 +259,7 @@ extension Optimobile { actualPendingNotificationIds.append(notification.message.id) } - let recordedPendingNotifications = PendingNotificationHelper.readAll() + let recordedPendingNotifications = pendingNoticationHelper.readAll() let deletions = recordedPendingNotifications.filter { !actualPendingNotificationIds.contains($0.id) } for deletion in deletions { @@ -416,8 +416,14 @@ class PushHelper { } }() + let optimobileHelper: OptimobileHelper + + init(optimobileHelper: OptimobileHelper) { + self.optimobileHelper = optimobileHelper + } + private func setBadge(userInfo: [AnyHashable: Any]) { - let badge: NSNumber? = OptimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) + let badge: NSNumber? = optimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) if let newBadge = badge { UIApplication.shared.applicationIconBadgeNumber = newBadge.intValue } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift index 19b0f52f..bb173bd4 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": AppGroupsHelper.isAppGroupDefined(), "push": push, ] } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift index d86d9c6c..367df907 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift @@ -21,8 +21,6 @@ public enum InAppDisplayMode: String { case paused } -// MARK: class - final class Optimobile { enum Error: LocalizedError { case alreadyInitialized @@ -43,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 { @@ -62,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 { @@ -102,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 } @@ -117,9 +111,9 @@ final class Optimobile { throw Error.configurationIsMissing } - writeDefaultsKeys(config: config, initialVisitorId: initialVisitorId) + try writeDefaultsKeys(config: config, storage: storage) - instance = Optimobile(config: config) + instance = Optimobile(config: config, storage: storage) instance!.initializeHelpers() @@ -133,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) } @@ -154,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. @@ -172,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: { @@ -207,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 8a5ea8b1..e2f4f9db 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift @@ -17,9 +17,11 @@ class InAppInboxItem { 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] @@ -61,7 +63,7 @@ class InAppInboxItem { func getImageUrl(width: UInt) -> URL? { if let imagePathNotNil = imagePath { - return try? MediaHelper.getCompletePictureUrl( + return try? mediaHelper.getCompletePictureUrl( pictureUrlString: imagePathNotNil, width: width ) @@ -100,7 +102,7 @@ enum OptimoveInApp { return Optimobile.sharedInstance.inAppManager.presenter.getDisplayMode() } - static func getInboxItems() -> [InAppInboxItem] { + static func getInboxItems(storage: OptimoveStorage) -> [InAppInboxItem] { guard let context = Optimobile.sharedInstance.inAppManager.messagesContext else { return [] } @@ -127,7 +129,10 @@ 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/Optimove.swift b/OptimoveSDK/Sources/Classes/Optimove.swift index 09912db6..73efa480 100644 --- a/OptimoveSDK/Sources/Classes/Optimove.swift +++ b/OptimoveSDK/Sources/Classes/Optimove.swift @@ -52,10 +52,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)") } @@ -170,22 +170,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() + ) + } } } @@ -203,21 +205,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() + ) + } } } @@ -294,17 +298,18 @@ 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() + ) + } } } } diff --git a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift index b3ece124..3f836113 100644 --- a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift +++ b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift @@ -150,7 +150,7 @@ open class OptimoveConfigBuilder: NSObject { override public required init() { features = [] - urlBuilder = UrlBuilder(storage: KeyValPersistenceHelper.self) + urlBuilder = UrlBuilder(storage: UserDefaults.optimoveAppGroup()) super.init() } diff --git a/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift index 51107fcf..6af833f1 100644 --- a/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift +++ b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift @@ -1,4 +1,402 @@ // Copyright © 2023 Optimove. All rights reserved. +import Foundation +import OptimoveCore + /// Combined protocol for a convenince access to stored values and files. typealias OptimoveStorage = FileStorage & KeyValueStorage & StorageValue + +enum StorageError: LocalizedError { + case noValue(StorageKey) + + var errorDescription: String? { + switch self { + case let .noValue(key): + return "StorageError: No value for key \(key.rawValue)" + } + } +} + +/// Class implements the Façade pattern for hiding complexity of the OptimoveStorage protocol. +final class StorageFacade: OptimoveStorage { + private let persistantStorage: KeyValueStorage + private let inMemoryStorage: KeyValueStorage + private let fileStorage: FileStorage + + init( + persistantStorage: KeyValueStorage, + inMemoryStorage: KeyValueStorage, + fileStorage: FileStorage + ) { + self.fileStorage = fileStorage + self.inMemoryStorage = inMemoryStorage + self.persistantStorage = persistantStorage + } + + func getStorage(for key: StorageKey) -> KeyValueStorage { + if StorageKey.inMemoryValues.contains(key) { + return inMemoryStorage + } + return persistantStorage + } +} + +// MARK: - KeyValueStorage + +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) { + getStorage(for: key).set(value: value, key: key) + } + + func value(for key: StorageKey) -> Any? { + return getStorage(for: key).value(for: key) + } + + subscript(key: StorageKey) -> T? { + get { + return getStorage(for: key).value(for: key) as? T + } + set { + getStorage(for: key).set(value: newValue, key: key) + } + } + + subscript(key: StorageKey) -> () throws -> T { + get { + { try cast(self.getStorage(for: key).value(for: key)) } + } + set { + getStorage(for: key).set(value: newValue, key: key) + } + } +} + +// MARK: - FileStorage + +extension StorageFacade { + func isExist(fileName: String, isTemporary: Bool) -> Bool { + return fileStorage.isExist(fileName: fileName, isTemporary: isTemporary) + } + + func save(data: T, toFileName: String, isTemporary: Bool) throws { + try fileStorage.save(data: data, toFileName: toFileName, isTemporary: isTemporary) + } + + func saveData(data: Data, toFileName: String, isTemporary: Bool) throws { + try fileStorage.saveData(data: data, toFileName: toFileName, isTemporary: isTemporary) + } + + func load(fileName: String, isTemporary: Bool) throws -> T { + return try unwrap(fileStorage.load(fileName: fileName, isTemporary: isTemporary)) + } + + func loadData(fileName: String, isTemporary: Bool) throws -> Data { + return try unwrap(fileStorage.loadData(fileName: fileName, isTemporary: isTemporary)) + } + + func delete(fileName: String, isTemporary: Bool) throws { + try fileStorage.delete(fileName: fileName, isTemporary: isTemporary) + } +} + +// MARK: - StorageValue + +extension KeyValueStorage where Self: StorageValue { + var installationID: String? { + get { + return self[.installationID] + } + set { + self[.installationID] = newValue + } + } + + var customerID: String? { + get { + return self[.customerID] + } + set { + self[.customerID] = newValue + } + } + + var visitorID: String? { + get { + return self[.visitorID] + } + set { + self[.visitorID] = newValue?.lowercased() + } + } + + var initialVisitorId: String? { + get { + return self[.initialVisitorId] + } + set { + self[.initialVisitorId] = newValue?.lowercased() + } + } + + var configurationEndPoint: URL? { + get { + do { + return try URL(string: unwrap(self[.configurationEndPoint])) + } catch { + return nil + } + } + set { + self[.configurationEndPoint] = newValue?.absoluteString + } + } + + var tenantToken: String? { + get { + return self[.tenantToken] + } + set { + self[.tenantToken] = newValue + } + } + + var version: String? { + get { + return self[.version] + } + set { + self[.version] = newValue + } + } + + var userAgent: String? { + get { + return self[.userAgent] + } + set { + self[.userAgent] = newValue + } + } + + var deviceResolutionWidth: Float? { + get { + return self[.deviceResolutionWidth] + } + set { + self[.deviceResolutionWidth] = newValue + } + } + + var deviceResolutionHeight: Float? { + get { + return self[.deviceResolutionHeight] + } + set { + self[.deviceResolutionHeight] = newValue + } + } + + var advertisingIdentifier: String? { + get { + return self[.advertisingIdentifier] + } + set { + self[.advertisingIdentifier] = newValue + } + } + + var migrationVersions: [String] { + get { + return self[.migrationVersions] ?? [] + } + set { + self[.migrationVersions] = newValue + } + } + + var firstRunTimestamp: Int64? { + get { + return self[.firstRunTimestamp] + } + set { + self[.firstRunTimestamp] = newValue + } + } + + var pushNotificationChannels: [String]? { + get { + return self[.pushNotificationChannels] + } + set { + self[.pushNotificationChannels] = newValue + } + } + + var optitrackEndpoint: URL? { + get { + do { + return try URL(string: unwrap(self[.optitrackEndpoint])) + } catch { + return nil + } + } + set { + self[.optitrackEndpoint] = newValue?.absoluteString + } + } + + var tenantID: Int? { + get { + return self[.tenantID] + } + set { + self[.tenantID] = newValue + } + } + + var userEmail: String? { + get { + return self[.userEmail] + } + set { + self[.userEmail] = newValue + } + } + + var siteID: Int? { + get { + return self[.siteID] + } + set { + self[.siteID] = newValue + } + } + + var isSettingUserSuccess: Bool? { + get { + return self[.settingUserSuccess] + } + set { + return self[.settingUserSuccess] = newValue + } + } + + var firstVisitTimestamp: Int64? { + get { + return self[.firstVisitTimestamp] + } + set { + self[.firstVisitTimestamp] = newValue + } + } + + func getConfigurationEndPoint() throws -> URL { + guard let value = configurationEndPoint else { + throw StorageError.noValue(.configurationEndPoint) + } + return value + } + + func getInstallationID() throws -> String { + guard let value = installationID else { + throw StorageError.noValue(.installationID) + } + return value + } + + func getCustomerID() throws -> String { + guard let value = customerID else { + throw StorageError.noValue(.customerID) + } + return value + } + + func getInitialVisitorId() throws -> String { + guard let value = initialVisitorId else { + throw StorageError.noValue(.initialVisitorId) + } + return value + } + + func getTenantToken() throws -> String { + guard let value = tenantToken else { + throw StorageError.noValue(.tenantToken) + } + return value + } + + func getVisitorID() throws -> String { + guard let value = visitorID else { + throw StorageError.noValue(.visitorID) + } + return value + } + + func getVersion() throws -> String { + guard let value = version else { + throw StorageError.noValue(.version) + } + return value + } + + func getUserAgent() throws -> String { + guard let value = userAgent else { + throw StorageError.noValue(.userAgent) + } + return value + } + + func getDeviceResolutionWidth() throws -> Float { + guard let value = deviceResolutionWidth else { + throw StorageError.noValue(.deviceResolutionWidth) + } + return value + } + + func getDeviceResolutionHeight() throws -> Float { + guard let value = deviceResolutionHeight else { + throw StorageError.noValue(.deviceResolutionHeight) + } + return value + } + + func getFirstRunTimestamp() throws -> Int64 { + guard let value = firstRunTimestamp else { + throw StorageError.noValue(.firstRunTimestamp) + } + return value + } + + func getTenantID() throws -> Int { + guard let value = tenantID else { + throw StorageError.noValue(.tenantID) + } + return value + } + + mutating func finishedMigration(to version: String) { + var versions = migrationVersions + versions.append(version) + migrationVersions = versions + } + + func isAlreadyMigrated(to version: String) -> Bool { + return migrationVersions.contains(version) + } + + func getUserEmail() throws -> String { + guard let value = userEmail else { + throw StorageError.noValue(.userEmail) + } + return value + } + + func getSiteID() throws -> Int { + guard let value = siteID else { + throw StorageError.noValue(.siteID) + } + return value + } +} diff --git a/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift b/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift deleted file mode 100644 index 750c5eea..00000000 --- a/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright © 2019 Optimove. All rights reserved. - -import Foundation -import OptimoveCore - -enum StorageError: LocalizedError { - case noValue(StorageKey) - - var errorDescription: String? { - switch self { - case let .noValue(key): - return "StorageError: No value for key \(key.rawValue)" - } - } -} - -/// Class implements the Façade pattern for hiding complexity of the OptimoveStorage protocol. -final class StorageFacade: OptimoveStorage { - private let persistantStorage: KeyValueStorage - private let inMemoryStorage: KeyValueStorage - private let fileStorage: FileStorage - - init( - persistantStorage: KeyValueStorage, - inMemoryStorage: KeyValueStorage, - fileStorage: FileStorage - ) { - self.fileStorage = fileStorage - self.inMemoryStorage = inMemoryStorage - self.persistantStorage = persistantStorage - } - - func getStorage(for key: StorageKey) -> KeyValueStorage { - if StorageKey.inMemoryValues.contains(key) { - return inMemoryStorage - } - return persistantStorage - } -} - -// MARK: - KeyValueStorage - -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) { - getStorage(for: key).set(value: value, key: key) - } - - func value(for key: StorageKey) -> Any? { - return getStorage(for: key).value(for: key) - } - - subscript(key: StorageKey) -> T? { - get { - return getStorage(for: key).value(for: key) as? T - } - set { - getStorage(for: key).set(value: newValue, key: key) - } - } - - subscript(key: StorageKey) -> () throws -> T { - get { - { try cast(self.getStorage(for: key).value(for: key)) } - } - set { - getStorage(for: key).set(value: newValue, key: key) - } - } -} - -// MARK: - FileStorage - -extension StorageFacade { - func isExist(fileName: String, isTemporary: Bool) -> Bool { - return fileStorage.isExist(fileName: fileName, isTemporary: isTemporary) - } - - func save(data: T, toFileName: String, isTemporary: Bool) throws { - try fileStorage.save(data: data, toFileName: toFileName, isTemporary: isTemporary) - } - - func saveData(data: Data, toFileName: String, isTemporary: Bool) throws { - try fileStorage.saveData(data: data, toFileName: toFileName, isTemporary: isTemporary) - } - - func load(fileName: String, isTemporary: Bool) throws -> T { - return try unwrap(fileStorage.load(fileName: fileName, isTemporary: isTemporary)) - } - - func loadData(fileName: String, isTemporary: Bool) throws -> Data { - return try unwrap(fileStorage.loadData(fileName: fileName, isTemporary: isTemporary)) - } - - func delete(fileName: String, isTemporary: Bool) throws { - try fileStorage.delete(fileName: fileName, isTemporary: isTemporary) - } -} - -// MARK: - StorageValue - -extension KeyValueStorage where Self: StorageValue { - var installationID: String? { - get { - return self[.installationID] - } - set { - self[.installationID] = newValue - } - } - - var customerID: String? { - get { - return self[.customerID] - } - set { - self[.customerID] = newValue - } - } - - var visitorID: String? { - get { - return self[.visitorID] - } - set { - self[.visitorID] = newValue?.lowercased() - } - } - - var initialVisitorId: String? { - get { - return self[.initialVisitorId] - } - set { - self[.initialVisitorId] = newValue?.lowercased() - } - } - - var configurationEndPoint: URL? { - get { - do { - return try URL(string: unwrap(self[.configurationEndPoint])) - } catch { - return nil - } - } - set { - self[.configurationEndPoint] = newValue?.absoluteString - } - } - - var tenantToken: String? { - get { - return self[.tenantToken] - } - set { - self[.tenantToken] = newValue - } - } - - var version: String? { - get { - return self[.version] - } - set { - self[.version] = newValue - } - } - - var userAgent: String? { - get { - return self[.userAgent] - } - set { - self[.userAgent] = newValue - } - } - - var deviceResolutionWidth: Float? { - get { - return self[.deviceResolutionWidth] - } - set { - self[.deviceResolutionWidth] = newValue - } - } - - var deviceResolutionHeight: Float? { - get { - return self[.deviceResolutionHeight] - } - set { - self[.deviceResolutionHeight] = newValue - } - } - - var advertisingIdentifier: String? { - get { - return self[.advertisingIdentifier] - } - set { - self[.advertisingIdentifier] = newValue - } - } - - var migrationVersions: [String] { - get { - return self[.migrationVersions] ?? [] - } - set { - self[.migrationVersions] = newValue - } - } - - var firstRunTimestamp: Int64? { - get { - return self[.firstRunTimestamp] - } - set { - self[.firstRunTimestamp] = newValue - } - } - - var pushNotificationChannels: [String]? { - get { - return self[.pushNotificationChannels] - } - set { - self[.pushNotificationChannels] = newValue - } - } - - var optitrackEndpoint: URL? { - get { - do { - return try URL(string: unwrap(self[.optitrackEndpoint])) - } catch { - return nil - } - } - set { - self[.optitrackEndpoint] = newValue?.absoluteString - } - } - - var tenantID: Int? { - get { - return self[.tenantID] - } - set { - self[.tenantID] = newValue - } - } - - var userEmail: String? { - get { - return self[.userEmail] - } - set { - self[.userEmail] = newValue - } - } - - var siteID: Int? { - get { - return self[.siteID] - } - set { - self[.siteID] = newValue - } - } - - var isSettingUserSuccess: Bool? { - get { - return self[.settingUserSuccess] - } - set { - return self[.settingUserSuccess] = newValue - } - } - - var firstVisitTimestamp: Int64? { - get { - return self[.firstVisitTimestamp] - } - set { - self[.firstVisitTimestamp] = newValue - } - } - - func getConfigurationEndPoint() throws -> URL { - guard let value = configurationEndPoint else { - throw StorageError.noValue(.configurationEndPoint) - } - return value - } - - func getInstallationID() throws -> String { - guard let value = installationID else { - throw StorageError.noValue(.installationID) - } - return value - } - - func getCustomerID() throws -> String { - guard let value = customerID else { - throw StorageError.noValue(.customerID) - } - return value - } - - func getInitialVisitorId() throws -> String { - guard let value = initialVisitorId else { - throw StorageError.noValue(.initialVisitorId) - } - return value - } - - func getTenantToken() throws -> String { - guard let value = tenantToken else { - throw StorageError.noValue(.tenantToken) - } - return value - } - - func getVisitorID() throws -> String { - guard let value = visitorID else { - throw StorageError.noValue(.visitorID) - } - return value - } - - func getVersion() throws -> String { - guard let value = version else { - throw StorageError.noValue(.version) - } - return value - } - - func getUserAgent() throws -> String { - guard let value = userAgent else { - throw StorageError.noValue(.userAgent) - } - return value - } - - func getDeviceResolutionWidth() throws -> Float { - guard let value = deviceResolutionWidth else { - throw StorageError.noValue(.deviceResolutionWidth) - } - return value - } - - func getDeviceResolutionHeight() throws -> Float { - guard let value = deviceResolutionHeight else { - throw StorageError.noValue(.deviceResolutionHeight) - } - return value - } - - func getFirstRunTimestamp() throws -> Int64 { - guard let value = firstRunTimestamp else { - throw StorageError.noValue(.firstRunTimestamp) - } - return value - } - - func getTenantID() throws -> Int { - guard let value = tenantID else { - throw StorageError.noValue(.tenantID) - } - return value - } - - mutating func finishedMigration(to version: String) { - var versions = migrationVersions - versions.append(version) - migrationVersions = versions - } - - func isAlreadyMigrated(to version: String) -> Bool { - return migrationVersions.contains(version) - } - - func getUserEmail() throws -> String { - guard let value = userEmail else { - throw StorageError.noValue(.userEmail) - } - return value - } - - func getSiteID() throws -> Int { - guard let value = siteID else { - throw StorageError.noValue(.siteID) - } - return value - } -} diff --git a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift index e4dfb222..9a9237f3 100644 --- a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift +++ b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift @@ -42,7 +42,7 @@ final class OptimoveConfigBuilderTests: XCTestCase { } func testBaseUrlMapping() throws { - KeyValPersistenceHelper.set(Region.DEV.rawValue, forKey: OptimobileUserDefaultsKey.REGION.rawValue) + 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/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift b/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift index 4ea35af2..2d765e50 100644 --- a/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift +++ b/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift @@ -1,5 +1,6 @@ // Copyright © 2019 Optimove. All rights reserved. +import OptimoveCore @testable import OptimoveSDK import XCTest From f9d268a382a80478d65e09dd77a06ae2c5c98e5d Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 16:58:49 +0200 Subject: [PATCH 04/12] feat: add appgroup to storge facade --- .../Sources/Classes/Storage/KeyValueStorage.swift | 2 +- OptimoveSDK/Sources/Classes/DI/Assembly.swift | 6 +++--- .../Sources/Classes/Storage/OptimoveStorage.swift | 15 +++++++++++---- .../Sources/Storage/KeyValueStorageTests.swift | 3 ++- .../Storage/OptimoveStorageFacadeTests.swift | 3 ++- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift index de9fca90..3529df0e 100644 --- a/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift +++ b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift @@ -39,7 +39,7 @@ public enum StorageKey: String, CaseIterable { case deferredLinkChecked = "KUMULOS_DDL_CHECKED" public static let inMemoryValues: Set = [.tenantToken, .version] - public static let appGroup: Set = [ + public static let appGroupValues: Set = [ .badgeCount, .dynamicCategory, .installUUID, diff --git a/OptimoveSDK/Sources/Classes/DI/Assembly.swift b/OptimoveSDK/Sources/Classes/DI/Assembly.swift index 777bf0ff..32349912 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 ) diff --git a/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift index 6af833f1..72479c1b 100644 --- a/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift +++ b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift @@ -19,25 +19,32 @@ enum StorageError: LocalizedError { /// Class implements the Façade pattern for hiding complexity of the OptimoveStorage protocol. final class StorageFacade: OptimoveStorage { - private let persistantStorage: KeyValueStorage + // FIXME: - Split persistance storage to AppGroup and Standart + private let standardStorage: KeyValueStorage + private let appGroupStorage: KeyValueStorage private let inMemoryStorage: KeyValueStorage private let fileStorage: FileStorage init( - persistantStorage: KeyValueStorage, + 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 } } diff --git a/OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift b/OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift index 50b8721c..7e4544a6 100644 --- a/OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift +++ b/OptimoveSDK/Tests/Sources/Storage/KeyValueStorageTests.swift @@ -62,7 +62,8 @@ class KeyValueStorageTests: XCTestCase { override func setUp() { storage = StorageFacade( - persistantStorage: MockKeyValueStorage(), + standardStorage: MockKeyValueStorage(), + appGroupStorage: MockKeyValueStorage(), inMemoryStorage: MockKeyValueStorage(), fileStorage: MockFileStorage() ) diff --git a/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift b/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift index 2d765e50..20406a0c 100644 --- a/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift +++ b/OptimoveSDK/Tests/Sources/Storage/OptimoveStorageFacadeTests.swift @@ -9,7 +9,8 @@ class OptimoveStorageFacadeTests: XCTestCase { override func setUp() { storage = StorageFacade( - persistantStorage: MockKeyValueStorage(), + standardStorage: MockKeyValueStorage(), + appGroupStorage: MockKeyValueStorage(), inMemoryStorage: MockKeyValueStorage(), fileStorage: MockFileStorage() ) From e24e830d520fada388f14d6801eaa80af97ba87d Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 17:05:44 +0200 Subject: [PATCH 05/12] chore: remove app group helper --- .../Sources/Classes/AppGroupsHelper.swift | 33 ------------------- .../Storage/FileManager+AppGroup.swift | 5 +-- .../Storage/UserDefaults+AppGroup.swift | 5 +-- .../Sources/OptimoveNotificationService.swift | 3 +- .../Classes/Optimobile/AnalyticsHelper.swift | 2 +- .../Classes/Optimobile/Optimobile+Push.swift | 3 -- .../Classes/Optimobile/Optimobile+Stats.swift | 2 +- .../Sources/Classes/OptimoveConfig.swift | 11 +++++-- .../Sources/OptimoveConfigBuilderTests.swift | 2 +- 9 files changed, 18 insertions(+), 48 deletions(-) delete mode 100644 OptimoveCore/Sources/Classes/AppGroupsHelper.swift diff --git a/OptimoveCore/Sources/Classes/AppGroupsHelper.swift b/OptimoveCore/Sources/Classes/AppGroupsHelper.swift deleted file mode 100644 index af813509..00000000 --- a/OptimoveCore/Sources/Classes/AppGroupsHelper.swift +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -enum AppGroupConfig { - static var suffix: String = ".optimove" -} - -public enum AppGroupsHelper { - public static func isKumulosAppGroupDefined() -> Bool { - let containerUrl = getSharedContainerPath() - - return containerUrl != nil - } - - public 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/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift index 116c9c9d..21947591 100644 --- a/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift +++ b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift @@ -3,7 +3,7 @@ import Foundation public extension FileManager { - static func optimoveAppGroupURL() -> URL { + static func optimoveAppGroupURL() throws -> URL { let suiteName = Bundle.optimoveAppGroupIdentifier guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) else { let message = """ @@ -11,7 +11,8 @@ public extension FileManager { 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 """ - fatalError(message) + assertionFailure(message) + throw GuardError.custom(message) } return url } diff --git a/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift index 74096c35..aaf6f3d6 100644 --- a/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift +++ b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift @@ -3,7 +3,7 @@ import Foundation public extension UserDefaults { - static func optimoveAppGroup() -> UserDefaults { + static func optimoveAppGroup() throws -> UserDefaults { let suiteName = Bundle.optimoveAppGroupIdentifier guard let userDefaults = UserDefaults(suiteName: suiteName) else { let message = """ @@ -11,7 +11,8 @@ public extension UserDefaults { 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 """ - fatalError(message) + assertionFailure(message) + throw GuardError.custom(message) } return userDefaults } diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index cc77fb91..5deb96c9 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -39,8 +39,7 @@ public enum OptimoveNotificationService { if bestAttemptContent.categoryIdentifier.isEmpty { bestAttemptContent.categoryIdentifier = await buildCategory(notification: notification) } - if AppGroupsHelper.isAppGroupDefined() { - let storage = UserDefaults.optimoveAppGroup() + if let storage = try? UserDefaults.optimoveAppGroup() { let mediaHelper = MediaHelper(storage: storage) if let attachment = try await maybeGetAttachment( notification: notification, diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift index 99c72b05..b548ee3f 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift @@ -80,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.isAppGroupDefined() + let appGroupExists = true let storeUrl = getMainStoreUrl(appGroupExists: appGroupExists) diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index 6bf0336e..d2e88bf3 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -246,9 +246,6 @@ extension Optimobile { @available(iOS 10.0, *) func maybeTrackPushDismissedEvents() { - if !AppGroupsHelper.isAppGroupDefined() { - return - } Task { do { let notifications = await UNUserNotificationCenter.current().deliveredNotifications() diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift index bb173bd4..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.isAppGroupDefined(), + "hasGroup": true, "push": push, ] } diff --git a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift index 3f836113..4ebba084 100644 --- a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift +++ b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift @@ -149,9 +149,14 @@ open class OptimoveConfigBuilder: NSObject { } override public required init() { - features = [] - urlBuilder = UrlBuilder(storage: UserDefaults.optimoveAppGroup()) - super.init() + do { + features = [] + // FIXME: Move UrlBuilder out of OptimoveConfigBuilder + urlBuilder = try UrlBuilder(storage: UserDefaults.optimoveAppGroup()) + super.init() + } catch { + fatalError(error.localizedDescription) + } } @discardableResult public func setCredentials(optimoveCredentials: String?, optimobileCredentials: String?) -> OptimoveConfigBuilder { diff --git a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift index 9a9237f3..991cc2c2 100644 --- a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift +++ b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift @@ -42,7 +42,7 @@ final class OptimoveConfigBuilderTests: XCTestCase { } func testBaseUrlMapping() throws { - UserDefaults.optimoveAppGroup().set(value: Region.DEV.rawValue, key: .region) + 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 From 78c4929970d3bad0dca3ee851a575c6c42a2e214 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 17:26:12 +0200 Subject: [PATCH 06/12] refactor: badge arithmetic --- .../Sources/Classes/OptimobileHelper.swift | 31 +++++-------------- .../Sources/Classes/PushNotification.swift | 6 ++-- .../Tests/Sources/PushNotificationTests.swift | 2 +- .../Sources/OptimoveNotificationService.swift | 20 ++---------- .../Classes/Optimobile/Optimobile+Push.swift | 9 +++--- 5 files changed, 17 insertions(+), 51 deletions(-) diff --git a/OptimoveCore/Sources/Classes/OptimobileHelper.swift b/OptimoveCore/Sources/Classes/OptimobileHelper.swift index f94a5f6c..2e90014d 100644 --- a/OptimoveCore/Sources/Classes/OptimobileHelper.swift +++ b/OptimoveCore/Sources/Classes/OptimobileHelper.swift @@ -42,31 +42,14 @@ public struct OptimobileHelper { return installId() } - // FIXME: Use PushNotifcation - public 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: NSNumber = storage[.badgeCount] { - newBadge = NSNumber(value: currentVal.intValue + incrementBy.intValue) - - if newBadge!.intValue < 0 { - newBadge = 0 - } + 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 newBadge + return notification.aps.badge } } diff --git a/OptimoveCore/Sources/Classes/PushNotification.swift b/OptimoveCore/Sources/Classes/PushNotification.swift index 39d5a366..03507d62 100644 --- a/OptimoveCore/Sources/Classes/PushNotification.swift +++ b/OptimoveCore/Sources/Classes/PushNotification.swift @@ -76,7 +76,7 @@ public struct PushNotification: Decodable { public let aps: Aps public let attachment: PushNotification.Attachment? /// Optimove badge - public let badge: Int? + public let badgeIncrement: Int? public let buttons: [PushNotification.Button]? public let deeplink: PushNotification.Data? public let message: PushNotification.Data @@ -86,7 +86,7 @@ public struct PushNotification: Decodable { case a case aps case attachments - case badge = "badge_inc" + case badgeIncrement = "badge_inc" case buttons = "k.buttons" case custom case deeplink = "k.deepLink" @@ -106,7 +106,7 @@ public struct PushNotification: Decodable { self.attachment = try container.decodeIfPresent(Attachment.self, forKey: .attachments) let custom = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .custom) - self.badge = try custom.decodeIfPresent(Int.self, forKey: .badge) + 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) diff --git a/OptimoveCore/Tests/Sources/PushNotificationTests.swift b/OptimoveCore/Tests/Sources/PushNotificationTests.swift index 8f0cac7f..fbea0fa0 100644 --- a/OptimoveCore/Tests/Sources/PushNotificationTests.swift +++ b/OptimoveCore/Tests/Sources/PushNotificationTests.swift @@ -18,7 +18,7 @@ final class PushNotificationTests: XCTestCase, FileAccessible { fileName = "notification-badge.json" let decoder = JSONDecoder() let notification = try decoder.decode(PushNotification.self, from: data) - XCTAssertEqual(notification.badge, 42) + XCTAssertEqual(notification.badgeIncrement, 42) } func test_decode_buttons() throws { diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 5deb96c9..6c775a02 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -48,9 +48,9 @@ public enum OptimoveNotificationService { bestAttemptContent.attachments = [attachment] } let optimobileHelper = OptimobileHelper(storage: storage) - if let badge = maybeSetBadge(userInfo: userInfo, optimobileHelper: optimobileHelper) { + if let badge = optimobileHelper.getBadge(notification: notification) { storage.set(value: badge, key: .badgeCount) - bestAttemptContent.badge = badge + bestAttemptContent.badge = NSNumber(integerLiteral: badge) } let pendingNoticationHelper = PendingNotificationHelper(storage: storage) pendingNoticationHelper.add( @@ -130,20 +130,4 @@ public enum OptimoveNotificationService { url: tempURL ) } - - static func maybeSetBadge( - userInfo: [AnyHashable: Any], - optimobileHelper: OptimobileHelper - ) -> NSNumber? { - let aps = userInfo["aps"] as! [AnyHashable: Any] - if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 { - return nil - } - - let newBadge: NSNumber? = optimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) - if newBadge == nil { - return nil - } - return newBadge - } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index d2e88bf3..c5eea925 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -348,7 +348,7 @@ class PushHelper { let notification = try PushNotification(userInfo: userInfo) let hasInApp = notification.deeplink != nil - self.setBadge(userInfo: userInfo) + self.setBadge(notification: notification) self.trackPushDelivery(notification: notification) if existingDidReceive == nil, !hasInApp { @@ -419,10 +419,9 @@ class PushHelper { self.optimobileHelper = optimobileHelper } - private func setBadge(userInfo: [AnyHashable: Any]) { - let badge: NSNumber? = optimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) - if let newBadge = badge { - UIApplication.shared.applicationIconBadgeNumber = newBadge.intValue + private func setBadge(notification: PushNotification) { + if let badge = optimobileHelper.getBadge(notification: notification) { + UIApplication.shared.applicationIconBadgeNumber = badge } } From 8eaf717f465d88855afdbf22ed81d7fa597400db Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Tue, 6 Feb 2024 09:45:23 +0200 Subject: [PATCH 07/12] chore: migration version 6.0.0 --- OptimoveSDK/Sources/Classes/DI/Assembly.swift | 2 +- OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift | 4 ++-- OptimoveSDK/Sources/Classes/Migration/Version.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/OptimoveSDK/Sources/Classes/DI/Assembly.swift b/OptimoveSDK/Sources/Classes/DI/Assembly.swift index 32349912..7a87c0a6 100644 --- a/OptimoveSDK/Sources/Classes/DI/Assembly.swift +++ b/OptimoveSDK/Sources/Classes/DI/Assembly.swift @@ -33,7 +33,7 @@ final class Assembly { private func migrate() { let migrations: [MigrationWork] = [ MigrationWork_3_3_0(), - MigrationWork_5_7_0(), + MigrationWork_6_0_0(), ] migrations .filter { $0.isAllowToMiragte(SDKVersion) } diff --git a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift index f58a501a..4decf8de 100644 --- a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift +++ b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift @@ -232,9 +232,9 @@ extension MigrationWork_3_3_0 { } /// Migration from Kumulos UserDefaults to Optimove UserDefaults. -final class MigrationWork_5_7_0: MigrationWorker { +final class MigrationWork_6_0_0: MigrationWorker { init() { - super.init(newVersion: .v_5_7_0) + super.init(newVersion: .v_6_0_0) } override func isAllowToMiragte(_: String) -> Bool { diff --git a/OptimoveSDK/Sources/Classes/Migration/Version.swift b/OptimoveSDK/Sources/Classes/Migration/Version.swift index c1c172ca..d35ae1f4 100644 --- a/OptimoveSDK/Sources/Classes/Migration/Version.swift +++ b/OptimoveSDK/Sources/Classes/Migration/Version.swift @@ -4,5 +4,5 @@ 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_5_7_0 = "5.7.0" + case v_6_0_0 = "6.0.0" } From 6d2fe1ea2f86f0d894586d8cbcac3650d5970077 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Tue, 6 Feb 2024 11:12:21 +0200 Subject: [PATCH 08/12] chore: add debug product --- OptimoveDebug/Sources/ApplicationInfo.swift | 9 +++ OptimoveDebug/Sources/StateService.swift | 68 +++++++++++++++++++++ Package.swift | 11 ++++ 3 files changed, 88 insertions(+) create mode 100644 OptimoveDebug/Sources/ApplicationInfo.swift create mode 100644 OptimoveDebug/Sources/StateService.swift diff --git a/OptimoveDebug/Sources/ApplicationInfo.swift b/OptimoveDebug/Sources/ApplicationInfo.swift new file mode 100644 index 00000000..9d789498 --- /dev/null +++ b/OptimoveDebug/Sources/ApplicationInfo.swift @@ -0,0 +1,9 @@ +// Copyright © 2024 Optimove. All rights reserved. + +import Foundation + +public enum ApplicationInfo { + public static let bundleIdentifier = Bundle.main.bundleIdentifier! + public static let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String + public static let build = Bundle.main.infoDictionary!["CFBundleVersion"] as! String +} diff --git a/OptimoveDebug/Sources/StateService.swift b/OptimoveDebug/Sources/StateService.swift new file mode 100644 index 00000000..f097ada4 --- /dev/null +++ b/OptimoveDebug/Sources/StateService.swift @@ -0,0 +1,68 @@ +// Copyright © 2024 Optimove. All rights reserved. + +import Foundation +import OptimoveCore +@testable import OptimoveSDK + +public final class StateService { + public enum State { + case app_version + case sdk_version + case installation + case tenant + case initial_visitor + case customer + case update_visitor + case email + } + + private enum Constants { + static let undefined = "" + } + + public static var shared: StateService = .init() + + private let storage: any OptimoveStorage + + private init() { + do { + storage = try StorageFacade( + standardStorage: UserDefaults.optimove(), + appGroupStorage: UserDefaults.optimoveAppGroup(), + inMemoryStorage: InMemoryStorage(), + fileStorage: FileStorageImpl( + persistentStorageURL: FileManager.optimoveURL(), + temporaryStorageURL: FileManager.temporaryURL() + ) + ) + } catch { + fatalError(error.localizedDescription) + } + } + + public func getState(_ state: State) -> String { + switch state { + case .app_version: + return "\(ApplicationInfo.version) build: \(ApplicationInfo.build)" + case .sdk_version: + return Optimove.version + case .installation: + return storage.installationID ?? Constants.undefined + case .tenant: + guard let tenantID = storage.tenantID else { return Constants.undefined } + return String(tenantID) + case .initial_visitor: + return storage.initialVisitorId ?? Constants.undefined + case .customer: + return storage.customerID ?? Constants.undefined + case .email: + return storage.userEmail ?? Constants.undefined + case .update_visitor: + return storage.visitorID ?? Constants.undefined + } + } + + public subscript(state: State) -> String { + return StateService.shared.getState(state) + } +} diff --git a/Package.swift b/Package.swift index 73b21dea..d0394087 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,10 @@ let package = Package( name: "OptimoveNotificationServiceExtension", targets: ["OptimoveNotificationServiceExtension"] ), + .library( + name: "OptimoveDebug", + targets: ["OptimoveDebug"] + ), ], dependencies: [ .package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.1"), @@ -54,6 +58,13 @@ let package = Package( .process("Resources"), ] ), + .target( + name: "OptimoveDebug", + dependencies: [ + "OptimoveSDK", + ], + path: "OptimoveDebug/Sources" + ), .testTarget( name: "OptimoveSDKTests", dependencies: [ From a64a40a08b9b0eb954e3adcd5b50433c60eedff8 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Tue, 6 Feb 2024 13:25:25 +0200 Subject: [PATCH 09/12] feat: expose sdk state --- OptimoveDebug/Sources/ApplicationInfo.swift | 9 --- OptimoveDebug/Sources/StateService.swift | 68 ------------------- .../Extensions/Bundle+AppVersion.swift | 4 ++ OptimoveSDK/Sources/Classes/Optimove.swift | 24 ++++++- .../Sources/Classes/States/SdkState.swift | 25 +++++++ Package.swift | 11 --- 6 files changed, 52 insertions(+), 89 deletions(-) delete mode 100644 OptimoveDebug/Sources/ApplicationInfo.swift delete mode 100644 OptimoveDebug/Sources/StateService.swift create mode 100644 OptimoveSDK/Sources/Classes/States/SdkState.swift diff --git a/OptimoveDebug/Sources/ApplicationInfo.swift b/OptimoveDebug/Sources/ApplicationInfo.swift deleted file mode 100644 index 9d789498..00000000 --- a/OptimoveDebug/Sources/ApplicationInfo.swift +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright © 2024 Optimove. All rights reserved. - -import Foundation - -public enum ApplicationInfo { - public static let bundleIdentifier = Bundle.main.bundleIdentifier! - public static let version = Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String - public static let build = Bundle.main.infoDictionary!["CFBundleVersion"] as! String -} diff --git a/OptimoveDebug/Sources/StateService.swift b/OptimoveDebug/Sources/StateService.swift deleted file mode 100644 index f097ada4..00000000 --- a/OptimoveDebug/Sources/StateService.swift +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright © 2024 Optimove. All rights reserved. - -import Foundation -import OptimoveCore -@testable import OptimoveSDK - -public final class StateService { - public enum State { - case app_version - case sdk_version - case installation - case tenant - case initial_visitor - case customer - case update_visitor - case email - } - - private enum Constants { - static let undefined = "" - } - - public static var shared: StateService = .init() - - private let storage: any OptimoveStorage - - private init() { - do { - storage = try StorageFacade( - standardStorage: UserDefaults.optimove(), - appGroupStorage: UserDefaults.optimoveAppGroup(), - inMemoryStorage: InMemoryStorage(), - fileStorage: FileStorageImpl( - persistentStorageURL: FileManager.optimoveURL(), - temporaryStorageURL: FileManager.temporaryURL() - ) - ) - } catch { - fatalError(error.localizedDescription) - } - } - - public func getState(_ state: State) -> String { - switch state { - case .app_version: - return "\(ApplicationInfo.version) build: \(ApplicationInfo.build)" - case .sdk_version: - return Optimove.version - case .installation: - return storage.installationID ?? Constants.undefined - case .tenant: - guard let tenantID = storage.tenantID else { return Constants.undefined } - return String(tenantID) - case .initial_visitor: - return storage.initialVisitorId ?? Constants.undefined - case .customer: - return storage.customerID ?? Constants.undefined - case .email: - return storage.userEmail ?? Constants.undefined - case .update_visitor: - return storage.visitorID ?? Constants.undefined - } - } - - public subscript(state: State) -> String { - return StateService.shared.getState(state) - } -} diff --git a/OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift b/OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift index 6970cb20..8fefc29f 100644 --- a/OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift +++ b/OptimoveSDK/Sources/Classes/Extensions/Bundle+AppVersion.swift @@ -6,4 +6,8 @@ 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/Optimove.swift b/OptimoveSDK/Sources/Classes/Optimove.swift index 73efa480..57380eb8 100644 --- a/OptimoveSDK/Sources/Classes/Optimove.swift +++ b/OptimoveSDK/Sources/Classes/Optimove.swift @@ -18,7 +18,7 @@ typealias Logger = OptimoveCore.Logger /// 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() { @@ -312,6 +312,28 @@ public extension Optimove { } } } + + 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 + } + } } // MARK: - Optimobile APIs 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/Package.swift b/Package.swift index d0394087..73b21dea 100644 --- a/Package.swift +++ b/Package.swift @@ -21,10 +21,6 @@ let package = Package( name: "OptimoveNotificationServiceExtension", targets: ["OptimoveNotificationServiceExtension"] ), - .library( - name: "OptimoveDebug", - targets: ["OptimoveDebug"] - ), ], dependencies: [ .package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.1"), @@ -58,13 +54,6 @@ let package = Package( .process("Resources"), ] ), - .target( - name: "OptimoveDebug", - dependencies: [ - "OptimoveSDK", - ], - path: "OptimoveDebug/Sources" - ), .testTarget( name: "OptimoveSDKTests", dependencies: [ From 1f677e8530109ccb365962eb2cc854e9048df8fa Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Tue, 6 Feb 2024 13:26:42 +0200 Subject: [PATCH 10/12] fix: return back access level of OptimoveInApp --- OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift index e2f4f9db..a530f6e6 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift @@ -81,7 +81,7 @@ struct InAppInboxSummary { typealias InboxUpdatedHandlerBlock = () -> Void typealias InboxSummaryBlock = (InAppInboxSummary?) -> Void -enum OptimoveInApp { +public enum OptimoveInApp { private static var _inboxUpdatedHandlerBlock: InboxUpdatedHandlerBlock? static func updateConsent(forUser consentGiven: Bool) { From c8fb2997e7d0ef80ee316d0e3f437ab242338f6d Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Tue, 6 Feb 2024 13:27:30 +0200 Subject: [PATCH 11/12] refactor: rename method --- .../Sources/OptimoveNotificationService.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 6c775a02..0140c4d7 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -37,7 +37,7 @@ public enum OptimoveNotificationService { let data = try JSONSerialization.data(withJSONObject: userInfo) let notification = try JSONDecoder().decode(PushNotification.self, from: data) if bestAttemptContent.categoryIdentifier.isEmpty { - bestAttemptContent.categoryIdentifier = await buildCategory(notification: notification) + bestAttemptContent.categoryIdentifier = await registerCategory(notification: notification) } if let storage = try? UserDefaults.optimoveAppGroup() { let mediaHelper = MediaHelper(storage: storage) @@ -94,7 +94,7 @@ public enum OptimoveNotificationService { } } - static func buildCategory(notification: PushNotification) async -> String { + static func registerCategory(notification: PushNotification) async -> String { let categoryIdentifier = CategoryManager.getCategoryId(messageId: notification.message.id) let category = UNNotificationCategory( identifier: categoryIdentifier, From 332bb01b9c795ba8f044b1e9eec8af69afbd3381 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Tue, 6 Feb 2024 14:08:31 +0200 Subject: [PATCH 12/12] docs: update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d748d2..e51ffdff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.0.0 + +- Minimum supported iOS version is now 13.0. + ## 5.6.0 - Support the delayed configuration for SDK. Add new public APIs: