diff --git a/CHANGELOG.md b/CHANGELOG.md index 06d748d2..4227d6fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## 6.0.0 + +- Add Preference Center feature. The new functionality is available through the following methods: + - `OptimovePreferenceCenter.getInstance().setCustomerPreferencesAsync()` + - `OptimovePreferenceCenter.getInstance().getPreferencesAsync()` + +Breaking changes: +- iOS 13 required for using Preference Center +- removed `OptimoveConfigBuilder.setCredentials` unintentionally public method + +## 5.9.0 + +- Remove `SetUserAgent` event + +## 5.8.0 + +- Add a new public API `Optimove.shared.urlOpened(url: url)` to allow easily forwarding urls to Optimove instead of relying on AppDeletegate's `application(_:continue:restorationHandler:)` +- Fix deeplink decoding issue +- Fix immediate initialization of the SDK + +## 5.7.0 + +- Add Privacy Manifest +- Fix explicit `self` for `weak self` captures to allow pre 5.8 Swift compilation +- Remove web to app banner code + ## 5.6.0 - Support the delayed configuration for SDK. Add new public APIs: diff --git a/Optimove.xcodeproj/project.pbxproj b/Optimove.xcodeproj/project.pbxproj index 1cef58c0..f8a0fa9f 100644 --- a/Optimove.xcodeproj/project.pbxproj +++ b/Optimove.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXFileReference section */ + 58371F3A2BA358CB00791552 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; FE31495485A751448C7DF822 /* SDK */ = {isa = PBXFileReference; lastKnownFileType = folder; name = SDK; path = .; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ @@ -14,6 +15,7 @@ 0FF4DF40AC6AAC3B8661208C = { isa = PBXGroup; children = ( + 58371F3A2BA358CB00791552 /* PrivacyInfo.xcprivacy */, 9BB5B51B953402330D6DC400 /* Packages */, ); sourceTree = ""; diff --git a/Optimove.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme b/Optimove.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme index 10e34e24..aa1295fa 100644 --- a/Optimove.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme +++ b/Optimove.xcodeproj/xcshareddata/xcschemes/UnitTests.xcscheme @@ -11,15 +11,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - @@ -67,7 +58,7 @@ diff --git a/OptimoveCore.podspec b/OptimoveCore.podspec index e4a98448..a78a98df 100644 --- a/OptimoveCore.podspec +++ b/OptimoveCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'OptimoveCore' - s.version = '5.6.0' + s.version = '6.0.0' s.summary = 'Official Optimove SDK for iOS. Core framework.' s.description = 'The core framework is used to share code-base between other Optimove frameworks.' s.homepage = 'https://github.com/optimove-tech/Optimove-SDK-iOS' diff --git a/OptimoveCore/Sources/Classes/Constants/SDKVersion.swift b/OptimoveCore/Sources/Classes/Constants/SDKVersion.swift index 0728ad67..97cc0984 100644 --- a/OptimoveCore/Sources/Classes/Constants/SDKVersion.swift +++ b/OptimoveCore/Sources/Classes/Constants/SDKVersion.swift @@ -1,3 +1,3 @@ // Copyright © 2019 Optimove. All rights reserved. -public let SDKVersion = "5.6.0" +public let SDKVersion = "6.0.0" 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/Sources/Classes/Storage/KeyValueStorage.swift b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift index 3529df0e..5a172881 100644 --- a/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift +++ b/OptimoveCore/Sources/Classes/Storage/KeyValueStorage.swift @@ -11,7 +11,6 @@ public enum StorageKey: String, CaseIterable { case tenantToken case visitorID case version - case userAgent case deviceResolutionWidth case deviceResolutionHeight case advertisingIdentifier @@ -58,7 +57,6 @@ public protocol StorageValue { 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 } @@ -77,7 +75,6 @@ public protocol StorageValue { 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. 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.podspec b/OptimoveNotificationServiceExtension.podspec index 6c35f25d..7238fd81 100644 --- a/OptimoveNotificationServiceExtension.podspec +++ b/OptimoveNotificationServiceExtension.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'OptimoveNotificationServiceExtension' - s.version = '5.6.0' + s.version = '6.0.0' s.summary = 'Official Optimove SDK for iOS. Notification service extension framework.' s.description = 'The notification service extension is used for handling additional content in push notifications.' s.homepage = 'https://github.com/optimove-tech/Optimove-SDK-iOS' @@ -12,5 +12,8 @@ Pod::Spec.new do |s| s.swift_version = '5' base_dir = "OptimoveNotificationServiceExtension/" s.source_files = base_dir + 'Sources/**/*', 'OptimobileShared/**/*' + s.resource_bundles = { + 'OptimoveNotificationServiceExtension' => ['OptimoveNotificationServiceExtension/PrivacyInfo.xcprivacy'] + } s.frameworks = 'Foundation', 'UserNotifications', 'UIKit' end diff --git a/OptimoveNotificationServiceExtension/PrivacyInfo.xcprivacy b/OptimoveNotificationServiceExtension/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..19f89636 --- /dev/null +++ b/OptimoveNotificationServiceExtension/PrivacyInfo.xcprivacy @@ -0,0 +1,23 @@ + + + + + NSPrivacyCollectedDataTypes + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 5deb96c9..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) @@ -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( @@ -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, @@ -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.podspec b/OptimoveSDK.podspec index e703ffdf..62bb1ff1 100644 --- a/OptimoveSDK.podspec +++ b/OptimoveSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'OptimoveSDK' - s.version = '5.6.0' + s.version = '6.0.0' s.summary = 'Official Optimove SDK for iOS.' s.description = 'The Optimove SDK framework is used for reporting events and receive push notifications.' s.homepage = 'https://github.com/optimove-tech/Optimove-SDK-iOS' @@ -14,6 +14,9 @@ Pod::Spec.new do |s| s.swift_version = '5' base_dir = "OptimoveSDK/" s.source_files = base_dir + 'Sources/Classes/**/*', 'OptimobileShared/**/*' + s.resource_bundles = { + 'OptimoveSDK' => ['OptimoveSDK/PrivacyInfo.xcprivacy'] + } s.dependency 'OptimoveCore', s.version.to_s s.frameworks = 'Foundation', 'UIKit', 'SystemConfiguration', 'UserNotifications', 'CoreData' end diff --git a/OptimoveSDK/PrivacyInfo.xcprivacy b/OptimoveSDK/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..7d35afb2 --- /dev/null +++ b/OptimoveSDK/PrivacyInfo.xcprivacy @@ -0,0 +1,50 @@ + + + + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeProductInteraction + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeProductPersonalization + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeUserID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAnalytics + NSPrivacyCollectedDataTypePurposeProductPersonalization + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + 1C8F.1 + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + + diff --git a/OptimoveSDK/Sources/Classes/Constants/OptimoveKeys.swift b/OptimoveSDK/Sources/Classes/Constants/OptimoveKeys.swift index 7e53359f..573f734e 100644 --- a/OptimoveSDK/Sources/Classes/Constants/OptimoveKeys.swift +++ b/OptimoveSDK/Sources/Classes/Constants/OptimoveKeys.swift @@ -23,7 +23,6 @@ enum OptimoveKeys { case userId = "user_id" case realtimeUserId = "userId" case realtimeupdatedVisitorId = "updatedVisitorId" - case setUserAgent = "user_agent_header_event" case email } } diff --git a/OptimoveSDK/Sources/Classes/DI/Assembly.swift b/OptimoveSDK/Sources/Classes/DI/Assembly.swift index 32349912..d754a44b 100644 --- a/OptimoveSDK/Sources/Classes/DI/Assembly.swift +++ b/OptimoveSDK/Sources/Classes/DI/Assembly.swift @@ -33,7 +33,8 @@ final class Assembly { private func migrate() { let migrations: [MigrationWork] = [ MigrationWork_3_3_0(), - MigrationWork_5_7_0(), + MigrationWork_6_0_0(), + MigrationWork_5_9_0() ] migrations .filter { $0.isAllowToMiragte(SDKVersion) } diff --git a/OptimoveSDK/Sources/Classes/Events/CoreEvents/SetUserAgent.swift b/OptimoveSDK/Sources/Classes/Events/CoreEvents/SetUserAgent.swift deleted file mode 100644 index 0e52571f..00000000 --- a/OptimoveSDK/Sources/Classes/Events/CoreEvents/SetUserAgent.swift +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright © 2019 Optimove. All rights reserved. - -final class SetUserAgent: Event { - enum Constants { - static let name = OptimoveKeys.Configuration.setUserAgent.rawValue - static let userAgentSliceLenght: Int = 255 - static let userAgentHeaderBase = "user_agent_header" - } - - init(userAgent: String) { - super.init( - name: Constants.name, - context: userAgent - .split(by: Constants.userAgentSliceLenght) - .enumerated() - .reduce(into: [String: Any]()) { result, userAgent in - let key = Constants.userAgentHeaderBase + String(userAgent.offset + 1) - result[key] = userAgent.element - } - ) - } -} 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/Factories/CoreEventFactory.swift b/OptimoveSDK/Sources/Classes/Factories/CoreEventFactory.swift index f16b47dc..d8739915 100644 --- a/OptimoveSDK/Sources/Classes/Factories/CoreEventFactory.swift +++ b/OptimoveSDK/Sources/Classes/Factories/CoreEventFactory.swift @@ -6,7 +6,6 @@ import OptimoveCore enum CoreEventType { case appOpen case metaData - case setUserAgent case setUserEmail(email: String) case setUser(user: User) case pageVisit(title: String, category: String?) @@ -45,8 +44,6 @@ extension CoreEventFactoryImpl: CoreEventFactory { return try createMetaDataEvent() case let .pageVisit(title: t, category: c): return createPageVisitEvent(title: t, category: c) - case .setUserAgent: - return try createSetUserAgentEvent() case let .setUserEmail(email): return try createSetUserEmailEvent(email: email) } @@ -96,12 +93,6 @@ private extension CoreEventFactoryImpl { return try unwrap(event) } - func createSetUserAgentEvent() throws -> SetUserAgent { - return try SetUserAgent( - userAgent: storage.getUserAgent() - ) - } - func createPageVisitEvent(title: String, category: String?) -> PageVisitEvent { return PageVisitEvent( title: title, diff --git a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift index f58a501a..b1d68d4f 100644 --- a/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift +++ b/OptimoveSDK/Sources/Classes/Migration/MigrationWork.swift @@ -140,7 +140,6 @@ extension MigrationWork_3_3_0 { .tenantToken, .visitorID, .version, - .userAgent, .deviceResolutionWidth, .deviceResolutionHeight, .advertisingIdentifier, @@ -232,9 +231,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 { @@ -273,6 +272,15 @@ final class MigrationWork_5_7_0: MigrationWorker { } } Logger.info("Migration from Kumulos UserDefaults to Optimove UserDefaults completed") +} + +final class MigrationWork_5_9_0: MigrationWorker { + init() { + super.init(newVersion: .v_5_9_0) + } + + override func runMigration() { + try? UserDefaults.optimove().removeObject(forKey: "userAgent") super.runMigration() } } diff --git a/OptimoveSDK/Sources/Classes/Migration/Version.swift b/OptimoveSDK/Sources/Classes/Migration/Version.swift index c1c172ca..b622f25a 100644 --- a/OptimoveSDK/Sources/Classes/Migration/Version.swift +++ b/OptimoveSDK/Sources/Classes/Migration/Version.swift @@ -4,5 +4,6 @@ enum Version: String { case v_2_10_0 = "2.10.0" case v_3_0_0 = "3.0.0" case v_3_3_0 = "3.3.0" - case v_5_7_0 = "5.7.0" + case v_6_0_0 = "6.0.0" + case v_5_9_0 = "5.9.0" } diff --git a/OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift index 97703acf..da308c41 100644 --- a/OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift +++ b/OptimoveSDK/Sources/Classes/NetworkClient/NetworkRequest.swift @@ -87,17 +87,32 @@ extension HTTPHeader { enum Fields: String { case contentType = "Content-Type" case userAgent = "User-Agent" + case tenantId = "X-Tenant-Id" + case accept } - enum Values: String { - case json = "application/json" + enum Values: CustomStringConvertible { + case json + case tenantId(id: String) + case textplain + + public var description: String { + switch self { + case .json: + return "application/json" + case .tenantId(let id): + return id + case .textplain: + return "text/plain" + } + } } } extension HTTPHeader { init(field: Fields, value: Values) { self.field = field.rawValue - self.value = value.rawValue + self.value = String(describing: value) } } diff --git a/OptimoveSDK/Sources/Classes/OnStartHelpers/OnStartEventGenerator.swift b/OptimoveSDK/Sources/Classes/OnStartHelpers/OnStartEventGenerator.swift index 12d878f8..edd4fc6e 100644 --- a/OptimoveSDK/Sources/Classes/OnStartHelpers/OnStartEventGenerator.swift +++ b/OptimoveSDK/Sources/Classes/OnStartHelpers/OnStartEventGenerator.swift @@ -30,11 +30,6 @@ final class OnStartEventGenerator { } private func asyncGenerate() { - UserAgentGenerator( - storage: storage, - synchronizer: synchronizer, - coreEventFactory: coreEventFactory - ).generate() AppOpenOnStartGenerator( synchronizer: synchronizer, coreEventFactory: coreEventFactory diff --git a/OptimoveSDK/Sources/Classes/OnStartHelpers/UserAgentGenerator.swift b/OptimoveSDK/Sources/Classes/OnStartHelpers/UserAgentGenerator.swift deleted file mode 100644 index f0e55482..00000000 --- a/OptimoveSDK/Sources/Classes/OnStartHelpers/UserAgentGenerator.swift +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright © 2019 Optimove. All rights reserved. - -import Foundation -import OptimoveCore -import WebKit - -final class UserAgentGenerator { - private var storage: OptimoveStorage - private let synchronizer: Pipeline - private let coreEventFactory: CoreEventFactory - private var webView: WKWebView? - - init(storage: OptimoveStorage, - synchronizer: Pipeline, - coreEventFactory: CoreEventFactory) - { - self.storage = storage - self.synchronizer = synchronizer - self.coreEventFactory = coreEventFactory - } - - func generate() { - guard Thread.isMainThread else { - DispatchQueue.main.async { - self.generate() - } - return - } - webView = WKWebView(frame: .zero) - webView?.evaluateJavaScript("navigator.userAgent") { result, error in - if let error = error { - Logger.error(error.localizedDescription) - } - self.storage.userAgent = (result as? String) ?? "user_agent_undefined" - tryCatch { - let event = try self.coreEventFactory.createEvent(.setUserAgent) - self.synchronizer.deliver(.report(events: [event])) - } - self.webView = nil - } - } -} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/DeepLinkFingerprinter.swift b/OptimoveSDK/Sources/Classes/Optimobile/DeepLinkFingerprinter.swift deleted file mode 100644 index d2e8556a..00000000 --- a/OptimoveSDK/Sources/Classes/Optimobile/DeepLinkFingerprinter.swift +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation -import WebKit - -private enum PrintDustMessage: String { - case clientReady = "READY" - case clientFingerprintGeneraged = "FINGERPRINT_GENERATED" - case requestFingerprint = "REQUEST_FINGERPRINT" -} - -private enum DeferredState { - case pending - case resolved(R) -} - -private class Deferred { - var state: DeferredState - var pendingWatchers: [(R) -> Void] - - init() { - state = .pending - pendingWatchers = [] - } - - func resolve(result: R) { - DispatchQueue.main.async { - switch self.state { - case .resolved: return - default: break - } - - self.state = DeferredState.resolved(result) - - self.pendingWatchers.forEach { cb in - cb(result) - } - - self.pendingWatchers.removeAll() - } - } - - func then(onResult: @escaping (R) -> Void) { - DispatchQueue.main.async { - switch self.state { - case .pending: - self.pendingWatchers.append(onResult) - case let .resolved(result): - onResult(result) - } - } - } -} - -class DeepLinkFingerprinter: NSObject, WKScriptMessageHandler, WKNavigationDelegate { - fileprivate static let printDustRuntimeUrl = "https://pd.app.delivery" - fileprivate static let printDustHandlerName = "printHandler" - - fileprivate var webView: WKWebView? - fileprivate let fingerprint: Deferred<[String: String]> - - override init() { - let controller = WKUserContentController() - let config = WKWebViewConfiguration() - config.userContentController = controller - - webView = WKWebView(frame: UIScreen.main.bounds, configuration: config) - fingerprint = Deferred() - - super.init() - - controller.add(self, name: DeepLinkFingerprinter.printDustHandlerName) - - let request = URLRequest(url: URL(string: DeepLinkFingerprinter.printDustRuntimeUrl)!, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10) - webView?.load(request) - } - - func getFingerprintComponents(_ onGenerated: @escaping ([String: String]) -> Void) { - fingerprint.then(onResult: onGenerated) - } - - func userContentController(_: WKUserContentController, didReceive message: WKScriptMessage) { - if message.name != DeepLinkFingerprinter.printDustHandlerName { - return - } - - let body = message.body as! NSDictionary - let type = body["type"] as! String - - switch type { - case PrintDustMessage.clientReady.rawValue: - postClientMessage(type: PrintDustMessage.requestFingerprint.rawValue, data: nil) - case PrintDustMessage.clientFingerprintGeneraged.rawValue: - guard let data = body["data"] as? [String: AnyObject], - let components = data["components"] as? [String: String] - else { - return - } - fingerprint.resolve(result: components) - DispatchQueue.main.async { self.cleanUpWebView() } - default: - print("Unhandled message: \(type)") - } - } - - func webView(_: WKWebView, didFail _: WKNavigation!, withError _: Error) { - DispatchQueue.main.async { self.cleanUpWebView() } - } - - fileprivate func postClientMessage(type: String, data: Any?) { - do { - let msg: [String: Any] = ["type": type, "data": data != nil ? data! : NSNull()] - let json: Data = try JSONSerialization.data(withJSONObject: msg, options: JSONSerialization.WritingOptions(rawValue: 0)) - - let jsonMsg = String(data: json, encoding: .utf8) - let evalString = String(format: "postHostMessage(%@);", jsonMsg!) - - webView?.evaluateJavaScript(evalString, completionHandler: nil) - } catch { - // Noop - } - } - - fileprivate func cleanUpWebView() { - webView?.stopLoading() - webView?.configuration.userContentController.removeScriptMessageHandler(forName: DeepLinkFingerprinter.printDustHandlerName) - webView = nil - } -} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift index 19d748c5..9085e668 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift @@ -60,7 +60,7 @@ class InAppManager { finishedInitializationToken = NotificationCenter.default .addObserver(forName: .optimobileInializationFinished, object: nil, queue: nil) { [weak self] notification in guard let self = self else { return } - handleEnrollmentAndSyncSetup() + self.handleEnrollmentAndSyncSetup() Logger.debug("Notification \(notification.name.rawValue) was processed") } } @@ -173,7 +173,7 @@ class InAppManager { func userConsented() -> Bool { // Note if this implementation is changed there is a usage in the main Optimobile initialisation path // that should be considered. - return UserDefaults.standard.bool(forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) + return storage[.inAppConsented] ?? false } func updateUserConsent(consentGiven: Bool) { @@ -182,7 +182,7 @@ class InAppManager { Optimobile.trackEventImmediately(eventType: OptimobileEvent.IN_APP_CONSENT_CHANGED.rawValue, properties: props) if consentGiven { - UserDefaults.standard.set(consentGiven, forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) + storage.set(value: consentGiven, key: .inAppConsented) handleEnrollmentAndSyncSetup() } else { DispatchQueue.global(qos: .default).async { @@ -237,9 +237,9 @@ class InAppManager { } NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_CONSENTED.rawValue) - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) - UserDefaults.standard.removeObject(forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) + storage.set(value: nil, key: .inAppConsented) + storage.set(value: nil, key: .inAppLastSyncedAt) + storage.set(value: nil, key: .inAppMostRecentUpdateAt) context.performAndWait { let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Message") @@ -267,23 +267,23 @@ class InAppManager { // MARK: Message management func syncDebounced(_ onComplete: InAppSyncCompletionHandler? = nil) { - syncQueue.async { - let lastSyncedAt = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) as? Date ?? Date(timeIntervalSince1970: 0) - - if lastSyncedAt.timeIntervalSinceNow < self.SYNC_DEBOUNCE_SECONDS { + syncQueue.async { [unowned self] in + let lastSyncedAt = storage.value(for: .inAppLastSyncedAt) as? Date ?? Date(timeIntervalSince1970: 0) + if lastSyncedAt.timeIntervalSinceNow < SYNC_DEBOUNCE_SECONDS { return } - self.sync(onComplete) + sync(onComplete) } } func sync(_ onComplete: InAppSyncCompletionHandler? = nil) { let currentUserIdentifier = optimobileHelper.currentUserIdentifier() - syncQueue.async { + syncQueue.async { [unowned self] in let syncBarrier = DispatchSemaphore(value: 0) - let mostRecentUpdate = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) as? NSDate + let mostRecentUpdate = storage.value(for: .inAppMostRecentUpdateAt) as? Date + var after = "" if let mostRecentUpdate = mostRecentUpdate { @@ -298,9 +298,9 @@ class InAppManager { let encodedIdentifier = KSHttpUtil.urlEncode(currentUserIdentifier) let path = "/v1/users/\(encodedIdentifier!)/messages\(after)" - self.httpClient.sendRequest(.GET, toPath: path, data: nil, onSuccess: { _, decodedBody in + httpClient.sendRequest(.GET, toPath: path, data: nil, onSuccess: { [weak self] _, decodedBody in defer { - UserDefaults.standard.set(Date(), forKey: OptimobileUserDefaultsKey.IN_APP_LAST_SYNCED_AT.rawValue) + self?.storage.set(value: Date(), key: .inAppLastSyncedAt) syncBarrier.signal() } @@ -310,7 +310,7 @@ class InAppManager { return } - self.persistInAppMessages(messages: messagesToPersist!) + self?.persistInAppMessages(messages: messagesToPersist!) onComplete?(1) DispatchQueue.main.async { @@ -318,9 +318,15 @@ class InAppManager { return } - DispatchQueue.global(qos: .default).async { - let messagesToPresent = self.getMessagesToPresent([InAppPresented.IMMEDIATELY.rawValue]) - self.presenter.queueMessagesForPresentation(messages: messagesToPresent, tickleIds: self.pendingTickleIds) + DispatchQueue.global(qos: .default).async { [weak self] in + if let messagesToPresent = self?.getMessagesToPresent([InAppPresented.IMMEDIATELY.rawValue]), + let pendingTickleIds = self?.pendingTickleIds + { + self?.presenter.queueMessagesForPresentation( + messages: messagesToPresent, + tickleIds: pendingTickleIds + ) + } } } }, onFailure: { _, _, _ in @@ -447,8 +453,7 @@ class InAppManager { removeNotificationTickle(id: idEvicted) } - UserDefaults.standard.set(mostRecentUpdate, forKey: OptimobileUserDefaultsKey.IN_APP_MOST_RECENT_UPDATED_AT.rawValue) - + storage.set(value: mostRecentUpdate, key: .inAppMostRecentUpdateAt) trackMessageDelivery(messages: messages) let inboxUpdated = fetchedWithInbox || evictedWithInbox || evictedExceedersWithInbox diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift index 25344eb4..f55a0e68 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift @@ -13,11 +13,10 @@ public struct DeepLink { public let url: URL public let content: DeepLinkContent public let data: [AnyHashable: Any?] - - init?(for url: URL, from jsonData: Data) { - guard let response = try? JSONSerialization.jsonObject(with: jsonData) as? [AnyHashable: Any], - let linkData = response["linkData"] as? [AnyHashable: Any?], - let content = response["content"] as? [AnyHashable: Any?] + + init?(for url: URL, from jsonData: [AnyHashable: Any]) { + guard let linkData = jsonData["linkData"] as? [AnyHashable: Any?], + let content = jsonData["content"] as? [AnyHashable: Any?] else { return nil } @@ -49,7 +48,6 @@ final class DeepLinkHelper { let storage: OptimoveStorage var anyContinuationHandled: Bool var cachedLink: CachedLink? - var cachedFingerprintComponents: [String: String]? var finishedInitializationToken: NSObjectProtocol? init(_ config: OptimobileConfig, httpClient: KSHttpClient, storage: OptimoveStorage) { @@ -70,10 +68,6 @@ final class DeepLinkHelper { handleDeepLinkUrl(cachedLink.url, wasDeferred: cachedLink.wasDeferred) self.cachedLink = nil } - if let cachedFingerprintComponents = cachedFingerprintComponents { - handleFingerprintComponents(components: cachedFingerprintComponents) - self.cachedFingerprintComponents = nil - } } func checkForNonContinuationLinkMatch() { @@ -86,12 +80,6 @@ final class DeepLinkHelper { @objc func appBecameActive() { NotificationCenter.default.removeObserver(self) - - if anyContinuationHandled { - return - } - - checkForWebToAppBannerTap() } private func checkForDeferredLinkOnClipboard() -> Bool { @@ -118,17 +106,7 @@ final class DeepLinkHelper { return handled } - - private func checkForWebToAppBannerTap() { - let fp = DeepLinkFingerprinter() - - fp.getFingerprintComponents { components in - DispatchQueue.global().async { - self.handleFingerprintComponents(components: components) - } - } - } - + private func urlShouldBeHandled(_ url: URL) -> Bool { guard let host = url.host else { return false @@ -148,8 +126,8 @@ final class DeepLinkHelper { httpClient.sendRequest(.GET, toPath: path, data: nil, onSuccess: { res, data in switch res?.statusCode { case 200: - guard let jsonData = data as? Data, - let link = DeepLink(for: url, from: jsonData) + guard let dictionary = data as? [AnyHashable: Any], + let link = DeepLink(for: url, from: dictionary) else { self.invokeDeepLinkHandler(.lookupFailed(url)) return @@ -182,64 +160,6 @@ final class DeepLinkHelper { }) } - private func handleFingerprintComponents(components: [String: String]) { - guard let componentJson = try? JSONSerialization.data(withJSONObject: components, options: JSONSerialization.WritingOptions(rawValue: 0)), - let encodedComponents = KSHttpUtil.urlEncode(componentJson.base64EncodedString()) - else { - return - } - - let path = "/v1/deeplinks/_taps?fingerprint=\(encodedComponents)" - - httpClient.sendRequest(.GET, toPath: path, data: nil, onSuccess: { res, data in - switch res?.statusCode { - case 200: - guard let jsonData = data as? Data, - let response = try? JSONSerialization.jsonObject(with: jsonData) as? [AnyHashable: Any], - let urlString = response["linkUrl"] as? String, - let url = URL(string: urlString), - let link = DeepLink(for: url, from: jsonData) - else { - // Fingerprint matches that fail to parse correctly can't know the URL so - // don't invoke any error handler. - return - } - - self.invokeDeepLinkHandler(.linkMatched(link)) - - let linkProps = ["url": url.absoluteString, "wasDeferred": false] as [String: Any] - Optimobile.getInstance().analyticsHelper.trackEvent(eventType: OptimobileEvent.DEEP_LINK_MATCHED.rawValue, properties: linkProps, immediateFlush: false) - default: - // Noop - break - } - }, onFailure: { res, error, data in - if let error = error { - if case HttpAuthorizationError.missingAuthHeader = error { - self.cachedFingerprintComponents = components - return - } - } - guard let jsonData = data as? Data, - let response = try? JSONSerialization.jsonObject(with: jsonData) as? [AnyHashable: Any], - let urlString = response["linkUrl"] as? String, - let url = URL(string: urlString) - else { - return - } - - switch res?.statusCode { - case 410: - self.invokeDeepLinkHandler(.linkExpired(url)) - case 429: - self.invokeDeepLinkHandler(.linkLimitExceeded(url)) - default: - // Noop - break - } - }) - } - private func invokeDeepLinkHandler(_ resolution: DeepLinkResolution) { DispatchQueue.main.async { self.config.deepLinkHandler?(resolution) @@ -254,20 +174,39 @@ final class DeepLinkHelper { } guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let url = userActivity.webpageURL, - urlShouldBeHandled(url) + let url = userActivity.webpageURL else { return false } - anyContinuationHandled = true + return handleContinuation(for: url) + } + + @discardableResult + fileprivate func handleContinuation(for url: URL) -> Bool { + if config.deepLinkHandler == nil { + print("Optimobile deep link handler not configured, aborting...") + return false + } + return handleUrl(url: url) + } + + fileprivate func handleUrl(url: URL) -> Bool { + if !urlShouldBeHandled(url) { + return false + } + handleDeepLinkUrl(url) return true } } extension Optimobile { + static func urlOpened(url: URL) -> Bool { + return getInstance().deepLinkHelper?.handleContinuation(for: url) ?? false + } + static func application(_: UIApplication, continue userActivity: NSUserActivity, restorationHandler _: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return getInstance().deepLinkHelper?.handleContinuation(for: userActivity) ?? false } 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 } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift index 367df907..cca11234 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift @@ -111,10 +111,10 @@ final class Optimobile { throw Error.configurationIsMissing } - try writeDefaultsKeys(config: config, storage: storage) - instance = Optimobile(config: config, storage: storage) + try writeDefaultsKeys(config: config, storage: storage) + instance!.initializeHelpers() if #available(iOS 10.0, *) { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift index e2f4f9db..39fa95f2 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift @@ -4,15 +4,15 @@ import CoreData import Foundation import OptimoveCore -class InAppInboxItem { - private(set) var id: Int64 - private(set) var title: String - private(set) var subtitle: String - private(set) var availableFrom: Date? - private(set) var availableTo: Date? - private(set) var dismissedAt: Date? - private(set) var sentAt: Date - private(set) var data: NSDictionary? +public class InAppInboxItem: Identifiable { + public private(set) var id: Int64 + public private(set) var title: String + public private(set) var subtitle: String + public private(set) var availableFrom: Date? + public private(set) var availableTo: Date? + public private(set) var dismissedAt: Date? + public private(set) var sentAt: Date + public private(set) var data: NSDictionary? private var readAt: Date? private var imagePath: String? @@ -43,7 +43,7 @@ class InAppInboxItem { imagePath = inboxConfig["imagePath"] as? String } - func isAvailable() -> Bool { + public func isAvailable() -> Bool { if availableFrom != nil, availableFrom!.timeIntervalSinceNow > 0 { return false } else if availableTo != nil, availableTo!.timeIntervalSinceNow < 0 { @@ -53,15 +53,15 @@ class InAppInboxItem { return true } - func isRead() -> Bool { + public func isRead() -> Bool { return readAt != nil } - func getImageUrl() -> URL? { + public func getImageUrl() -> URL? { return getImageUrl(width: InAppInboxItem.defaultImageWidth) } - func getImageUrl(width: UInt) -> URL? { + public func getImageUrl(width: UInt) -> URL? { if let imagePathNotNil = imagePath { return try? mediaHelper.getCompletePictureUrl( pictureUrlString: imagePathNotNil, @@ -73,18 +73,18 @@ class InAppInboxItem { } } -struct InAppInboxSummary { - let totalCount: Int64 - let unreadCount: Int64 +public struct InAppInboxSummary { + public let totalCount: Int64 + public let unreadCount: Int64 } -typealias InboxUpdatedHandlerBlock = () -> Void -typealias InboxSummaryBlock = (InAppInboxSummary?) -> Void +public typealias InboxUpdatedHandlerBlock = () -> Void +public typealias InboxSummaryBlock = (InAppInboxSummary?) -> Void -enum OptimoveInApp { +public enum OptimoveInApp { private static var _inboxUpdatedHandlerBlock: InboxUpdatedHandlerBlock? - static func updateConsent(forUser consentGiven: Bool) { + public static func updateConsent(forUser consentGiven: Bool) { if Optimobile.inAppConsentStrategy != InAppConsentStrategy.explicitByUser { NSException(name: NSExceptionName(rawValue: "Optimobile: Invalid In-app consent strategy"), reason: "You can only manage in-app messaging consent when the feature is enabled and strategy is set to InAppConsentStrategyExplicitByUser", userInfo: nil).raise() @@ -94,14 +94,21 @@ enum OptimoveInApp { Optimobile.sharedInstance.inAppManager.updateUserConsent(consentGiven: consentGiven) } - static func setDisplayMode(mode: InAppDisplayMode) { + public static func setDisplayMode(mode: InAppDisplayMode) { Optimobile.sharedInstance.inAppManager.presenter.setDisplayMode(mode) } - static func getDisplayMode() -> InAppDisplayMode { + public static func getDisplayMode() -> InAppDisplayMode { return Optimobile.sharedInstance.inAppManager.presenter.getDisplayMode() } + public static func getInboxItems() -> [InAppInboxItem] { + return Optimove.shared.container.resolve { container in + let storage = container.storage() + return getInboxItems(storage: storage) + } ?? [] + } + static func getInboxItems(storage: OptimoveStorage) -> [InAppInboxItem] { guard let context = Optimobile.sharedInstance.inAppManager.messagesContext else { return [] @@ -145,7 +152,7 @@ enum OptimoveInApp { return results } - static func presentInboxMessage(item: InAppInboxItem) -> InAppMessagePresentationResult { + public static func presentInboxMessage(item: InAppInboxItem) -> InAppMessagePresentationResult { if getDisplayMode() == .paused { return .PAUSED } @@ -159,11 +166,11 @@ enum OptimoveInApp { return result ? InAppMessagePresentationResult.PRESENTED : InAppMessagePresentationResult.FAILED } - static func deleteMessageFromInbox(item: InAppInboxItem) -> Bool { + public static func deleteMessageFromInbox(item: InAppInboxItem) -> Bool { return Optimobile.sharedInstance.inAppManager.deleteMessageFromInbox(withId: item.id) } - static func markAsRead(item: InAppInboxItem) -> Bool { + public static func markAsRead(item: InAppInboxItem) -> Bool { if item.isRead() { return false } @@ -173,15 +180,15 @@ enum OptimoveInApp { return res } - static func markAllInboxItemsAsRead() -> Bool { + public static func markAllInboxItemsAsRead() -> Bool { return Optimobile.sharedInstance.inAppManager.markAllInboxItemsAsRead() } - static func setOnInboxUpdated(inboxUpdatedHandlerBlock: InboxUpdatedHandlerBlock?) { + public static func setOnInboxUpdated(inboxUpdatedHandlerBlock: InboxUpdatedHandlerBlock?) { _inboxUpdatedHandlerBlock = inboxUpdatedHandlerBlock } - static func getInboxSummaryAsync(inboxSummaryBlock: @escaping InboxSummaryBlock) { + public static func getInboxSummaryAsync(inboxSummaryBlock: @escaping InboxSummaryBlock) { Optimobile.sharedInstance.inAppManager.readInboxSummary(inboxSummaryBlock: inboxSummaryBlock) } diff --git a/OptimoveSDK/Sources/Classes/Optimove.swift b/OptimoveSDK/Sources/Classes/Optimove.swift index 73efa480..235ae38e 100644 --- a/OptimoveSDK/Sources/Classes/Optimove.swift +++ b/OptimoveSDK/Sources/Classes/Optimove.swift @@ -12,13 +12,14 @@ typealias Logger = OptimoveCore.Logger /// - WARNING: /// To initialize and configure SDK using `Optimove.configure(for:)` first. @objc public final class Optimove: NSObject { + public typealias InApp = OptimoveInApp /// The current OptimoveSDK version string value. public static let version = OptimoveCore.SDKVersion /// The shared instance of Optimove SDK. @objc public static let shared: Optimove = .init() - private let container: Container + let container: Container private var config: OptimoveConfig! override private init() { @@ -60,20 +61,57 @@ typealias Logger = OptimoveCore.Logger throw GuardError.custom("Failed on OptimobileSDK initialization. Reason: \(error.localizedDescription)") } } + } + + if config.isPreferenceCenterConfigured() { + guard config.isOptimoveConfigured() else { + Logger.error("Preference Center requires the optimove feature enabled.") + return + } + + shared.container.resolve { serviceLocator in + do { + try OptimovePreferenceCenter.initialize(with: config, storage: serviceLocator.storage(), networkClient: NetworkClientImpl()) + } catch { + throw GuardError.custom("Failed on PreferenceCenter initialization. Reason: \(error.localizedDescription)") + } + } } } + static func getConfig() -> OptimoveConfig? { + return shared.config + } + /// Set the credentials for the Optimove server. Intent to use as a step for the delayed initialization. - public static func setCredentials(optimoveCredentials: String?, optimobileCredentials: String?) { + private static func setCredentialsInternal(optimoveCredentials: String?, optimobileCredentials: String?, preferenceCenterCredentials: String? = nil) { guard let currentConfig = shared.config else { Logger.error("Optimove SDK is not configured yet. Please call Optimove.initialize(with:) first.") return } + let builder = OptimoveConfigBuilder(from: currentConfig) - builder.setCredentials(optimoveCredentials: optimoveCredentials, optimobileCredentials: optimobileCredentials) + builder.setCredentials( + optimoveCredentials: optimoveCredentials, + optimobileCredentials: optimobileCredentials, + preferenceCenterCredentials: preferenceCenterCredentials + ) + let config = builder.build() initialize(with: config) } + + public static func setCredentials(optimoveCredentials: String?, optimobileCredentials: String?) { + setCredentialsInternal(optimoveCredentials: optimoveCredentials, optimobileCredentials: optimobileCredentials) + } + + public static func setCredentials(optimoveCredentials: String?, optimobileCredentials: String?, preferenceCenterCredentials: String?) { + setCredentialsInternal( + optimoveCredentials: optimoveCredentials, + optimobileCredentials: optimobileCredentials, + preferenceCenterCredentials: preferenceCenterCredentials + ) + } public static func isFeatureRunning(_ feature: Feature) -> Bool { switch feature { @@ -81,6 +119,8 @@ typealias Logger = OptimoveCore.Logger return Optimobile.isSdkRunning case .optimove: return RunningFlagsIndication.isSdkRunning + case .preferenceCenter: + return OptimovePreferenceCenter.isSdkRunning default: return false } @@ -312,6 +352,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 @@ -363,6 +425,13 @@ public extension Optimove { @objc func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { return Optimobile.application(application, continue: userActivity, restorationHandler: restorationHandler) } + + /** + Used for Deferred Deep Linking to pass the continuation url to the Optimove SDK to be processed. + */ + @objc func urlOpened(url: URL) -> Bool { + return Optimobile.urlOpened(url: url) + } /** Used for Deferred Deep Linking to pass the continuation to the Optimove SDK to be processed in scene-based apps. diff --git a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift index 4ebba084..aee90cc0 100644 --- a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift +++ b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift @@ -14,6 +14,8 @@ public struct Feature: OptionSet, @unchecked Sendable, CustomStringConvertible { public static let optimobile = Feature(rawValue: 1 << 1) /// Optimove feature. public static let optimove = Feature(rawValue: 1 << 2) + /// Preference center feature. + public static let preferenceCenter = Feature(rawValue: 1 << 4) static let delayedConfiguration = Feature(rawValue: 1 << 3) public init(rawValue: Int) { @@ -31,6 +33,9 @@ public struct Feature: OptionSet, @unchecked Sendable, CustomStringConvertible { if contains(.delayedConfiguration) { descriptions.append("Delayed Configuration") } + if contains(.preferenceCenter) { + descriptions.append("Preference Center") + } return descriptions.isEmpty ? "No Features" : descriptions.joined(separator: ", ") } @@ -40,6 +45,7 @@ public struct OptimoveConfig { let features: Feature let tenantInfo: OptimoveTenantInfo? let optimobileConfig: OptimobileConfig? + let preferenceCenterConfig: PreferenceCenterConfig? func isOptimoveConfigured() -> Bool { return features.contains(.optimove) @@ -48,6 +54,14 @@ public struct OptimoveConfig { func isOptimobileConfigured() -> Bool { return features.contains(.optimobile) } + + func isPreferenceCenterConfigured() -> Bool { + return features.contains(.preferenceCenter) + } + + func getPreferenceCenterConfig() -> PreferenceCenterConfig? { + return preferenceCenterConfig + } } class OptimoveTenantInfo: NSObject { @@ -90,6 +104,7 @@ public typealias Region = OptimobileConfig.Region open class OptimoveConfigBuilder: NSObject { private var credentials: OptimobileCredentials? + private var preferenceCenterCredentials: String? public private(set) var features: Feature var region: OptimobileConfig.Region? var urlBuilder: UrlBuilder @@ -159,27 +174,44 @@ open class OptimoveConfigBuilder: NSObject { } } - @discardableResult public func setCredentials(optimoveCredentials: String?, optimobileCredentials: String?) -> OptimoveConfigBuilder { - if optimoveCredentials == nil, optimoveCredentials == nil { + @discardableResult func setCredentials( + optimoveCredentials: String?, + optimobileCredentials: String?, + preferenceCenterCredentials: String? = nil + ) -> OptimoveConfigBuilder { + if optimoveCredentials == nil, optimobileCredentials == nil { assertionFailure("Should provide at least optimove or optimobile credentials") } - do { - if let optimoveCredentials = optimoveCredentials, !optimoveCredentials.isEmpty { + + if let optimoveCredentials = optimoveCredentials, !optimoveCredentials.isEmpty { + do { let args = try OptimoveArguments(base64: optimoveCredentials) + features.insert(.optimove) _tenantToken = args.tenantToken _configName = args.configName + } catch { + Logger.error("Invalid Optimove credentials: \(error.localizedDescription)") } - } catch { - Logger.error(error.localizedDescription) } - do { - if let optimobileCredentials = optimobileCredentials, !optimobileCredentials.isEmpty { + + if let optimobileCredentials = optimobileCredentials, !optimobileCredentials.isEmpty { + do { let args = try OptimobileArguments(base64: optimobileCredentials) + features.insert(.optimobile) credentials = args.credentials region = args.region + } catch { + Logger.error("Invalid Optimobile credentials: \(error.localizedDescription)") + } + } + + if let preferenceCenterCredentials = preferenceCenterCredentials, !preferenceCenterCredentials.isEmpty { + if optimoveCredentials == nil || optimoveCredentials == "" { + Logger.error("Preference Center requires optimove credentials set"); + } else { + self.preferenceCenterCredentials = preferenceCenterCredentials + features.insert(.preferenceCenter) } - } catch { - Logger.error(error.localizedDescription) } return self @@ -228,6 +260,14 @@ open class OptimoveConfigBuilder: NSObject { return self } + @discardableResult public func enablePreferenceCenter(credentials: String) -> OptimoveConfigBuilder + { + features.insert(.preferenceCenter) + preferenceCenterCredentials = credentials + + return self + } + /** Internal SDK embedding API to support override of stats data in x-plat SDKs. Do not call or depend on this method in your app */ @@ -305,16 +345,51 @@ open class OptimoveConfigBuilder: NSObject { return nil }() + let preferenceCenterConfig: PreferenceCenterConfig? = { + if !features.contains(.optimove), + tenantInfo == nil, + !features.contains(.delayedConfiguration) { + Logger.error("Preference center cannot be inialized without optimove") + return nil + } + + if preferenceCenterCredentials == nil { + if !features.contains(.delayedConfiguration) { + Logger.error("Preference center could not be initialized due to missing credentials.") + } + return nil + } + + return getPreferenceCenterConfig(from: preferenceCenterCredentials) + }() + + return OptimoveConfig( features: features, tenantInfo: tenantInfo, - optimobileConfig: optimobileConfig + optimobileConfig: optimobileConfig, + preferenceCenterConfig: preferenceCenterConfig ) } + + private func getPreferenceCenterConfig(from credentials: String?) -> PreferenceCenterConfig? { + do { + let args = try PreferenceCenterArguments(base64: credentials!) + + return PreferenceCenterConfig( + region: args.region, + tenantId: args.tenantId, + brandGroupId: args.brandGroupId + ) + } catch { + Logger.error("Invalid preference center credentials: \(error.localizedDescription)") + return nil + } + } } public extension OptimobileConfig { - enum Region: String, CaseIterable { + enum Region: String, CaseIterable, Codable { case DEV = "uk-1" case EU = "eu-central-2" case US = "us-east-1" @@ -431,3 +506,50 @@ struct OptimobileArguments: Decodable { credentials = OptimobileCredentials(apiKey: apiKey, secretKey: secretKey) } } + +struct PreferenceCenterArguments: Decodable { + enum Error: Foundation.LocalizedError { + case emptyBase64 + case failedDecodingBase64(String) + + var errorDescription: String? { + switch self { + case .emptyBase64: + return "The base64 string is empty" + case let .failedDecodingBase64(string): + return "Failed on decoding base64 the value \(string)" + } + } + } + + let version: String + let region: String + let tenantId: Int + let brandGroupId: String + + enum CodingKeys: String, CodingKey { + case version + case environment + case tenantId + case brandGroupId + } + + init(base64: String) throws { + guard !base64.isEmpty else { + throw Error.emptyBase64 + } + guard let data = Data(base64Encoded: base64) else { + throw Error.failedDecodingBase64(base64) + } + self = try JSONDecoder().decode(PreferenceCenterArguments.self, from: data) + } + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + version = try container.decode(String.self) + region = try container.decode(String.self) + tenantId = try container.decode(Int.self) + brandGroupId = try container.decode(String.self) + } +} diff --git a/OptimoveSDK/Sources/Classes/PreferenceCenter/Models/OptimovePC.swift b/OptimoveSDK/Sources/Classes/PreferenceCenter/Models/OptimovePC.swift new file mode 100644 index 00000000..6a642d5c --- /dev/null +++ b/OptimoveSDK/Sources/Classes/PreferenceCenter/Models/OptimovePC.swift @@ -0,0 +1,66 @@ +import Foundation + +public struct OptimovePC { + public enum Channel: Int, Codable { + case mobilePush = 489 + case webPush = 490 + case sms = 493 + case inApp = 427 + case whatsapp = 498 + case mail = 15 + case inbox = 495 + } + + public struct Topic: Codable { + public let id: String + public let name: String + public let description: String + public let subscribedChannels: [Channel] + + init(id: String, name: String, description: String, subscribedChannels: [Channel]) { + self.id = id + self.name = name + self.description = description + self.subscribedChannels = subscribedChannels + } + + enum CodingKeys: String, CodingKey { + case id = "topicId" + case name = "topicName" + case description = "topicDescription" + case subscribedChannels = "channelSubscription" + } + } + + public struct Preferences: Codable { + public let customerPreferences: [Topic] + public let configuredChannels: [Channel] + + init(customerPreferences: [Topic], configuredChannels: [Channel]) { + self.customerPreferences = customerPreferences + self.configuredChannels = configuredChannels + } + + enum CodingKeys: String, CodingKey { + case customerPreferences = "topics" + case configuredChannels = "channels" + } + } + + public struct PreferenceUpdate: Codable { + public let topicId: String + public let subscribedChannels: [Channel] + + public init(topicId: String, subscribedChannels: [Channel]) { + self.topicId = topicId + self.subscribedChannels = subscribedChannels + } + + enum CodingKeys: String, CodingKey { + case topicId = "topicId" + case subscribedChannels = "channelSubscription" + } + } +} + + diff --git a/OptimoveSDK/Sources/Classes/PreferenceCenter/Models/PreferenceCenterConfig.swift b/OptimoveSDK/Sources/Classes/PreferenceCenter/Models/PreferenceCenterConfig.swift new file mode 100644 index 00000000..d9c5c6c9 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/PreferenceCenter/Models/PreferenceCenterConfig.swift @@ -0,0 +1,5 @@ +struct PreferenceCenterConfig: Codable { + let region: String + let tenantId: Int + let brandGroupId: String +} diff --git a/OptimoveSDK/Sources/Classes/PreferenceCenter/OptimovePreferenceCenter.swift b/OptimoveSDK/Sources/Classes/PreferenceCenter/OptimovePreferenceCenter.swift new file mode 100644 index 00000000..636a5bf2 --- /dev/null +++ b/OptimoveSDK/Sources/Classes/PreferenceCenter/OptimovePreferenceCenter.swift @@ -0,0 +1,239 @@ +import Foundation +import OptimoveCore + +public class OptimovePreferenceCenter { + enum Error: LocalizedError { + case alreadyInitialized + case notInitialized + case configurationIsMissing + + var errorDescription: String? { + switch self { + case .alreadyInitialized: + return "The PreferenceCenterSDK has already been initialized." + case .notInitialized: + return "Preference center has not been initialized." + case .configurationIsMissing: + return "Preference center configuration is missing, but the feauture was requested. Please provide valid credentials." + } + } + } + + private static var instance: OptimovePreferenceCenter? + private var networkClient: NetworkClient? + private var storage: OptimoveStorage? + + static var isSdkRunning: Bool { + return Optimove.getConfig()?.getPreferenceCenterConfig() != nil + } + + public enum ResultType { + case success + case errorUserNotSet + case errorCredentialsNotSet + case error + } + + public typealias PreferencesGetHandler = (_ result: ResultType, _ preferences: OptimovePC.Preferences?) -> Void + public typealias PreferencesSetHandler = (_ result: ResultType) -> Void + + public static func getInstance() throws -> OptimovePreferenceCenter { + guard let instance = instance else { + throw Error.notInitialized + } + return instance + } + + static func initialize(with optimoveConfig: OptimoveConfig, storage: OptimoveStorage, networkClient: NetworkClient) throws { + if instance !== nil, optimoveConfig.features.contains(.delayedConfiguration) { + guard optimoveConfig.preferenceCenterConfig != nil else { + throw Error.configurationIsMissing + } + return + } + + guard instance == nil else { + assertionFailure(Error.alreadyInitialized.localizedDescription) + throw Error.alreadyInitialized + } + + instance = OptimovePreferenceCenter(storage: storage, networkClient: networkClient) + } + + private init(storage: OptimoveStorage, networkClient: NetworkClient) { + self.networkClient = networkClient + self.storage = storage + } + + @available(iOS 13.0, *) + public func getPreferencesAsync(completion: @escaping PreferencesGetHandler) { + guard let config = Optimove.getConfig()?.getPreferenceCenterConfig() else { + Logger.error("Preference center credentials are not set") + completion(.errorCredentialsNotSet, nil) + return + } + + guard + let customerId = try? storage?.getCustomerID(), + let visitorId = try? storage?.getVisitorID(), + customerId != visitorId + else { + Logger.warn("Customer ID is not set") + completion(.errorUserNotSet, nil) + return + } + + Task { + do { + let request = try createGetPreferencesRequest(for: customerId, with: config) + + networkClient?.perform(request) { [self] result in + switch result { + case .success(let response): + do { + let preferences = try response.decode(to: OptimovePC.Preferences.self) + DispatchQueue.main.async { + completion(.success, preferences) + } + } catch { + logFailedResponse(error) + DispatchQueue.main.async { + completion(.error, nil) + } + } + case .failure(let error): + logFailedResponse(error) + DispatchQueue.main.async { + completion(.error, nil) + } + } + } + + } catch { + logFailedResponse(error) + DispatchQueue.main.async { + completion(.error, nil) + } + } + } + } + private func createGetPreferencesRequest( + for customerId: String, + with config: PreferenceCenterConfig) throws -> NetworkRequest { + let (region, brandGroupId, tenantId) = getConfigValues(from: config) + + return NetworkRequest( + method: .get, + baseURL: URL(string: "https://preference-center-\(region).optimove.net")!, + path: "/api/v1/preferences", + headers: [ + HTTPHeader(field: .accept, value: .textplain), + HTTPHeader(field: .tenantId, value: .tenantId(id: tenantId)) + ], + queryItems: [ + URLQueryItem(name: "customerId", value: customerId), + URLQueryItem(name: "brandGroupId", value: brandGroupId) + ] + ) + } + + private func getConfigValues(from config: PreferenceCenterConfig) -> (region: String, brandGroupId: String, tenantId: String) { + let region = config.region + let brandGroupId = config.brandGroupId + let tenantId = config.tenantId.description + return (region, brandGroupId, tenantId) + } + + + @available(iOS 13.0, *) + public func setCustomerPreferencesAsync(completion: @escaping PreferencesSetHandler, updates: [OptimovePC.PreferenceUpdate]) { + guard let config = Optimove.getConfig()?.getPreferenceCenterConfig() else { + Logger.error("Preference center credentials are not set") + completion(.errorCredentialsNotSet) + return + } + + guard + let customerId = try? storage?.getCustomerID(), + let visitorId = try? storage?.getVisitorID(), + customerId != visitorId else { + Logger.warn("Customer ID is not set") + completion(.errorUserNotSet) + return + } + + Task { + do { + let request = try createSetPreferencesRequest(for: customerId, with: config, updates: updates) + + networkClient?.perform(request) { [self] result in + switch result { + case .success(let response): + do { + _ = try response.unwrap() + DispatchQueue.main.async { + completion(.success) + } + } catch { + logFailedResponse(error) + DispatchQueue.main.async { + completion(.error) + } + } + case .failure(let error): + logFailedResponse(error) + DispatchQueue.main.async { + completion(.error) + } + } + } + + } catch { + logFailedResponse(error) + DispatchQueue.main.async { + completion(.error) + } + } + } + } + + private func createSetPreferencesRequest( + for customerId: String, + with config: PreferenceCenterConfig, + updates: [OptimovePC.PreferenceUpdate]) throws -> NetworkRequest { + let (region, brandGroupId, tenantId) = getConfigValues(from: config) + + return try NetworkRequest( + method: .put, + baseURL: URL(string: "https://preference-center-\(region).optimove.net")!, + path: "/api/v1/preferences", + headers: [ + HTTPHeader(field: .accept, value: .textplain), + HTTPHeader(field: .tenantId, value: .tenantId(id: tenantId)) + ], + queryItems: [ + URLQueryItem(name: "customerId", value: customerId), + URLQueryItem(name: "brandGroupId", value: brandGroupId) + ], + body: updates + ) + } + + private func logFailedResponse(_ error: Swift.Error) { + Logger.error("Request failed with error: \(error.localizedDescription)") + } + + private func logFailedResponse(_ response: URLResponse) { + if let httpResponse = response as? HTTPURLResponse { + let code = httpResponse.statusCode; + let msg = "Request failed with code \(code): \(HTTPURLResponse.localizedString(forStatusCode: code))." + + switch httpResponse.statusCode { + case 400: + Logger.error("\(msg) Check preference center configuration"); + default: + Logger.error(msg) + } + } + } +} diff --git a/OptimoveSDK/Sources/Classes/Services/ServiceLocator.swift b/OptimoveSDK/Sources/Classes/Services/ServiceLocator.swift index fb893649..74cdb1f6 100644 --- a/OptimoveSDK/Sources/Classes/Services/ServiceLocator.swift +++ b/OptimoveSDK/Sources/Classes/Services/ServiceLocator.swift @@ -106,7 +106,6 @@ final class ServiceLocator { func deviceStateObserver() -> DeviceStateObserver { return _deviceStateObserver } - // MARK: - Factories func componentFactory() -> ComponentFactory { 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/OptimoveStorage.swift b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift index 72479c1b..30ef098f 100644 --- a/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift +++ b/OptimoveSDK/Sources/Classes/Storage/OptimoveStorage.swift @@ -178,15 +178,6 @@ extension KeyValueStorage where Self: StorageValue { } } - var userAgent: String? { - get { - return self[.userAgent] - } - set { - self[.userAgent] = newValue - } - } - var deviceResolutionWidth: Float? { get { return self[.deviceResolutionWidth] @@ -348,13 +339,6 @@ extension KeyValueStorage where Self: StorageValue { 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) diff --git a/OptimoveSDK/Tests/Sources/Events/CoreEventFactoryTests.swift b/OptimoveSDK/Tests/Sources/Events/CoreEventFactoryTests.swift index c90a95bc..575b0203 100644 --- a/OptimoveSDK/Tests/Sources/Events/CoreEventFactoryTests.swift +++ b/OptimoveSDK/Tests/Sources/Events/CoreEventFactoryTests.swift @@ -49,15 +49,6 @@ class CoreEventFactoryTests: OptimoveTestCase { wait(for: [expectation], timeout: defaultTimeout) } - func test_create_setUserAgent() throws { - prefillStorageAsVisitor() - let expectation = XCTestExpectation(description: "Event creation failed for \(#function)") - let event = try factory.createEvent(.setUserAgent) - XCTAssert(event.name == SetUserAgent.Constants.name) - expectation.fulfill() - wait(for: [expectation], timeout: defaultTimeout) - } - func test_create_pageVisit() throws { let expectation = XCTestExpectation(description: "Event creation failed for \(#function)") let event = try factory.createEvent(.pageVisit(title: "", category: "")) diff --git a/OptimoveSDK/Tests/Sources/Events/CoreEvents/SetUserAgentEventTests.swift b/OptimoveSDK/Tests/Sources/Events/CoreEvents/SetUserAgentEventTests.swift deleted file mode 100644 index 1a0158dc..00000000 --- a/OptimoveSDK/Tests/Sources/Events/CoreEvents/SetUserAgentEventTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright © 2019 Optimove. All rights reserved. - -@testable import OptimoveSDK -import XCTest - -class SetUserAgentEventTests: XCTestCase { - func test_event_name() { - // given - let userAgent = "" - - // when - let event = SetUserAgent(userAgent: userAgent) - - // then - XCTAssert(event.name == SetUserAgent.Constants.name) - } - - func test_event_long_useragent() { - // given - let userAgentLenght = 600 - let userAgent = Array(repeating: "a", count: userAgentLenght).joined() - let expectedParameterCount = (Float(userAgentLenght) / Float(SetUserAgent.Constants.userAgentSliceLenght)).rounded(.up) - - // when - let event = SetUserAgent(userAgent: userAgent) - - // then - XCTAssert(event.context.count == Int(expectedParameterCount)) - } - - func test_event_short_useragent() { - // given - let userAgentLenght = 100 - let userAgent = Array(repeating: "a", count: userAgentLenght).joined() - let expectedParameterCount = (Float(userAgentLenght) / Float(SetUserAgent.Constants.userAgentSliceLenght)).rounded(.up) - - // when - let event = SetUserAgent(userAgent: userAgent) - - // then - XCTAssert(event.context.count == Int(expectedParameterCount)) - } - - func test_event_useragent_key_started_with_1() { - // given - let userAgentLenght = 100 - let userAgent = Array(repeating: "a", count: userAgentLenght).joined() - - // when - let event = SetUserAgent(userAgent: userAgent) - - // then - let first = event.context.first - XCTAssert(first?.key == SetUserAgent.Constants.userAgentHeaderBase + String(1)) - } -} diff --git a/OptimoveSDK/Tests/Sources/Generators/OnStartEventGeneratorTests.swift b/OptimoveSDK/Tests/Sources/Generators/OnStartEventGeneratorTests.swift index a2dc6683..07c93730 100644 --- a/OptimoveSDK/Tests/Sources/Generators/OnStartEventGeneratorTests.swift +++ b/OptimoveSDK/Tests/Sources/Generators/OnStartEventGeneratorTests.swift @@ -32,7 +32,6 @@ final class OnStartEventGeneratorTests: OptimoveTestCase { // then let metaDataEventExpectation = expectation(description: "MetaDataEvent was not generated.") - let userAgentEventExpectation = expectation(description: "SetUserAgent was not generated.") let appOpenEventExpectation = expectation(description: "AppOpenEvent was not generated.") synchronizer.assertFunction = { operation in switch operation { @@ -41,8 +40,6 @@ final class OnStartEventGeneratorTests: OptimoveTestCase { switch event.name { case MetaDataEvent.Constants.name: metaDataEventExpectation.fulfill() - case SetUserAgent.Constants.name: - userAgentEventExpectation.fulfill() case AppOpenEvent.Constants.name: appOpenEventExpectation.fulfill() default: @@ -60,7 +57,6 @@ final class OnStartEventGeneratorTests: OptimoveTestCase { wait( for: [ metaDataEventExpectation, - userAgentEventExpectation, appOpenEventExpectation, ], // Additional second to complete the async operation and prevent a flickering. diff --git a/README.md b/README.md index b1ffa1a0..406b1949 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ In this guide we will go through the following steps necessary to have the basic 1. [Tracking custom events](https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/Tracking-custom-events) 2. [Location and Beacons](https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/Location-and-Beacons) +3. [Preference Center](https://github.com/optimove-tech/Optimove-SDK-iOS/wiki/Preference-Center) ## Mobile Messaging diff --git a/Shared/Sources/OptimoveTestCase.swift b/Shared/Sources/OptimoveTestCase.swift index b4385b48..36c93f96 100644 --- a/Shared/Sources/OptimoveTestCase.swift +++ b/Shared/Sources/OptimoveTestCase.swift @@ -28,7 +28,6 @@ open class OptimoveTestCase: XCTestCase { public func prefillStorageWithTheFirstLaunch() { prefillStorageWithConfiguration() storage.installationID = UUID().uuidString - storage.userAgent = "user-agent" storage.firstRunTimestamp = Date().timeIntervalSince1970.seconds }