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: 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/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..2e90014d 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,40 +32,24 @@ 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? { - let custom = userInfo["custom"] as? [AnyHashable: Any] - let aps = userInfo["aps"] as? [AnyHashable: Any] - - if custom == nil || aps == nil { - return nil - } - - let incrementBy: NSNumber? = custom!["badge_inc"] as? NSNumber - let badge: NSNumber? = aps!["badge"] as? NSNumber - - if badge == nil { - return nil - } - - var newBadge: NSNumber? = badge - if let incrementBy = incrementBy, let currentVal = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) as? NSNumber { - newBadge = NSNumber(value: currentVal.intValue + incrementBy.intValue) - - if newBadge!.intValue < 0 { - newBadge = 0 - } + 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/OptimobileUserDefaultsKey.swift b/OptimoveCore/Sources/Classes/OptimobileUserDefaultsKey.swift index 4c290b0a..824bc186 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" @@ -10,9 +10,6 @@ public enum OptimobileUserDefaultsKey: String { case BADGE_COUNT = "KumulosBadgeCount" case PENDING_NOTIFICATIONS = "KumulosPendingNotifications" case PENDING_ANALYTICS = "KumulosPendingAnalytics" - - // exist only in standard defaults for app - case MIGRATED_TO_GROUPS = "KumulosDidMigrateToAppGroups" case IN_APP_LAST_SYNCED_AT = "KumulosMessagesLastSyncedAt" case IN_APP_MOST_RECENT_UPDATED_AT = "KumulosInAppMostRecentUpdatedAt" case IN_APP_CONSENTED = "KumulosInAppConsented" diff --git a/OptimoveCore/Sources/Classes/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/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/Sources/Classes/Storage/FileManager+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift new file mode 100644 index 00000000..21947591 --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/FileManager+AppGroup.swift @@ -0,0 +1,19 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public extension FileManager { + static func optimoveAppGroupURL() throws -> URL { + let suiteName = Bundle.optimoveAppGroupIdentifier + guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: suiteName) else { + let message = """ + Unable to initialize UserDefault with suit name "\(suiteName)". + Highly possible that the client forgot to add the app group as described in the documentation. + Link: https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/SDK-Setup-Capabilities + """ + assertionFailure(message) + throw GuardError.custom(message) + } + return url + } +} diff --git a/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift new file mode 100644 index 00000000..3529df0e --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift @@ -0,0 +1,153 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +/// The enum used as keys for storage values. +public enum StorageKey: String, CaseIterable { + case installationID + case customerID + case configurationEndPoint + case initialVisitorId + case tenantToken + case visitorID + case version + case 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 + case deferredLinkChecked = "KUMULOS_DDL_CHECKED" + + public static let inMemoryValues: Set = [.tenantToken, .version] + public static let appGroupValues: Set = [ + .badgeCount, + .dynamicCategory, + .installUUID, + .mediaURL, + .pendingNotifications, + .userID, + ] +} + +/// The protocol used as convenience accessor to storage values. +public protocol StorageValue { + var installationID: String? { get set } + var customerID: String? { get set } + var configurationEndPoint: URL? { get set } + var initialVisitorId: String? { get set } + var tenantToken: String? { get set } + var visitorID: String? { get set } + var version: String? { get set } + var 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. +public protocol KeyValueStorage { + func set(value: Any?, key: StorageKey) + func value(for: StorageKey) -> Any? + subscript(_: StorageKey) -> T? { get set } +} + +/// ``UserDefaults`` uses as persistent ``KeyValueStorage``. +extension UserDefaults: KeyValueStorage { + public func set(value: Any?, key: StorageKey) { + set(value, forKey: key.rawValue) + } + + public func value(for key: StorageKey) -> Any? { + return value(forKey: key.rawValue) + } + + public subscript(key: StorageKey) -> T? { + get { + return value(for: key) as? T + } + set { + set(value: newValue, key: key) + } + } +} + +/// ``InMemoryStorage`` uses as in-memory ``KeyValueStorage``. +public final class InMemoryStorage: KeyValueStorage { + private var storage = [StorageKey: Any]() + private let queue = DispatchQueue(label: "com.optimove.sdk.inmemorystorage", attributes: .concurrent) + + public init() {} + + public func set(value: Any?, key: StorageKey) { + queue.async(flags: .barrier) { [self] in + storage[key] = value + } + } + + public subscript(key: StorageKey) -> T? { + get { + var result: T? + queue.sync { + result = storage[key] as? T + } + return result + } + set { + queue.async(flags: .barrier) { [self] in + storage[key] = newValue + } + } + } + + public func value(for key: StorageKey) -> Any? { + var result: Any? + queue.sync { + result = storage[key] + } + return result + } +} diff --git a/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift new file mode 100644 index 00000000..aaf6f3d6 --- /dev/null +++ b/OptimoveCore/Sources/Classes/Storage/UserDefaults+AppGroup.swift @@ -0,0 +1,19 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public extension UserDefaults { + static func optimoveAppGroup() throws -> UserDefaults { + let suiteName = Bundle.optimoveAppGroupIdentifier + guard let userDefaults = UserDefaults(suiteName: suiteName) else { + let message = """ + Unable to initialize UserDefault with suit name "\(suiteName)". + Highly possible that the client forgot to add the app group as described in the documentation. + Link: https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/SDK-Setup-Capabilities + """ + assertionFailure(message) + throw GuardError.custom(message) + } + return userDefaults + } +} diff --git a/OptimoveCore/Tests/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/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 9c79734c..0140c4d7 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -37,14 +37,23 @@ 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 AppGroupsHelper.isKumulosAppGroupDefined() { - if let attachment = try await maybeGetAttachment(notification: notification) { + if let storage = try? 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 = optimobileHelper.getBadge(notification: notification) { + storage.set(value: badge, key: .badgeCount) + bestAttemptContent.badge = NSNumber(integerLiteral: badge) + } + let pendingNoticationHelper = PendingNotificationHelper(storage: storage) + pendingNoticationHelper.add( notification: PendingNotification( id: notification.message.id, identifier: request.identifier @@ -85,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, @@ -98,10 +107,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)) ) @@ -121,19 +130,4 @@ public enum OptimoveNotificationService { url: tempURL ) } - - static func maybeSetBadge(bestAttemptContent: UNMutableNotificationContent, userInfo: [AnyHashable: Any]) { - let aps = userInfo["aps"] as! [AnyHashable: Any] - if let contentAvailable = aps["content-available"] as? Int, contentAvailable == 1 { - return - } - - let newBadge: NSNumber? = OptimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) - if newBadge == nil { - return - } - - bestAttemptContent.badge = newBadge - KeyValPersistenceHelper.set(newBadge, forKey: OptimobileUserDefaultsKey.BADGE_COUNT.rawValue) - } } diff --git a/OptimoveSDK/Sources/Classes/DI/Assembly.swift b/OptimoveSDK/Sources/Classes/DI/Assembly.swift index 5b85aa56..7a87c0a6 100644 --- a/OptimoveSDK/Sources/Classes/DI/Assembly.swift +++ b/OptimoveSDK/Sources/Classes/DI/Assembly.swift @@ -12,14 +12,14 @@ final class Assembly { /// A special storage migration, before an actual storage going to be in use. migrate() do { - let keyValureStorage = try UserDefaults.optimove() let fileStorage = try FileStorageImpl( persistentStorageURL: FileManager.optimoveURL(), temporaryStorageURL: FileManager.temporaryURL() ) - return ServiceLocator( + return try ServiceLocator( storageFacade: StorageFacade( - persistantStorage: keyValureStorage, + standardStorage: UserDefaults.optimove(), + appGroupStorage: UserDefaults.optimoveAppGroup(), inMemoryStorage: InMemoryStorage(), fileStorage: fileStorage ) @@ -33,6 +33,7 @@ final class Assembly { private func migrate() { let migrations: [MigrationWork] = [ MigrationWork_3_3_0(), + MigrationWork_6_0_0(), ] migrations .filter { $0.isAllowToMiragte(SDKVersion) } 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/Migration/MigrationWork.swift b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift index 4deddd58..4decf8de 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,49 @@ extension MigrationWork_3_3_0 { } } } + +/// Migration from Kumulos UserDefaults to Optimove UserDefaults. +final class MigrationWork_6_0_0: MigrationWorker { + init() { + super.init(newVersion: .v_6_0_0) + } + + override func isAllowToMiragte(_: String) -> Bool { + guard let storage = try? UserDefaults.optimove() else { + return true + } + let key = StorageKey.migrationVersions + let versions = storage.object(forKey: key.rawValue) as? [String] ?? [] + return !versions.contains(version.rawValue) + } + + override func runMigration() { + Logger.info("Migration from Kumulos UserDefaults to Optimove UserDefaults started") + let keyMapping: [OptimobileUserDefaultsKey: StorageKey] = [ + .REGION: .region, + .MEDIA_BASE_URL: .mediaURL, + .INSTALL_UUID: .installUUID, + .USER_ID: .userID, + .BADGE_COUNT: .badgeCount, + .PENDING_NOTIFICATIONS: .pendingNotifications, + .PENDING_ANALYTICS: .pendingAnaltics, + .IN_APP_LAST_SYNCED_AT: .inAppLastSyncedAt, + .IN_APP_MOST_RECENT_UPDATED_AT: .inAppMostRecentUpdateAt, + .IN_APP_CONSENTED: .inAppConsented, + .DYNAMIC_CATEGORY: .dynamicCategory, + ] + /// Move values from Kumulos UserDefaults to Optimove UserDefaults. + let kumulosStorage = UserDefaults.standard + let optimoveStorage = try! UserDefaults.optimove() + OptimobileUserDefaultsKey.allCases.forEach { key in + if let value = kumulosStorage.object(forKey: key.rawValue), + let storageKey = keyMapping[key] + { + optimoveStorage.set(value: value, key: storageKey) + kumulosStorage.removeObject(forKey: key.rawValue) + } + } + Logger.info("Migration from Kumulos UserDefaults to Optimove UserDefaults completed") + super.runMigration() + } +} diff --git a/OptimoveSDK/Sources/Classes/Migration/Version.swift b/OptimoveSDK/Sources/Classes/Migration/Version.swift index e55b4653..d35ae1f4 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_6_0_0 = "6.0.0" } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift index 3c34e005..b548ee3f 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 = true 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..c5eea925 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,9 +246,6 @@ extension Optimobile { @available(iOS 10.0, *) func maybeTrackPushDismissedEvents() { - if !AppGroupsHelper.isKumulosAppGroupDefined() { - return - } Task { do { let notifications = await UNUserNotificationCenter.current().deliveredNotifications() @@ -259,7 +256,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 { @@ -351,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 { @@ -416,10 +413,15 @@ class PushHelper { } }() - private func setBadge(userInfo: [AnyHashable: Any]) { - let badge: NSNumber? = OptimobileHelper.getBadgeFromUserInfo(userInfo: userInfo) - if let newBadge = badge { - UIApplication.shared.applicationIconBadgeNumber = newBadge.intValue + let optimobileHelper: OptimobileHelper + + init(optimobileHelper: OptimobileHelper) { + self.optimobileHelper = optimobileHelper + } + + private func setBadge(notification: PushNotification) { + if let badge = optimobileHelper.getBadge(notification: notification) { + UIApplication.shared.applicationIconBadgeNumber = badge } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift index 19b0f52f..f17792f2 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift @@ -159,7 +159,7 @@ extension Optimobile { } return [ - "hasGroup": AppGroupsHelper.isKumulosAppGroupDefined(), + "hasGroup": true, "push": push, ] } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift index 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..a530f6e6 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 ) @@ -79,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) { @@ -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..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() { @@ -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,40 @@ public extension Optimove { /// Call this function to unset the customerID and revert to an anonymous visitor func signOutUser() { if config.isOptimoveConfigured() { - let function: (ServiceLocator) -> Void = { serviceLocator in - tryCatch { - serviceLocator.storage().set(value: nil, key: StorageKey.customerID) - serviceLocator.storage().set(value: serviceLocator.storage().initialVisitorId, key: StorageKey.visitorID) - } + container.resolve { serviceLocator in + serviceLocator.storage().set(value: nil, key: StorageKey.customerID) + serviceLocator.storage().set(value: serviceLocator.storage().initialVisitorId, key: StorageKey.visitorID) } - container.resolve(function) } if config.isOptimobileConfigured() { - Optimobile.clearUserAssociation() + container.resolve { serviceLocator in + Optimobile.clearUserAssociation( + storage: serviceLocator.storage() + ) + } + } + } + + enum Debug { + enum Constants { + static let undefined = "" + } + + public static var state: SdkState { + return Optimove.shared.container.resolve { locator in + let storage = locator.storage() + return SdkState( + appVersion: "\(Bundle.main.appVersion) build: \(Bundle.main.buildVersion)", + sdkVersion: Optimove.version, + installation: storage.installationID ?? Constants.undefined, + tenant: storage.tenantID?.description ?? Constants.undefined, + initialVisitor: storage.initialVisitorId ?? Constants.undefined, + customer: storage.customerID ?? Constants.undefined, + email: storage.userEmail ?? Constants.undefined, + updateVisitor: storage.visitorID ?? Constants.undefined + ) + } ?? SdkState.empty } } } diff --git a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift index b3ece124..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: KeyValPersistenceHelper.self) - super.init() + do { + features = [] + // FIXME: Move UrlBuilder out of OptimoveConfigBuilder + urlBuilder = try UrlBuilder(storage: UserDefaults.optimoveAppGroup()) + super.init() + } catch { + fatalError(error.localizedDescription) + } } @discardableResult public func setCredentials(optimoveCredentials: String?, optimobileCredentials: String?) -> OptimoveConfigBuilder { 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/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift similarity index 70% rename from OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift rename to OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift index ab63d8ff..72479c1b 100644 --- a/OptimoveSDK/Sources/Classes/Storage/StorageFacade.swift +++ b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift @@ -1,4 +1,4 @@ -// Copyright © 2019 Optimove. All rights reserved. +// Copyright © 2023 Optimove. All rights reserved. import Foundation import OptimoveCore @@ -6,138 +6,6 @@ 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) @@ -151,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/OptimoveConfigBuilderTests.swift b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift index e4dfb222..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 { - KeyValPersistenceHelper.set(Region.DEV.rawValue, forKey: OptimobileUserDefaultsKey.REGION.rawValue) + try UserDefaults.optimoveAppGroup().set(value: Region.DEV.rawValue, key: .region) let urlsMap = UrlBuilder.defaultMapping(for: Region.DEV.rawValue) let config = optimoveConfigBuilder.build() try UrlBuilder.Service.allCases.forEach { service in diff --git a/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 4ea35af2..20406a0c 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 @@ -8,7 +9,8 @@ class OptimoveStorageFacadeTests: XCTestCase { override func setUp() { storage = StorageFacade( - persistantStorage: MockKeyValueStorage(), + standardStorage: MockKeyValueStorage(), + appGroupStorage: MockKeyValueStorage(), inMemoryStorage: MockKeyValueStorage(), fileStorage: MockFileStorage() )