From d8fd61475c00b0e685769985625972e48f6cbae9 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Wed, 4 Aug 2021 16:13:09 +0100 Subject: [PATCH 01/10] Mark methods that use UIApplication as unavailable for non-app extensions (#13) This has been merged into xcode13-beta branch, which will be maintained up to date with new functionality until the official release of Xcode --- Sources/Ometria/ApplicationUtils.swift | 3 ++- .../Ometria/AutomaticLifecycleTracker.swift | 3 ++- Sources/Ometria/AutomaticPushTracker.swift | 24 ++++++++++++------- Sources/Ometria/Ometria.swift | 6 ++++- 4 files changed, 25 insertions(+), 11 deletions(-) diff --git a/Sources/Ometria/ApplicationUtils.swift b/Sources/Ometria/ApplicationUtils.swift index 908503f..01b219c 100644 --- a/Sources/Ometria/ApplicationUtils.swift +++ b/Sources/Ometria/ApplicationUtils.swift @@ -21,7 +21,8 @@ extension Ometria { } return sharedApplication } - + + @available(iOSApplicationExtension, unavailable) static func doesAppUseScenes() -> Bool { let delegateClass: AnyClass! = object_getClass(UIApplication.shared.delegate) diff --git a/Sources/Ometria/AutomaticLifecycleTracker.swift b/Sources/Ometria/AutomaticLifecycleTracker.swift index 5fc87e9..9bb4b8a 100644 --- a/Sources/Ometria/AutomaticLifecycleTracker.swift +++ b/Sources/Ometria/AutomaticLifecycleTracker.swift @@ -11,7 +11,8 @@ import UIKit open class AutomaticLifecycleTracker { var isRunning = false - + + @available(iOSApplicationExtension, unavailable) func startTracking() { guard !isRunning else { return diff --git a/Sources/Ometria/AutomaticPushTracker.swift b/Sources/Ometria/AutomaticPushTracker.swift index 3b8cec4..f426772 100644 --- a/Sources/Ometria/AutomaticPushTracker.swift +++ b/Sources/Ometria/AutomaticPushTracker.swift @@ -25,7 +25,8 @@ open class AutomaticPushTracker: NSObject { open var isRunning = false open var isDelegateObserverAdded = false - + + @available(iOSApplicationExtension, unavailable) open func startTracking() { guard !isRunning else { return @@ -39,7 +40,8 @@ open class AutomaticPushTracker: NSObject { NotificationCenter.default.addObserver(self, selector: #selector(firebaseTokenDidRefresh(notification:)), name: Notification.Name.MessagingRegistrationTokenRefreshed, object: nil) } - + + @available(iOSApplicationExtension, unavailable) open func stopTracking() { guard isRunning else { return @@ -59,7 +61,8 @@ open class AutomaticPushTracker: NSObject { UNUserNotificationCenter.current().removeDelegateObserver(observer: self) } } - + + @available(iOSApplicationExtension, unavailable) private func swizzleDidRegisterForRemoteNotificationsWithDeviceToken() { Logger.verbose(message: "Swizzle did register for remote notifications") let newSelector = #selector(UIResponder.om_application(_:didRegisterForRemoteNotificationsWithDeviceToken:)) @@ -73,13 +76,15 @@ open class AutomaticPushTracker: NSObject { Logger.verbose(message: "Application did register for remote notifications") } } - + + @available(iOSApplicationExtension, unavailable) private func unswizzleDidRegisterForRemoteNotificationsWithDeviceToken() { let delegateClass: AnyClass! = object_getClass(UIApplication.shared.delegate) let originalSelector = #selector(UIApplicationDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)) Swizzler.unswizzleSelector(originalSelector, aClass: delegateClass) } - + + @available(iOSApplicationExtension, unavailable) private func swizzleDidFailToRegisterForRemoteNotificationsWithError() { Logger.verbose(message: "Swizzle did fail to register for remote notifications") let newSelector = #selector(UIResponder.om_application(_:didFailToRegisterForRemoteNotificationsWithError:)) @@ -93,14 +98,16 @@ open class AutomaticPushTracker: NSObject { Logger.verbose(message: "Application did fail to register for remote notifications") } } - + + @available(iOSApplicationExtension, unavailable) private func unswizzleDidFailToRegisterForRemoteNotificationsWithError() { let delegateClass: AnyClass! = object_getClass(UIApplication.shared.delegate) let originalSelector = #selector(UIApplicationDelegate.application(_:didFailToRegisterForRemoteNotificationsWithError:)) Swizzler.unswizzleSelector(originalSelector, aClass: delegateClass) } - + + @available(iOSApplicationExtension, unavailable) private func swizzleDidReceiveSilentNotification() { Logger.verbose(message: "Swizzle application:didReceiveRemoteNotification:fetchCompletionHandler:") let newSelector = #selector(UIResponder.om_application(_:didReceiveRemoteNotification:fetchCompletionHandler:)) @@ -114,7 +121,8 @@ open class AutomaticPushTracker: NSObject { Logger.debug(message: "Application didReceiveSilentNotification") } } - + + @available(iOSApplicationExtension, unavailable) private func unswizzleDidReceiveSilentNotification() { let delegateClass: AnyClass! = object_getClass(UIApplication.shared.delegate) let originalSelector = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:)) diff --git a/Sources/Ometria/Ometria.swift b/Sources/Ometria/Ometria.swift index 52afec3..f5ea5a4 100644 --- a/Sources/Ometria/Ometria.swift +++ b/Sources/Ometria/Ometria.swift @@ -44,6 +44,7 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { - Returns: returns an initialized Ometria instance object if needed to use or keep throughout the project. You can always get the initialized instance by calling sharedInstance() */ @discardableResult + @available(iOSApplicationExtension, unavailable) open class func initialize(apiToken: String) -> Ometria { let ometria = Ometria(apiToken: apiToken, config: OmetriaConfig()) instance = ometria @@ -62,7 +63,8 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { } return instance! } - + + @available(iOSApplicationExtension, unavailable) init(apiToken: String, config: OmetriaConfig) { self.config = config self.apiToken = apiToken @@ -481,7 +483,9 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { } // MARK: - Deeplink Interaction +@available(iOSApplicationExtension, unavailable) extension Ometria: OmetriaNotificationInteractionDelegate { + public func handleDeepLinkInteraction(_ deepLink: URL) { Logger.debug(message: "Open URL: \(deepLink)", category: .push) if Ometria.sharedUIApplication()?.canOpenURL(deepLink) == true { From 37cfae930cd7eda4db4435b8fb9edf20d0cdbacd Mon Sep 17 00:00:00 2001 From: Sergiu Corbu Date: Wed, 29 Sep 2021 15:46:29 +0300 Subject: [PATCH 02/10] updated --- Ometria Sample/Podfile.lock | 91 ++++++++++++++++++++++ Sources/Ometria/Decodable.swift | 17 ++++ Sources/Ometria/OmetriaNotification.swift | 95 +++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 Ometria Sample/Podfile.lock create mode 100644 Sources/Ometria/Decodable.swift create mode 100644 Sources/Ometria/OmetriaNotification.swift diff --git a/Ometria Sample/Podfile.lock b/Ometria Sample/Podfile.lock new file mode 100644 index 0000000..579291f --- /dev/null +++ b/Ometria Sample/Podfile.lock @@ -0,0 +1,91 @@ +PODS: + - FirebaseCore (8.7.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.4) + - GoogleUtilities/Logger (~> 7.4) + - FirebaseCoreDiagnostics (8.7.0): + - GoogleDataTransport (~> 9.0) + - GoogleUtilities/Environment (~> 7.4) + - GoogleUtilities/Logger (~> 7.4) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.7.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.4) + - GoogleUtilities/UserDefaults (~> 7.4) + - PromisesObjC (< 3.0, >= 1.2) + - FirebaseMessaging (8.7.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleDataTransport (~> 9.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.4) + - GoogleUtilities/Environment (~> 7.4) + - GoogleUtilities/Reachability (~> 7.4) + - GoogleUtilities/UserDefaults (~> 7.4) + - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.1.0): + - GoogleUtilities/Environment (~> 7.2) + - nanopb (~> 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.5.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.5.2): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.5.2): + - GoogleUtilities/Environment + - GoogleUtilities/Network (7.5.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.5.2)" + - GoogleUtilities/Reachability (7.5.2): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.5.2): + - GoogleUtilities/Logger + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - Ometria (1.2.0): + - FirebaseMessaging + - PromisesObjC (2.0.0) + +DEPENDENCIES: + - Ometria (from `https://github.com/Ometria/ometria.ios_sdk.git`) + +SPEC REPOS: + trunk: + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseInstallations + - FirebaseMessaging + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + Ometria: + :git: https://github.com/Ometria/ometria.ios_sdk.git + +CHECKOUT OPTIONS: + Ometria: + :commit: eb89038304cd8e01461fcd10aeea51086d431186 + :git: https://github.com/Ometria/ometria.ios_sdk.git + +SPEC CHECKSUMS: + FirebaseCore: f4804c1d3f4bbbefc88904d15653038f2c99ddf7 + FirebaseCoreDiagnostics: b63732f581a1c6a453ec7241f9ab60b3a5bd3450 + FirebaseInstallations: ede6fb72bb6337914e5888b399271259d0c4910c + FirebaseMessaging: 93227dd71d7888e200baef65043f81acb2b6596e + GoogleDataTransport: 85fd18ff3019bb85d3f2c551d04c481dedf71fc9 + GoogleUtilities: 8de2a97a17e15b6b98e38e8770e2d129a57c0040 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + Ometria: 6477d0bcaefbdba4effbdaff0e76707dd472ec5b + PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 + +PODFILE CHECKSUM: 67fb874528c489a0ad0238479b9dc2a899a739c9 + +COCOAPODS: 1.10.1 diff --git a/Sources/Ometria/Decodable.swift b/Sources/Ometria/Decodable.swift new file mode 100644 index 0000000..eb3484a --- /dev/null +++ b/Sources/Ometria/Decodable.swift @@ -0,0 +1,17 @@ +// +// Decodable.swift +// Ometria +// +// Created by Sergiu Corbu on 29.09.2021. +// Copyright © 2021 Cata. All rights reserved. +// + +import Foundation + +extension Decodable { + + init(from: [String: Any]) throws { + let data = try JSONSerialization.data(withJSONObject: from, options: .prettyPrinted) + self = try JSONDecoder().decode(Self.self, from: data) + } +} diff --git a/Sources/Ometria/OmetriaNotification.swift b/Sources/Ometria/OmetriaNotification.swift new file mode 100644 index 0000000..58189c5 --- /dev/null +++ b/Sources/Ometria/OmetriaNotification.swift @@ -0,0 +1,95 @@ +// +// OmetriaNotification.swift +// Ometria +// +// Created by Sergiu Corbu on 29.09.2021. +// Copyright © 2021 Cata. All rights reserved. +// + +import Foundation + +/** + An object representing a notification. + + - Parameter deepLink: The URL that was sent in the notification. We append tracking parameters to the URL specified in the account and campaign settings (these can be changed in the Ometria app) + - Parameter imageUrl: The image URL that was sent in the notification. + - Parameter campaign_type: Can be trigger, mass, transactional (currently only trigger is used). + - Parameter externalCustomerId: The id of the contact that was specified at customer creation (in the mobile app) or ingesting to Ometria. + - Parameter sendId: Unique id of the message + - Parameter tracking: An object that contains all tracking fields specified in the account and campaign settings (can be changed in the Ometria app, uses some defaults if not specified). It has the same values that we add to the deeplink url. + */ + +open class OmetriaNotification: Decodable { + + open var deepLinkActionUrl: String + open var imageUrl: String + open var context: Context + + enum CodingKeys: String, CodingKey { + case deepLinkActionUrl + case imageUrl + case context + } + + required public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + deepLinkActionUrl = try values.decode(String.self, forKey: .deepLinkActionUrl) + imageUrl = try values.decode(String.self, forKey: .imageUrl) + context = try values.decode(Context.self, forKey: .context) + } + + + public struct Context: Decodable { + + var campaign_type: String + var ext_customer_id: String + var app_install_id: String + var campaign_hash: String + var campaing_id: String + var mobile_app_id: String + var om_customer_id: String + var account_id: String + var campaign_version: String + var node_id: String + var send_id: String + var tracking: [String:Any] + + enum CodingKeys: String, CodingKey, CaseIterable { + case campaign_type + case ext_customer_id + case tracking + case app_install_id + case campaign_hash + case campaing_id + case mobile_app_id + case om_customer_id + case account_id + case campaign_version + case node_id + case send_id + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + campaign_type = try values.decode(String.self, forKey: .campaign_type) + ext_customer_id = try values.decode(String.self, forKey: .ext_customer_id) + app_install_id = try values.decode(String.self, forKey: .app_install_id) + campaign_hash = try values.decode(String.self, forKey: .campaign_hash) + campaing_id = try values.decode(String.self, forKey: .campaing_id) + campaign_version = try values.decode(String.self, forKey: .campaign_version) + mobile_app_id = try values.decode(String.self, forKey: .mobile_app_id) + om_customer_id = try values.decode(String.self, forKey: .om_customer_id) + node_id = try values.decode(String.self, forKey: .node_id) + send_id = try values.decode(String.self, forKey: .send_id) + account_id = try values.decode(String.self, forKey: .account_id) + + guard values.contains(.tracking), + let jsonData = try? values.decode(Data.self, forKey: .tracking) else { + tracking = [:] + return + } + tracking = (try? JSONSerialization.jsonObject(with: jsonData) as? [String : Any]) ?? [String:Any]() + } + } + +} From d980651c62fe84e098ce212b80070133dcf7c4c7 Mon Sep 17 00:00:00 2001 From: Sergiu Corbu Date: Thu, 30 Sep 2021 18:25:06 +0300 Subject: [PATCH 03/10] updated with documentation --- .../OmetriaSample/AppDelegate.swift | 4 + Ometria.xcodeproj/project.pbxproj | 12 ++- Sources/Ometria/NotificationHandler.swift | 44 ++++++++- Sources/Ometria/Ometria.swift | 71 +++++++++------ Sources/Ometria/OmetriaNotification.swift | 91 ++++++------------- 5 files changed, 126 insertions(+), 96 deletions(-) diff --git a/Ometria Sample/OmetriaSample/AppDelegate.swift b/Ometria Sample/OmetriaSample/AppDelegate.swift index bc98c21..961dd61 100644 --- a/Ometria Sample/OmetriaSample/AppDelegate.swift +++ b/Ometria Sample/OmetriaSample/AppDelegate.swift @@ -101,6 +101,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } } + func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { + + } + // MARK: - Universal Link Handling diff --git a/Ometria.xcodeproj/project.pbxproj b/Ometria.xcodeproj/project.pbxproj index 125cb1b..d224368 100644 --- a/Ometria.xcodeproj/project.pbxproj +++ b/Ometria.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 4E8961FA250FB1C200EEC9F6 /* Ometria.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 4E8961F9250FB1C200EEC9F6 /* Ometria.podspec */; }; 4E96478524E3F90B00274FC3 /* OmetriaBasketItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E96478324E3F90B00274FC3 /* OmetriaBasketItem.swift */; }; 4E96478624E3F90B00274FC3 /* OmetriaBasket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E96478424E3F90B00274FC3 /* OmetriaBasket.swift */; }; - 4E96AB4824D30ADB00623FA6 /* AutomaticScreenViewsTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E96AB4724D30ADB00623FA6 /* AutomaticScreenViewsTracker.swift */; }; 4E96AB4F24D4927900623FA6 /* NotificationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E96AB4E24D4927900623FA6 /* NotificationHandler.swift */; }; 4EA10CE524ED5A97007FF49D /* JSONDecoder+KeyPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA10CE424ED5A97007FF49D /* JSONDecoder+KeyPath.swift */; }; 4EA10CE724ED70AB007FF49D /* ParamEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EA10CE624ED70AB007FF49D /* ParamEncoder.swift */; }; @@ -39,6 +38,8 @@ 4EFF18B624EA55690020389A /* JSONCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFF18B524EA55690020389A /* JSONCache.swift */; }; 4EFF18B824EA559A0020389A /* CodableCaching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFF18B724EA559A0020389A /* CodableCaching.swift */; }; 4EFF18BB24EA63230020389A /* EventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EFF18BA24EA63230020389A /* EventHandler.swift */; }; + EC56B60127048E17009D808A /* Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC56B60027048E17009D808A /* Decodable.swift */; }; + EC56B60327048E23009D808A /* OmetriaNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC56B60227048E23009D808A /* OmetriaNotification.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -51,7 +52,6 @@ 4E8961F9250FB1C200EEC9F6 /* Ometria.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Ometria.podspec; sourceTree = ""; }; 4E96478324E3F90B00274FC3 /* OmetriaBasketItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OmetriaBasketItem.swift; sourceTree = ""; }; 4E96478424E3F90B00274FC3 /* OmetriaBasket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OmetriaBasket.swift; sourceTree = ""; }; - 4E96AB4724D30ADB00623FA6 /* AutomaticScreenViewsTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutomaticScreenViewsTracker.swift; sourceTree = ""; }; 4E96AB4E24D4927900623FA6 /* NotificationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationHandler.swift; sourceTree = ""; }; 4EA10CE424ED5A97007FF49D /* JSONDecoder+KeyPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+KeyPath.swift"; sourceTree = ""; }; 4EA10CE624ED70AB007FF49D /* ParamEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParamEncoder.swift; sourceTree = ""; }; @@ -77,6 +77,8 @@ 4EFF18B724EA559A0020389A /* CodableCaching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableCaching.swift; sourceTree = ""; }; 4EFF18BA24EA63230020389A /* EventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHandler.swift; sourceTree = ""; }; 7F5D7686F934DEAE653B4617 /* Pods_Ometria.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Ometria.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EC56B60027048E17009D808A /* Decodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decodable.swift; sourceTree = ""; }; + EC56B60227048E23009D808A /* OmetriaNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmetriaNotification.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -96,6 +98,7 @@ children = ( 4EA10CE424ED5A97007FF49D /* JSONDecoder+KeyPath.swift */, 4E47423724EE6590005F1F68 /* JSONEncoder+Ometria.swift */, + EC56B60027048E17009D808A /* Decodable.swift */, 4EE7A8C924C81BDD00B316A7 /* ApplicationUtils.swift */, 4EF3710924F39C4B00450B52 /* Array+Chunks.swift */, 4EF3710C24F56D0E00450B52 /* Constants.swift */, @@ -155,6 +158,7 @@ children = ( 4E96478424E3F90B00274FC3 /* OmetriaBasket.swift */, 4E96478324E3F90B00274FC3 /* OmetriaBasketItem.swift */, + EC56B60227048E23009D808A /* OmetriaNotification.swift */, 4EE7A8D224C88F0500B316A7 /* OmetriaEvent.swift */, 4EF3711124F7912C00450B52 /* OmetriaNotificationBody.swift */, ); @@ -184,7 +188,6 @@ 4E567CC9250F7B4900D68DEB /* Screen Views */ = { isa = PBXGroup; children = ( - 4E96AB4724D30ADB00623FA6 /* AutomaticScreenViewsTracker.swift */, ); name = "Screen Views"; sourceTree = ""; @@ -354,7 +357,9 @@ 4EE7A8D024C8483600B316A7 /* ReadWriteLock.swift in Sources */, 4E88929424ED9A07002593FD /* EventsAPI.swift in Sources */, 4E96AB4F24D4927900623FA6 /* NotificationHandler.swift in Sources */, + EC56B60127048E17009D808A /* Decodable.swift in Sources */, 4E0DB1ED24CABE9200EA2859 /* OmetriaDefaults.swift in Sources */, + EC56B60327048E23009D808A /* OmetriaNotification.swift in Sources */, 4EE7A8D324C88F0500B316A7 /* OmetriaEvent.swift in Sources */, 4E5B2E2824ED4B470063FA50 /* NetworkService.swift in Sources */, 4EC94EDE24B8BCFB00B8591C /* OmetriaConfig.swift in Sources */, @@ -369,7 +374,6 @@ 4E5B2E2A24ED4C320063FA50 /* Result.swift in Sources */, 4EF3710A24F39C4B00450B52 /* Array+Chunks.swift in Sources */, 4E96478524E3F90B00274FC3 /* OmetriaBasketItem.swift in Sources */, - 4E96AB4824D30ADB00623FA6 /* AutomaticScreenViewsTracker.swift in Sources */, 4EE7A8CA24C81BDD00B316A7 /* ApplicationUtils.swift in Sources */, 4EBDBF8D24C1906600AB8DCE /* Swizzler.swift in Sources */, 4EFF18BB24EA63230020389A /* EventHandler.swift in Sources */, diff --git a/Sources/Ometria/NotificationHandler.swift b/Sources/Ometria/NotificationHandler.swift index 1036b77..72b0b20 100644 --- a/Sources/Ometria/NotificationHandler.swift +++ b/Sources/Ometria/NotificationHandler.swift @@ -18,7 +18,18 @@ public protocol OmetriaNotificationInteractionDelegate: AnyObject { - Parameter deepLink: the processed url string that was received in the interacted notification payload */ + + @available(*, deprecated, message: "Deprecated in version 1.2.1. Use getOmetriaNotification instead") func handleDeepLinkInteraction(_ deepLink: URL) + + func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) +} + +extension OmetriaNotificationInteractionDelegate { + + func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { + + } } class NotificationHandler { @@ -43,6 +54,10 @@ class NotificationHandler { } } } + + if let ometriaNotification = parseOmetriaNotification(response.notification.request.content) { + interactionDelegate?.handleOmetriaNotificationInteraction(ometriaNotification) + } } func processDeliveredNotifications() { @@ -58,14 +73,33 @@ class NotificationHandler { } } + func parseOmetriaNotification(_ content: UNNotificationContent) -> OmetriaNotification? { + guard let aps = content.userInfo["aps"] as? [String: Any], + let alert = aps["alert"] as? [String: Any], + let ometriaContent = alert["ometria"] as? [String: Any] else { + return nil + } + do { + let ometriaNotification = try OmetriaNotification(from: ometriaContent) + return ometriaNotification + } catch let error as OmetriaError { + Logger.error(message: error.localizedDescription) + Ometria.sharedInstance().trackErrorOccuredEvent(error: error) + return nil + } catch { + Logger.error(message: error.localizedDescription) + return nil + } + } + func parseNotificationContent(_ content: UNNotificationContent) -> OmetriaNotificationBody? { let info = content.userInfo guard let aps = info["aps"] as? [String: Any], - let alert = aps["alert"] as? [String: Any], - let ometriaContent = alert["ometria"] as? [String: Any] else { - return nil - } + let alert = aps["alert"] as? [String: Any], + let ometriaContent = alert["ometria"] as? [String: Any] else { + return nil + } do { let notificationBody = try OmetriaNotificationBody(dictionary: ometriaContent) @@ -91,7 +125,7 @@ class NotificationHandler { switch settings.authorizationStatus { case .authorized, .provisional: if lastKnownStatus != .authorized, - #available(iOS 12.0, *), lastKnownStatus != .provisional { + #available(iOS 12.0, *), lastKnownStatus != .provisional { Logger.verbose(message: "Notification authorization status changed to 'authorized'.", category: .push) Ometria.sharedInstance().trackPermissionsUpdateEvent(hasPermissions: true) } diff --git a/Sources/Ometria/Ometria.swift b/Sources/Ometria/Ometria.swift index d8d9aa2..467e662 100644 --- a/Sources/Ometria/Ometria.swift +++ b/Sources/Ometria/Ometria.swift @@ -51,10 +51,10 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { } /** - Gets the previously initialized Ometria instance - - - Returns: returns the Ometria instance - */ + Gets the previously initialized Ometria instance + + - Returns: returns the Ometria instance + */ open class func sharedInstance() -> Ometria { guard instance != nil else { fatalError("You are not allowed to call the sharedInstance() method before calling initialize(apiToken:).") @@ -72,7 +72,7 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { // didSet not called from initializer. setLoggingEnabled is force called to remedy that. setLoggerEnabled(isLoggingEnabled) - + if config.automaticallyTrackNotifications { automaticPushTracker.startTracking() } @@ -80,15 +80,15 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { if config.automaticallyTrackAppLifecycle { automaticLifecycleTracker.startTracking() } - + self.notificationHandler.interactionDelegate = self } /** - This allows enabling or disabling runtime logs - - Note: All logging is disabled by default. This is only required - when you encounter issues with the SDK and you want to debug it. - */ + This allows enabling or disabling runtime logs + - Note: All logging is disabled by default. This is only required + when you encounter issues with the SDK and you want to debug it. + */ open var isLoggingEnabled: Bool = false { didSet { setLoggerEnabled(isLoggingEnabled) @@ -101,11 +101,11 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { Logger.enableLevel(.info) Logger.enableLevel(.warning) Logger.enableLevel(.error) - + Logger.debug(message: "Logger Enabled") } else { Logger.debug(message: "Logger Disabled") - + Logger.disableLevel(.debug) Logger.disableLevel(.info) Logger.disableLevel(.warning) @@ -179,7 +179,7 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { - Parameter screenName: the name of the screen - Parameter additionalInfo: a dictionary containing any key-value pairs that provide valuable information to your platform - */ + */ open func trackScreenViewedEvent(screenName: String, additionalInfo:[String: Any] = [:]) { let data: [String: Any] = ["page": screenName, "extra": additionalInfo] @@ -278,10 +278,10 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { } /** - Track when a user has removed a product to their wishlist - - - Parameter productId: the unique identifier of the product that has been removed from the wishlist - */ + Track when a user has removed a product to their wishlist + + - Parameter productId: the unique identifier of the product that has been removed from the wishlist + */ open func trackWishlistRemovedFromEvent(productId: String) { trackEvent(type: .wishlistRemovedFrom, data: ["productId": productId]) } @@ -409,7 +409,7 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { /** Uploads tracked events data to the Ometria server. - + By default, tracked events are flushed to the Ometria servers every time it reaches a limit of 10 events, but no earlier than 10 seconds from the last flush operation. You only need to call this method manually if you want to force a flush at a particular moment. */ @@ -435,8 +435,8 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { } open func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { notificationHandler.handleNotificationResponse(response, withCompletionHandler: completionHandler) @@ -465,15 +465,29 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { */ public static func isOmetriaNotification(_ content: UNNotificationContent) -> Bool { guard let aps = content.userInfo["aps"] as? [String: Any], - let alert = aps["alert"] as? [String: Any], - let _ = alert["ometria"] as? [String: Any] else { - - return false - } + let alert = aps["alert"] as? [String: Any], + let _ = alert["ometria"] as? [String: Any] else { + + return false + } return true } + /** + Validates if a notification comes from ometria by checking its content and retrieves an OmetriaNotification object + + - Parameter content: The content taken from a UNUnotification that has been received + + - Returns: An optional OmetriaNotification object + */ + public func parseNotification(_ content: UNNotificationContent) -> OmetriaNotification? { + guard Ometria.isOmetriaNotification(content) else { + return nil + } + + return notificationHandler.parseOmetriaNotification(content) + } // MARK: Universal Links @@ -486,12 +500,17 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { - Note: If no redirect url is found, the initial url will be provided in the callback */ open func processUniversalLink(_ url: URL, callback: @escaping (URL?, Error?)->()) { - RedirectService().getRedirect(url: url, callback: callback) + RedirectService().getRedirect(url: url, callback: callback) } } // MARK: - Deeplink Interaction extension Ometria: OmetriaNotificationInteractionDelegate { + + public func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { + + } + public func handleDeepLinkInteraction(_ deepLink: URL) { Logger.debug(message: "Open URL: \(deepLink)", category: .push) if Ometria.sharedUIApplication()?.canOpenURL(deepLink) == true { diff --git a/Sources/Ometria/OmetriaNotification.swift b/Sources/Ometria/OmetriaNotification.swift index 58189c5..4f1aafa 100644 --- a/Sources/Ometria/OmetriaNotification.swift +++ b/Sources/Ometria/OmetriaNotification.swift @@ -19,77 +19,46 @@ import Foundation - Parameter tracking: An object that contains all tracking fields specified in the account and campaign settings (can be changed in the Ometria app, uses some defaults if not specified). It has the same values that we add to the deeplink url. */ -open class OmetriaNotification: Decodable { +public struct OmetriaNotification: Decodable { - open var deepLinkActionUrl: String - open var imageUrl: String - open var context: Context + public var deepLink: String? + public var imageUrl: String? + public var externalCustomerId: String + public var context: OmetriaNotificationContext enum CodingKeys: String, CodingKey { - case deepLinkActionUrl + case externalCustomerId = "ext_customer_id" + case deepLink = "deepLinkActionUrl" case imageUrl case context } +} + + +public struct OmetriaNotificationContext: Decodable { - required public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - deepLinkActionUrl = try values.decode(String.self, forKey: .deepLinkActionUrl) - imageUrl = try values.decode(String.self, forKey: .imageUrl) - context = try values.decode(Context.self, forKey: .context) - } - + public var campaignType: String? + public var sendId: String? + public var tracking: [String:Any]? - public struct Context: Decodable { - - var campaign_type: String - var ext_customer_id: String - var app_install_id: String - var campaign_hash: String - var campaing_id: String - var mobile_app_id: String - var om_customer_id: String - var account_id: String - var campaign_version: String - var node_id: String - var send_id: String - var tracking: [String:Any] - - enum CodingKeys: String, CodingKey, CaseIterable { - case campaign_type - case ext_customer_id - case tracking - case app_install_id - case campaign_hash - case campaing_id - case mobile_app_id - case om_customer_id - case account_id - case campaign_version - case node_id - case send_id - } + enum CodingKeys: String, CodingKey { - public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - campaign_type = try values.decode(String.self, forKey: .campaign_type) - ext_customer_id = try values.decode(String.self, forKey: .ext_customer_id) - app_install_id = try values.decode(String.self, forKey: .app_install_id) - campaign_hash = try values.decode(String.self, forKey: .campaign_hash) - campaing_id = try values.decode(String.self, forKey: .campaing_id) - campaign_version = try values.decode(String.self, forKey: .campaign_version) - mobile_app_id = try values.decode(String.self, forKey: .mobile_app_id) - om_customer_id = try values.decode(String.self, forKey: .om_customer_id) - node_id = try values.decode(String.self, forKey: .node_id) - send_id = try values.decode(String.self, forKey: .send_id) - account_id = try values.decode(String.self, forKey: .account_id) + case campaignType = "campaign_type" + case sendId = "send_id" + case tracking + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + campaignType = try? values.decode(String.self, forKey: .campaignType) + sendId = try? values.decode(String.self, forKey: .sendId) - guard values.contains(.tracking), - let jsonData = try? values.decode(Data.self, forKey: .tracking) else { - tracking = [:] - return - } - tracking = (try? JSONSerialization.jsonObject(with: jsonData) as? [String : Any]) ?? [String:Any]() + guard values.contains(.tracking), + let jsonData = try? values.decode(Data.self, forKey: .tracking) else { + tracking = [:] + return } + tracking = (try? JSONSerialization.jsonObject(with: jsonData) as? [String : Any]) ?? [String:Any]() + } - } From 057ab4679a58e76a19687924590f57907c57650d Mon Sep 17 00:00:00 2001 From: Sergiu Corbu Date: Tue, 5 Oct 2021 16:04:10 +0300 Subject: [PATCH 04/10] removed OmetriaContext & updated for flat decoding --- Sources/Ometria/OmetriaNotification.swift | 37 ++++++++++++----------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/Sources/Ometria/OmetriaNotification.swift b/Sources/Ometria/OmetriaNotification.swift index 4f1aafa..3d00273 100644 --- a/Sources/Ometria/OmetriaNotification.swift +++ b/Sources/Ometria/OmetriaNotification.swift @@ -23,8 +23,10 @@ public struct OmetriaNotification: Decodable { public var deepLink: String? public var imageUrl: String? - public var externalCustomerId: String - public var context: OmetriaNotificationContext + public var externalCustomerId: String? + public var campaignType: String? + public var sendId: String? + public var tracking: [String:Any]? enum CodingKeys: String, CodingKey { case externalCustomerId = "ext_customer_id" @@ -32,33 +34,32 @@ public struct OmetriaNotification: Decodable { case imageUrl case context } -} - - -public struct OmetriaNotificationContext: Decodable { - public var campaignType: String? - public var sendId: String? - public var tracking: [String:Any]? + enum TrackingKeys: CodingKey { + case tracking + } - enum CodingKeys: String, CodingKey { - + enum ContextKeys: String, CodingKey { case campaignType = "campaign_type" case sendId = "send_id" case tracking } public init(from decoder: Decoder) throws { - let values = try decoder.container(keyedBy: CodingKeys.self) - campaignType = try? values.decode(String.self, forKey: .campaignType) - sendId = try? values.decode(String.self, forKey: .sendId) - - guard values.contains(.tracking), - let jsonData = try? values.decode(Data.self, forKey: .tracking) else { + let valuesContainer = try decoder.container(keyedBy: CodingKeys.self) + deepLink = try? valuesContainer.decode(String.self, forKey: .deepLink) + imageUrl = try? valuesContainer.decode(String.self, forKey: .imageUrl) + externalCustomerId = try? valuesContainer.decode(String.self, forKey: .externalCustomerId) + + let contextContainer = try valuesContainer.nestedContainer(keyedBy: ContextKeys.self, forKey: .context) + campaignType = try? contextContainer.decode(String.self, forKey: .campaignType) + sendId = try? contextContainer.decode(String.self, forKey: .sendId) + + guard contextContainer.contains(.tracking), + let jsonData = try? contextContainer.decode(Data.self, forKey: .tracking) else { tracking = [:] return } tracking = (try? JSONSerialization.jsonObject(with: jsonData) as? [String : Any]) ?? [String:Any]() - } } From 53999b909deca2c4f5b7a73f0985ed0112e643a1 Mon Sep 17 00:00:00 2001 From: Sergiu Corbu Date: Tue, 5 Oct 2021 17:13:40 +0300 Subject: [PATCH 05/10] modified deprecation message --- Sources/Ometria/NotificationHandler.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Ometria/NotificationHandler.swift b/Sources/Ometria/NotificationHandler.swift index 72b0b20..cae570b 100644 --- a/Sources/Ometria/NotificationHandler.swift +++ b/Sources/Ometria/NotificationHandler.swift @@ -19,7 +19,7 @@ public protocol OmetriaNotificationInteractionDelegate: AnyObject { - Parameter deepLink: the processed url string that was received in the interacted notification payload */ - @available(*, deprecated, message: "Deprecated in version 1.2.1. Use getOmetriaNotification instead") + @available(*, deprecated, message: "handleDeepLinkInteraction is deprecated and will be removed in a future version. Please use handleOmetriaNotificationInteraction instead") func handleDeepLinkInteraction(_ deepLink: URL) func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) From 46db767fe8139e08c293f5ad2d489bda707a17f6 Mon Sep 17 00:00:00 2001 From: Sergiu Corbu Date: Tue, 5 Oct 2021 17:20:23 +0300 Subject: [PATCH 06/10] renamed decoding + serialization file --- Ometria Sample/Podfile | 4 ++-- Ometria Sample/Podfile.lock | 11 +++-------- ...{Decodable.swift => Decodable+Serialization.swift} | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) rename Sources/Ometria/{Decodable.swift => Decodable+Serialization.swift} (91%) diff --git a/Ometria Sample/Podfile b/Ometria Sample/Podfile index b540362..a4e9e5c 100644 --- a/Ometria Sample/Podfile +++ b/Ometria Sample/Podfile @@ -4,10 +4,10 @@ target 'OmetriaSample' do use_frameworks! - pod 'Ometria', :git => 'https://github.com/Ometria/ometria.ios_sdk.git' + pod 'Ometria', :path => '../' target 'OmetriaSampleNotificationService' do - pod 'Ometria', :git => 'https://github.com/Ometria/ometria.ios_sdk.git' + pod 'Ometria', :path => '../' end end diff --git a/Ometria Sample/Podfile.lock b/Ometria Sample/Podfile.lock index 579291f..4864c1f 100644 --- a/Ometria Sample/Podfile.lock +++ b/Ometria Sample/Podfile.lock @@ -53,7 +53,7 @@ PODS: - PromisesObjC (2.0.0) DEPENDENCIES: - - Ometria (from `https://github.com/Ometria/ometria.ios_sdk.git`) + - Ometria (from `../`) SPEC REPOS: trunk: @@ -68,12 +68,7 @@ SPEC REPOS: EXTERNAL SOURCES: Ometria: - :git: https://github.com/Ometria/ometria.ios_sdk.git - -CHECKOUT OPTIONS: - Ometria: - :commit: eb89038304cd8e01461fcd10aeea51086d431186 - :git: https://github.com/Ometria/ometria.ios_sdk.git + :path: "../" SPEC CHECKSUMS: FirebaseCore: f4804c1d3f4bbbefc88904d15653038f2c99ddf7 @@ -86,6 +81,6 @@ SPEC CHECKSUMS: Ometria: 6477d0bcaefbdba4effbdaff0e76707dd472ec5b PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 -PODFILE CHECKSUM: 67fb874528c489a0ad0238479b9dc2a899a739c9 +PODFILE CHECKSUM: 971be00875f337bfd598cb9bcd32e1c6b5cbc944 COCOAPODS: 1.10.1 diff --git a/Sources/Ometria/Decodable.swift b/Sources/Ometria/Decodable+Serialization.swift similarity index 91% rename from Sources/Ometria/Decodable.swift rename to Sources/Ometria/Decodable+Serialization.swift index eb3484a..49d9d41 100644 --- a/Sources/Ometria/Decodable.swift +++ b/Sources/Ometria/Decodable+Serialization.swift @@ -1,5 +1,5 @@ // -// Decodable.swift +// Decodable+Serialization.swift // Ometria // // Created by Sergiu Corbu on 29.09.2021. From 0e77d91aaad6b9eea56f3ce3d5ebcdf55986d662 Mon Sep 17 00:00:00 2001 From: Catalin Demian <> Date: Tue, 5 Oct 2021 21:31:35 +0300 Subject: [PATCH 07/10] update OmetriaNotification initializer and overall notification related documentation --- Ometria Sample/Podfile | 5 +- Ometria Sample/Podfile.lock | 16 ++--- .../UserInterfaceState.xcuserstate | Bin 26277 -> 41405 bytes Sources/Ometria/NotificationHandler.swift | 16 +++-- Sources/Ometria/Ometria.swift | 2 +- Sources/Ometria/OmetriaNotification.swift | 65 +++++++----------- Sources/Ometria/OmetriaNotificationBody.swift | 3 +- 7 files changed, 49 insertions(+), 58 deletions(-) diff --git a/Ometria Sample/Podfile b/Ometria Sample/Podfile index a4e9e5c..fb4e243 100644 --- a/Ometria Sample/Podfile +++ b/Ometria Sample/Podfile @@ -4,10 +4,10 @@ target 'OmetriaSample' do use_frameworks! - pod 'Ometria', :path => '../' + pod 'Ometria', :git => 'https://github.com/Ometria/ometria.ios_sdk.git' target 'OmetriaSampleNotificationService' do - pod 'Ometria', :path => '../' + pod 'Ometria', :git => 'https://github.com/Ometria/ometria.ios_sdk.git' end end @@ -21,4 +21,3 @@ post_install do |installer| end end end - diff --git a/Ometria Sample/Podfile.lock b/Ometria Sample/Podfile.lock index 4864c1f..1eb1d09 100644 --- a/Ometria Sample/Podfile.lock +++ b/Ometria Sample/Podfile.lock @@ -1,19 +1,19 @@ PODS: - - FirebaseCore (8.7.0): + - FirebaseCore (8.8.0): - FirebaseCoreDiagnostics (~> 8.0) - GoogleUtilities/Environment (~> 7.4) - GoogleUtilities/Logger (~> 7.4) - - FirebaseCoreDiagnostics (8.7.0): + - FirebaseCoreDiagnostics (8.8.0): - GoogleDataTransport (~> 9.0) - GoogleUtilities/Environment (~> 7.4) - GoogleUtilities/Logger (~> 7.4) - nanopb (~> 2.30908.0) - - FirebaseInstallations (8.7.0): + - FirebaseInstallations (8.8.0): - FirebaseCore (~> 8.0) - GoogleUtilities/Environment (~> 7.4) - GoogleUtilities/UserDefaults (~> 7.4) - PromisesObjC (< 3.0, >= 1.2) - - FirebaseMessaging (8.7.0): + - FirebaseMessaging (8.8.0): - FirebaseCore (~> 8.0) - FirebaseInstallations (~> 8.0) - GoogleDataTransport (~> 9.0) @@ -71,10 +71,10 @@ EXTERNAL SOURCES: :path: "../" SPEC CHECKSUMS: - FirebaseCore: f4804c1d3f4bbbefc88904d15653038f2c99ddf7 - FirebaseCoreDiagnostics: b63732f581a1c6a453ec7241f9ab60b3a5bd3450 - FirebaseInstallations: ede6fb72bb6337914e5888b399271259d0c4910c - FirebaseMessaging: 93227dd71d7888e200baef65043f81acb2b6596e + FirebaseCore: 98b29e3828f0a53651c363937a7f7d92a19f1ba2 + FirebaseCoreDiagnostics: fe77f42da6329d6d83d21fd9d621a6b704413bfc + FirebaseInstallations: 2563cb18a723ef9c6ef18318a49519b75dce613c + FirebaseMessaging: 419b5c9d84f294a753c6501d8cfb9ced1ce37304 GoogleDataTransport: 85fd18ff3019bb85d3f2c551d04c481dedf71fc9 GoogleUtilities: 8de2a97a17e15b6b98e38e8770e2d129a57c0040 nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 diff --git a/Ometria.xcodeproj/project.xcworkspace/xcuserdata/cata.xcuserdatad/UserInterfaceState.xcuserstate b/Ometria.xcodeproj/project.xcworkspace/xcuserdata/cata.xcuserdatad/UserInterfaceState.xcuserstate index 55526ce51c73d274fe4dcf81d0aff6d76c323465..8d603c379636d43f13416cc7141eb7abaefc99c1 100644 GIT binary patch literal 41405 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 delta 12988 zcmb`tcR*8D|398{?t%m`5E4ch30V+O2_p~!F%Uu^ggq1&B13V21FP0*?y)y3$t*ee|)w*hze$P$7wogCL^ZVyVxHq}y+%sOU_c-tOx%bOm zaOolN_877xAHqej9WH~*;U{nnTnpF14R9y?8Xkow;VF0; zo`vV&d3X_Cg+IWX@JDzD-X$o4CK$qt@Fw_9LO}QszJwnUK!gyXgp`mIQG}L=CE|#l zL_DD*3`81XB+`itB9|y4Dv7?tKw>b_Knx*<6V1dZVl*+1m`F?`UMJ=d?-BEe4~aIS zomfVENo*yy5!;C!#7<%tv76XKd`0XhjuGDwXNZf$CE_x1g}6mHZxi>3pNad#Q{oxP zBLN9XKe8JsB>l+%GMEe@BS|IMgX~GhlWC-pOef7`Hra=)BKwm4$o}L2av)hv4kCw< z!^sikNOBxGk$jPyLORJA*$FtyCMeoa&&~P;04m)MwNd>PzYbb&@(oouB+=1IwpZhWRjRf$$GH)@nn0J`D%!kY(W-+so*~DySzF@X6Uou;nZOnFN z2eXse#q4JGFkdlynSIRH%pv9k^9^&Bxz5~TZZr3opP5I@W9B#Jcjj4zvT59i5hr-Q zJQrk=fx(r^Ads?dGs6Ja+Dys1@4O_mztGa$R8u={gsiN-c3flq*xGeI-R=Vs+&y^I zh{mxk@$n}CBuJ`9&t#Xsf0}Lu?c*O9943oWYmYNt-h%Q`H7!G{8k_3sC*(FZwY2g1 zy^BWHw~T455rX5se%*wwBmOG?A$#{8>`MxHxJiJZozEA!N&-aGaj_)GUEdAY?^~RX zYeS^CHq6RAZ91KT*ZJ3$n4~a|_FO2;(>J<*wd!$iDS{<<3aQn? z7+2=Z7$?t*=g*V!dhjxM`MgSAKi(kTFy2VsINo^POT1aU4|&UY9lTF?Yk7Nk`+48- zZu1`Te&am_5)cf+K|JUMazOzo0_9)`cmX)UH1Haj4;F$pupaCNhrns@EjSN;05`zT z;5YCG1d#Sj3Ig}Hh{rQ{3cAiP!4)L_+NIzJyI$gZD>?D{@w$On(1WLK196}`+nepv z#_JE_*$TFbjjtA}T$jWW*FXvFN|*F-T@)*wdSKycKL#lv6&OGoFoJZD0Ze$ZW{}An z%JyUXvjfvVj#J**q0@@S=f(`vr>F26iYu8d#0{kg&tt zzlL*z`{!j_EzKiF)QlQiUzgK3qJC_((DhXEQ~%@cX%7Gc7qdf-^V)drU=XOOC@d?h zZ)pRAc*{KXwV-Y>+vurp07C{AmKBU^sTn+?K6A{Nnu+b80Sx6W>s?spz7##{{Bu1E z3UZAJE+o0euHC^VVO=|50RmIm7Ip&rBHOqwAS46OYuL32DArYkZU8=YZQugHluH*8=(U(_ zW!pNn#@jt%sD>KX_=rfN4Mw}hMZ^fSFcvD|29qUG#d zwyn#IJkW;`ASw{c4m3-XqZ^oJ85(9@%AM*(GDLyBSWv z4m{WjJj};%3D)x-gKNgt_ru>~>PKM7y-<$_{fgb~5$|gFDK;Ri4Q+6>>#{PZQw=|Z z>s_N&L0AnxcQvb|&i{*qjc^m(48MR|FbNy+c{|*}9%jE~&$0*DhZz51d_KcI_$U5d za5oI-#DAJS*kvUi{QF_}5_kX}goof^_7HoNJ;9!I^Wb!w%|>_}tNLN~2xm0i@FIkj zx2a`pkFnz$8(M^9mqmO7&*0mRvB%lA@`{$m5kl|I8Z4-Pna?FRxu^baK3())bJP0` zM=wz=EXVv>W&bhg->JUF(dwc!vZ8zNH=ecw{tWNK2k;?$1Ruj+;1l>OdyYNNe#c&5 zzh^J9m)Ohfl@9njc8d4|pAkH;nWrKM_9}LS+~)>+7tcz=-doqJcQVK#y20>PLdgEW zcDTtRM3^ihkPveRdyT#BK01Ux!XBNbLxd3#JV`4N&faV#BH15ZvrE(?2nC_$N!kb{ zp<-{bx7!E}5zYR@-eGn9Ml?3nyKSLobu^Vt;1uKkw7!jIlYZo>TUJo01%E;$0&$i7c!|aw3b!!CJH!YY~rq%s#_f zv>2bC{)ZM3d4&DB7X8Wo@((Q{iaBCRh*F}AC}*8d*k9S-*x&y$C+vK&dbnM0m&W|f z$^Lg$A!^;K^2h(BDsJEE@uGiMi42xUVpCh`8rU-|Ob9(Rj=?UD7>fYJYZUPUuNyI* znBbD7ggXfYWdD^T;w54-UN2SBO`;SVBO60QFCv-r#t8 zlX#1mMZApw?L+{_dR_>4|Np?%B91Er`2SyIc_I;FIkA>krR^X(h!w<2;$vbJ@d>e- z_>@?KfG+}m2y{b0h=4x=0SJf?2t+{KL9FxG4zYpQ2%E6&e1U+(V>}21BM^?gJ*z<= z!fimA#9q#T_8}0|WkClp0bsM|gD2|wbaW~8apELrJtq(dX(dh}5X#w)llYc6@3EhA z>@5VOoU&hFe?lOP)%}On`1?#&n4|w`DOZW>9!t5#{)|B6|7h)Y&%{r}oob;N$3xb- z#+K$Wxvr1X1UQYn>spu==M)lMbA3QO!HXpEka$EqCVoLciGT_LH3FI?#IM9}FpBsC zfoOySy;cNzvhjT!^$jhRjrHTJN*jj^b^q$?sF~P2uBD*4uHG|Z84l*%HQDup#|_CG zQ(x0L8cC2|cr-GKBq@?6vC_mKphX}Sfw(0&y4y@>c+pr6bJ?FvY3bg8qepHMOkmxGDym{t-CE1r#aTL>uwb#0`aj6y(%K+^M2fK-t&JV`sa0afkK{t@yn})Voh|67H2FylJ)7?_IyWCMP={413Yzu=iAj~p z+7UHl$MzYIleOmYdD+#%z)|&M#^M|gN051S_|(|YSU-mK7N)UY?*5bFkEf&>(p-yu zB$W|`WyR0$uE?`C)neVME3I#+A5-5{Ti<3dV#q~zh0}+*V*b zQC!lqw5;6qs<{^>Wc}Fy*Qc2}XG&_HDtvEmH=NFU`xtQ-{rdMWEX!`J<>K0!F%#SR z83)EsNNF(C4X#fz=u>Lr-6>rtPsxknCGb*sc{rh)z+-vu@D|}@?i1byoXDN#-Q+z2 z0-VN0f*v@HO8`kA8R*^lk`d=hy?`?dXFfSNyUPc)pb@+Z-UOS#VQ>Uo2Y0|D@Dz$+ z5DbA*yg+GS0yM(@IK~|cn{hlk6^ER!!q*@dYP#@-)-JdQ?!}u~_uwA{AcTYn?@;N9 zK6nqRKi-9U0XuaUb`wjnJKaER_80;LPR|eCz|QZ^FXC$iQr+%`%p|i0JWrRhv9`D4 zA`5RBR#EP%%W$n^Ay2!U%pq-LE}2K#$$Zj579fy@fDwUo1TqjXABbbVy9l?AA3lJ=H9kBFw_06tPlQm>Lj^fF|WGz{TfCYhU z1g!031382oia-tm{Si)~E@dxFAe+e1IEf{j$x#T{5XfsI$B=k+%}1b&jsM$XOitk9 zGX!#7pIWQ^pGU~#WKXec_RNO3E_^IGjT;`3Q^}VRa3FxO6}A%^@>Oy==j)3QD0a=Y zsa#KTB9% z@;$71o%tzFW82AhxkR>WB6G2!Y-RR3Xrpjd#tn zDLUsO_xB946fbt&ef_4dh0+o!m@dISfFc8i5)FY7wYMUtgU0`G{liHu)2IhrCPPBY!6ElMl#;2#i2r zBmzwcG$Vjjcr*fI5WouD(m_7<#5d$`9rhy8hBHiH)6~To!FSb!K3g_075P0cd z@r_&2D3vEG2zsXI@+zH}DJ|9A9qUkWT&&~#TURc5eQcol)^2)u^Cj1J1=;e^Was^#oVoIpqR3Gj@;0*-c^qkoaVK-r7*Tk!-8jhMl2+V4w1|#q`M@!+yqY0s!sWBe3qdBy$f7A}7CQws3tP`mhsY%pJ)MRQ3<)l~&1AP~P_Yim= zfw>5LfWSNiu%$0RU||RKawpd5#71fcPer|sz=vH}Tix;PN1OsIbECCV?{jEzFN?a+ z&galBAjI5(z+(2g`-HJR=t9~~E#;6dL7=Ub`Urt`4rwU0f?9>KP%8-~0!!FiZPX_O z9%m_z=Z+2tR0uD2HD6C{^kCk=VP1|gQ=f4Daj8eZTd92<=xx+?Y6rEG+C}ZA_E29@ zdl6WHz)A$L$61BICkU)Y;8O(FAh5QB`nnVJp-urGLttGO==EI!-rTA4-*J#HAn;ii z}o^}OI&?HUKG|kXnv^UMC1+)(WUm~y-fo%wEM_>m6I}zB0z-|Pv zGyG}=?dO3?2hbuqkQVb)bPxi2yTBem;2;7=5ID*S`IxI$?-j11;j5gq5`(5y2<+$b#;(QXS<1^EgUh&|LTAvW*}{+%dzNEo1AaDkOZxJ|)z&QlYchD7`oK(?$>3*0Iyuf_d#mFTDt|4%pqvM9h!s$ki zi(v>{=;C4|$3+v}%pC}PkHAIG8FP^?ly}Yi1$qKj1R6Wh%dPZ81g>C3&{R#PS&WCC z!s)?PtOqpW^xy})J@T*A&TEds9O@xN7~9qB4EhZZAFp$K+~oK`9HBqB&4HdntG3OKtjVi<<+M#^|&q|gV;i}Atog1-Nj zH(b}%mp>Ee#?Oc_e%S4=MsB5NLK!8-&q$dtCY*_2A{iMYXQCJdg8m2wASgmG5J545 z5(I+~3`Q`dgK=*G@>EQWXCs5@j$mjP{;=m88Bm59&;zpp6TqY)DD5PG$-o5s*L@5o zo5|t8TM^gHhF}B-UdrS%g&yz)9Qa61j#x-|p%3GLNSSh`w+Ck>hg1F^^|8zVrWz;j z%s>Pct;`?7sEcGZOD1FpbPGW;infK{bLJ1fvm* zS;90i&CDofG=f?LV-bu)Fv+!|`KSKOiyVYW2zF;XPBK&QGYz~C&NvyCsb{8EjjbPn zGfAAlA=nea9(Zg76BYPb-kyh`Y--oIuQ0FSbb)!5nT}vQg1RAc)0fL@*t}3|1svD|VH#B$3;Fb@~$s#a8;hIk z3~@TQw_x!0dn?bMg?RtZvoFUSW{zSZb7if}F$5i*J2cEmQ0KX`(bMt_mo5}{UYvvB zD~KF&9o)_gWG*t7n9Ix+<|=%H#pXW%CkS|B5Eda=f?yef6$tib_i#Io&trM!M|U8N zp9{%z8YF)!zw0S4{hu*D^U!lwIhP>(75X#3dWtK#(Eo46Pd&wba8@|L^~+3&)BPfY zhU$r9(M&1_zrtY0FD{f&E5IwzOX3ym73vk{72_4_)!nP7m(DBEE7>c> ztFPAtulZi9y>@tA^19>or`I!Y;7xc_-i)`mx4=8pJIp)6Tjm|*t@KuVM|*3%4c>jc zo4sA$TfL8ZKjTaIL3|}&!;j&|@q6&&`C0rTC%=?m&hO2y;`ig%@<;K<@yGKg@~7}w z{#5=n{ww@B{P+1E@aOXv^V|4K_#g3C@jvJ9;P2;OF3kpGsWjMpE*7YeU|&I z@cGzhmCtIQH9qTncKe+7`N8K$pW8lneD3+&_xatI@@0HOeHFgRzUjVJ-$LI8-{HP5 z`_A`W?AzhH-}i{`J>Nh5Xg@DMzMqfN&(AN&FT_vk7w#A7C-+PBbNJQ!v3_s*z2!H{ zZ?@mZew+NZ_-*yu;kV0gkKaYVo89PcUfuZq$NkR*kO6{#q<{|swgh|~a5&&-!0~`@ z0=^A67x0~k5=D!0L}j88q9)NO(HK#S=mpUP(Tk#&L{mhpXsT$MXrXA4s8!T1axN7u z6LpAIidKnMi`IzNiPnoYhz^LZh<*w54~z@61=a>m4V)dgG;nj^uE5=aUj-fsJQa90 z@Jis-z-xhb1D}d1v9DMn?k?7gE#e$;t~g&@ATAP*6~82&EOv@t6Td5-E1oA_C|)FP z6|Wa>74H%66@M*0AU-5ME50PY;uQZNzAnBaz9+sfekgt{AtnA2r9>x5m8411B_@eg zVw2=a@+Ae5N=dC`m}G>cNis?@UNTWKNitdDl+2MVl5CM|m28*nl6@1tK@gdpOR-mAczQ}f|wxhAVH9CkT579$Qc+E9h4u$2E88i zNzj&{BSH6rL9iy+5L^;GBzR8nqTsgRCBe&rJAzjRZwuZRd?@%x@Uh?%!54$C2j2|7 z75r21!{EokPlA67{v(7R5)`5hNeambv4@m|l!a7;^bQ#qGALwlNL@%n$b=9S@?J=L z$cB*pAqPVahkO%qCFDlPLuV)mg`q?!6&ew$3pI!K2^}6fJ#!ELk&I+9!>I&Tx zdN}l(&~HP}g?<(hbs0 z(l4Z6O7}|-N)JnqO3z3yNv}yCNS}pyhY7-b!@7mBVKc%Og)Ir&9Om2=b~x;4*zxed z@bK`M@E+l5;g;~G@Uh_^hIfRo4PPI=A$(K#SK&v(kA_{H$c;Xj1m2)`Bn zQv`?zicm!Kh%iQEMPx_hMC3*kMU+I8NA!-UifD>J5w3^@5$zF6BbG&UM68TB8F4Q% zFj5_v9XTR$LFA{ATO!X!o_9uGh`bniIr3`cwa6QhKSth;ydz^|-ZFvASJqADFB8ea zWOA88rjlu7Niw}GRhA}m$f{)hWCLW?vJtW-*(li<*(TXnvcs~ovh%VFvWv2tvb(Z} zaxZzRJX79BK0w|gUnpNBZiBaaLtf=g$oT##>im2XERZ%rj zO;Mww#zeJ59f&#;bvEjJ)P<;r3ZMv7NEE?}P(_#`LLpP&{dk31VNe(q8H!$tOoc^Z zRoE1HihQS{Kru`)L$OM6MDaiwqRdeCS58t+Q%+aTP`<8wN4ZeBNZG1vS1wg9Q+6m< zDmN;>P=2Z0uH32It-PeXqkN!zr2Iwshw>@+P*>%r3RkIB8dZ!cPL-_6QrT2_s(e+k zs#I03s#Nt?4Oh)qEma*>9aSAyom8Dxol!Z@s?Mt}s4l85tA1Dgsd}acYC=t^8MROy zqL!+|)sbqsTA_|tC#aLudbLGuRom2g>PmGVbzgOVb)$N?dZfBpJzhOc{i^yk^-T5q z>JQZO)eF^|)L*D~tM{t+s}HJAsqd*Ds2^)0HFAwYqt?V|Vl~}0JvBN_qEnNsDb$o` z$~BdmKAOIo{+fDClV-H0MKfOWqUI&d49)ABH#M^~3p5{U7Hirxt2Jvh>on^%J2iVW zdo^Ecj%hAvE^01ou4;acCZm~Xezb42Fj^EXi4KVli;j#=h)#}9i8e$VqfOD7(b>^C z(RtDN(LG>f%+#1yVqS}x8RLps5c6To;+VFWB{3hxtc&?P zW>d_Tm~AmTVlKqoin$kaKju-)Z!v$wJktWLP%F}gX=AiKwDH*tJ>#wueqvD(<~ zvGK8nSYvEPY_HhNSW9e8?3CEn*v+wfWB11%iai>8JoaSljo3%APhx+I{WFdi2jc{B z!Z=Z!BrZ5k6&DxRBQ8EJA$2G&E!VBoZP9Jj?a=Mk?bYqq z9nu}uozR`q-A?dMkR)_Z=#|hvp($ZT!g~n|6BZ}5Cw!E!D&f--acbf#iI)@aCjOlGAn|chzoel_&CaATN#l|xBsr5% z(zK-MNi&jGBpph+lT0Q1CHp4_CI=i^nFD73}{y`7*A^Ir2N*}F{)hFqb^(p#neQ$kV{Q&(SeXYJh->4t1 zZ_P9YuBSXqd7Sbj<@eOysdcFhsY6qTr|wQYmU<%fRO%T+k|EPzF<1?`hFOLMhSi3( zhV_PxhA#|T4Lc0G4SNmy4c{3q8ZH~I8m<{`8g3iz815M!7#=y(0@8G86=~zs=BMpQ z`@u*W6~;JYPh)~H$(UixG3FZWMu)M`SZo|%9AvCDHW(X?!;P;Q-!;xNE-)@KE-@}M zt}uRV{KB}~xW~BHxZilfc+vQS@w)Lx<6Yx@<0IoQ>2$g%U7a429+%!TJs~|gJvH5! zZc5KgFHi5C?yO4hmp&kUQ2OBXy7Y$h#`NLouca?bKal=YhJQv%M!$>~Gd{>@%UGJR zJYz-1x{NIu+cI`$?8(@du|MNr#^H=}85c4xWn9g;o^jJen?$ByQ>ZE26lGGH)TU@t ziYeQaXUaDfn#xQSrrxG1(@@hm(`%;JO>dcIo8B?KZ<^;cEi^4QwVOUSZ8Cjf`qH$` zw9~Z5w9mBPbjWnXbi0?JSI=H0y~g#L-)md13%&j{(`Ik8kJ-;0WR{r~W|cYGtTo4( zdzg)8lR49zZMK>7%njzT<_YFW=E>%%=2y(q%`?n%&GXF*%`42S&1=k`nKzlgFmE+) zHy<&dH#_f|@0%Z)pO}9)Kg|T0WG0iz&kWC$WkzKxGu4?fnX#GOGkazx;FP8$b5!O# znV)B#&EjP#vn*MIvxa9iWsS}noApxGD_PUCW@Npd^=8(rtOZ$%vf8qiW-ZTJk+nDL zWY)J?=d&(kUCFwZbtCJ?tY5SKuy|YiEJBOO5^M=|TEZ=nmL3+PrPxwtskBsC`dg|k zgDv%zp_bv6CdwfDY>rv|o>uKw^*7Mfyt(UEjtWT`J zS^uy;%>g-N4wK`ZO4o@sJvNupX6=G+no1h-nP7bdB^fjNlWS7}hc8y(Y?`co4C)xFOtG(3T+g@ewZ?Cb}*&FOb?c?p!?C;y>*%#Uu z+uQ9Q**okX+gIDy+V|M^+4tKI+7H{0*-zL{*}t)$wV${Dnjf5>o90rHck?Y8J6go;A<&H|nSO@EP#WCG6(=p32+wqR$ zJ;zeVa>shdX2%xCHpgzqSB|e82OMV|*B!q({w&}X5CwFBcY#kqw}OBIaY1xJY(e*e zo&~ysq=J-!w1V`4UIm#20}Cb>v=;0txL)X6s4FZkY%ZKoIH_<-p_46~Q8=gYox=AD z=N8T@Tu|6i_;KOt!nK9#3pW&=EWA{Bt?)+St-^bS4+lj%R(!VjJ7@95;>*PkivKL(l@KL#iFZjrNo0w#L{p+I=~N@D<4^Yy!`v}OXXL}uT`W~ v*edcW@+%4}mQ<{%SXZ&WVxzV)uu@*Bs8m&UzPSUTXPei{^Y#2(`O5zTQi9Vh diff --git a/Sources/Ometria/NotificationHandler.swift b/Sources/Ometria/NotificationHandler.swift index cae570b..e7d4d31 100644 --- a/Sources/Ometria/NotificationHandler.swift +++ b/Sources/Ometria/NotificationHandler.swift @@ -16,20 +16,24 @@ public protocol OmetriaNotificationInteractionDelegate: AnyObject { /** Allows you to handle the outcome when a user interacts with an Ometria push notifications that has a valid deeplink url in the payload - - Parameter deepLink: the processed url string that was received in the interacted notification payload + - Parameter deepLink: the processed url string that was received in the interacted notification payload. */ - - @available(*, deprecated, message: "handleDeepLinkInteraction is deprecated and will be removed in a future version. Please use handleOmetriaNotificationInteraction instead") + @available(*, deprecated, message: "will be removed in a future version. Please use 'handleOmetriaNotificationInteraction' instead.") func handleDeepLinkInteraction(_ deepLink: URL) + /** + Allows you to handle the outcome when a user interacts with an Ometria push notifications + + - Parameter notification: a representation of the notification payload. + */ func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) } extension OmetriaNotificationInteractionDelegate { - func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { + func handleDeepLinkInteraction(_ deepLink: URL) {} - } + func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) {} } class NotificationHandler { @@ -102,7 +106,7 @@ class NotificationHandler { } do { - let notificationBody = try OmetriaNotificationBody(dictionary: ometriaContent) + let notificationBody = try OmetriaNotificationBody(from: ometriaContent) return notificationBody } catch let error as OmetriaError { diff --git a/Sources/Ometria/Ometria.swift b/Sources/Ometria/Ometria.swift index 467e662..f14d92d 100644 --- a/Sources/Ometria/Ometria.swift +++ b/Sources/Ometria/Ometria.swift @@ -477,7 +477,7 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { /** Validates if a notification comes from ometria by checking its content and retrieves an OmetriaNotification object - - Parameter content: The content taken from a UNUnotification that has been received + - Parameter content: The content of a push notification that has been received. - Returns: An optional OmetriaNotification object */ diff --git a/Sources/Ometria/OmetriaNotification.swift b/Sources/Ometria/OmetriaNotification.swift index 3d00273..23d66af 100644 --- a/Sources/Ometria/OmetriaNotification.swift +++ b/Sources/Ometria/OmetriaNotification.swift @@ -11,55 +11,42 @@ import Foundation /** An object representing a notification. - - Parameter deepLink: The URL that was sent in the notification. We append tracking parameters to the URL specified in the account and campaign settings (these can be changed in the Ometria app) + - Parameter deepLinkActionUrl: The URL that was sent in the notification. We append tracking parameters to the URL specified in the account and campaign settings (these can be changed in the Ometria app) - Parameter imageUrl: The image URL that was sent in the notification. - - Parameter campaign_type: Can be trigger, mass, transactional (currently only trigger is used). + - Parameter campaignType: Can be trigger, mass, transactional (currently only trigger is used). - Parameter externalCustomerId: The id of the contact that was specified at customer creation (in the mobile app) or ingesting to Ometria. - Parameter sendId: Unique id of the message - Parameter tracking: An object that contains all tracking fields specified in the account and campaign settings (can be changed in the Ometria app, uses some defaults if not specified). It has the same values that we add to the deeplink url. */ -public struct OmetriaNotification: Decodable { +public struct OmetriaNotification { - public var deepLink: String? - public var imageUrl: String? - public var externalCustomerId: String? - public var campaignType: String? - public var sendId: String? - public var tracking: [String:Any]? + var deepLinkActionUrl: String? + var imageUrl: String? + var externalCustomerId: String? + var campaignType: String + var sendId: String + var tracking: [String:Any] - enum CodingKeys: String, CodingKey { - case externalCustomerId = "ext_customer_id" - case deepLink = "deepLinkActionUrl" - case imageUrl - case context - } - - enum TrackingKeys: CodingKey { - case tracking - } - - enum ContextKeys: String, CodingKey { - case campaignType = "campaign_type" - case sendId = "send_id" - case tracking - } - - public init(from decoder: Decoder) throws { - let valuesContainer = try decoder.container(keyedBy: CodingKeys.self) - deepLink = try? valuesContainer.decode(String.self, forKey: .deepLink) - imageUrl = try? valuesContainer.decode(String.self, forKey: .imageUrl) - externalCustomerId = try? valuesContainer.decode(String.self, forKey: .externalCustomerId) + public init(from dictionary: [String: Any]) throws { + imageUrl = dictionary["imageUrl"] as? String + deepLinkActionUrl = dictionary["deepLinkActionUrl"] as? String + + guard let context = dictionary["context"] as? [String: Any], + let campaignType = context["campaign_type"] as? String, + let sendId = context["send_id"] as? String + else { + throw OmetriaError.invalidNotificationContent(content: dictionary) + } - let contextContainer = try valuesContainer.nestedContainer(keyedBy: ContextKeys.self, forKey: .context) - campaignType = try? contextContainer.decode(String.self, forKey: .campaignType) - sendId = try? contextContainer.decode(String.self, forKey: .sendId) + externalCustomerId = context["ext_customer_id"] as? String + self.campaignType = campaignType + self.sendId = sendId - guard contextContainer.contains(.tracking), - let jsonData = try? contextContainer.decode(Data.self, forKey: .tracking) else { - tracking = [:] - return + guard let tracking = context["tracking"] as? [String: Any] else { + throw OmetriaError.invalidNotificationContent(content: dictionary) } - tracking = (try? JSONSerialization.jsonObject(with: jsonData) as? [String : Any]) ?? [String:Any]() + + self.tracking = tracking } } diff --git a/Sources/Ometria/OmetriaNotificationBody.swift b/Sources/Ometria/OmetriaNotificationBody.swift index 9cdb146..78cce7c 100644 --- a/Sources/Ometria/OmetriaNotificationBody.swift +++ b/Sources/Ometria/OmetriaNotificationBody.swift @@ -12,10 +12,11 @@ struct OmetriaNotificationBody { var imageURL: String? var deepLinkActionURL: String? - init(dictionary: [String: Any]) throws { + init(from dictionary: [String: Any]) throws { guard let context = dictionary["context"] as? [String: Any] else { throw OmetriaError.invalidNotificationContent(content: dictionary) } + self.context = context self.imageURL = dictionary["imageUrl"] as? String self.deepLinkActionURL = dictionary["deepLinkActionUrl"] as? String From 9697a35ee66d3eab2299813e0a087f2aa40306d8 Mon Sep 17 00:00:00 2001 From: Catalin Demian <> Date: Wed, 6 Oct 2021 10:04:12 +0300 Subject: [PATCH 08/10] update README.md --- README.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index f6923da..c0930ca 100644 --- a/README.md +++ b/README.md @@ -402,17 +402,16 @@ The Ometria SDK will automatically source all the required tokens and provide th This way your app will start receiving notifications from Ometria. Handling those notifications while the app is running in the foreground is up to you. -### Handling interaction with notifications that contain URLs +### Handling interaction with notifications -Ometria allows you to send URLs alongside your push notifications and allows you to handle them on the device. +Ometria allows you to send URLs and tracking info alongside your push notifications and allows you to handle them on the device. By default, the Ometria SDK automatically handles any interaction with push notifications that contain URLs by opening them in a browser. However, it enables developers to handle those URLs as they see fit (e.g. take the user to a specific screen in the app). To get access to those interactions and the URLs, implement the `OmetriaNotificationInteractionDelegate`. - -There is only one method that is required, and it will be triggered every time the user taps on a notification that has a deepLink action URL. + This is what it would look like in code: ```swift @@ -429,10 +428,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } // This method will be called each time the user interacts with a notification from Ometria - // which contains a deepLinkURL. Write your own custom code in order to - // properly redirect the app to the screen that should be displayed. - func handleDeepLinkInteraction(_ deepLink: URL) { - print("url: \(deepLink)") + // Write your own custom code in order to properly redirect the app to the screen that should be displayed. + func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { + print(notification.deepLinkActionUrl) + } +} +``` + +The `OmetriaNotification` object also provides access to other fields in the notification payload, including custom tracking properties that you choose to send. + +One can access all of these fields in the above-mentioned method. + +If for some reason developers need access to the `OmetriaNotification` object in a context other than the OmetriaNotificationInteractionDelegate, Ometria SDK provides a method called `parseNotification(_ content:UNNotificationContent)` for this purpose: + +```swift +import UserNotifications + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, OmetriaNotificationInteractionDelegate { + + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + let notificationContent = notification.request.content + let ometriaNotification = Ometria.sharedInstance().parseNotification(notificationContent) + completionHandler([.alert, .sound]) } } ``` From 18d70f79ac67853cc4cdf2aa852d041d56d6db3f Mon Sep 17 00:00:00 2001 From: Catalin Demian <> Date: Wed, 6 Oct 2021 10:04:45 +0300 Subject: [PATCH 09/10] update OmetriaNotificationInteractionDelegate default functionality and privacy options provide Sample app usage example --- .../OmetriaSample/AppDelegate.swift | 24 ++++++++++--------- Sources/Ometria/Ometria.swift | 8 +++---- Sources/Ometria/OmetriaNotification.swift | 12 +++++----- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/Ometria Sample/OmetriaSample/AppDelegate.swift b/Ometria Sample/OmetriaSample/AppDelegate.swift index 961dd61..eb08831 100644 --- a/Ometria Sample/OmetriaSample/AppDelegate.swift +++ b/Ometria Sample/OmetriaSample/AppDelegate.swift @@ -88,21 +88,23 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // MARK: - OmetriaNotificationInteractionDelegate - // This method will be called each time the user interacts with a notification from Ometria - // which contains a deepLinkURL. Write your own custom code in order to - // properly redirect the app to the screen that should be displayed. - // If you do not implement this method at all, the default behaviour is to open the browser and log a DeepLinkOpened Event as you can see below. + // Note, this is now deprecated and will be removed in a future version. Use 'handleOmetriaNotificationInteraction' instead. func handleDeepLinkInteraction(_ deepLink: URL) { - if UIApplication.shared.canOpenURL(deepLink) == true { - UIApplication.shared.open(deepLink) - Ometria.sharedInstance().trackDeepLinkOpenedEvent(link: deepLink.absoluteString, screenName: "Safari") - } else { - print("The provided deeplink URL (\(deepLink.absoluteString) cannot be processed.") - } } + // This method will be called each time the user interacts with a notification from Ometria. + // Write your own custom code in order to + // properly redirect the app to the screen that should be displayed. + // If you do not implement this interface at all, the default behaviour is to open the browser and log a DeepLinkOpened Event as you can see below. func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { - + if let urlString = notification.deepLinkActionUrl, let url = URL(string: urlString) { + if UIApplication.shared.canOpenURL(url) == true { + UIApplication.shared.open(url) + Ometria.sharedInstance().trackDeepLinkOpenedEvent(link: urlString, screenName: "Safari") + } else { + print("The provided deeplink URL (\(urlString) cannot be processed.") + } + } } diff --git a/Sources/Ometria/Ometria.swift b/Sources/Ometria/Ometria.swift index f14d92d..27d34de 100644 --- a/Sources/Ometria/Ometria.swift +++ b/Sources/Ometria/Ometria.swift @@ -508,12 +508,12 @@ open class Ometria: NSObject, UNUserNotificationCenterDelegate { extension Ometria: OmetriaNotificationInteractionDelegate { public func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) { + guard let urlString = notification.deepLinkActionUrl, let url = URL(string: urlString) else { + return + } - } - - public func handleDeepLinkInteraction(_ deepLink: URL) { - Logger.debug(message: "Open URL: \(deepLink)", category: .push) if Ometria.sharedUIApplication()?.canOpenURL(deepLink) == true { + Logger.debug(message: "Open URL: \(notification.deepLinkActionUrl)", category: .push) Ometria.sharedUIApplication()?.open(deepLink) trackDeepLinkOpenedEvent(link: deepLink.absoluteString, screenName: "Safari") } diff --git a/Sources/Ometria/OmetriaNotification.swift b/Sources/Ometria/OmetriaNotification.swift index 23d66af..42227ec 100644 --- a/Sources/Ometria/OmetriaNotification.swift +++ b/Sources/Ometria/OmetriaNotification.swift @@ -21,12 +21,12 @@ import Foundation public struct OmetriaNotification { - var deepLinkActionUrl: String? - var imageUrl: String? - var externalCustomerId: String? - var campaignType: String - var sendId: String - var tracking: [String:Any] + public var deepLinkActionUrl: String? + public var imageUrl: String? + public var externalCustomerId: String? + public var campaignType: String + public var sendId: String + public var tracking: [String:Any] public init(from dictionary: [String: Any]) throws { imageUrl = dictionary["imageUrl"] as? String From 5a8a4a1a237d2c0b1f6dfea9d3e9e6dbda267bbd Mon Sep 17 00:00:00 2001 From: Sergiu Corbu Date: Wed, 6 Oct 2021 10:57:43 +0300 Subject: [PATCH 10/10] updated method naming in OmetriaNotificationInteractionDelegate --- Sources/Ometria/NotificationHandler.swift | 2 +- Sources/Ometria/Ometria.swift | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/Ometria/NotificationHandler.swift b/Sources/Ometria/NotificationHandler.swift index e7d4d31..f9fda26 100644 --- a/Sources/Ometria/NotificationHandler.swift +++ b/Sources/Ometria/NotificationHandler.swift @@ -31,7 +31,7 @@ public protocol OmetriaNotificationInteractionDelegate: AnyObject { extension OmetriaNotificationInteractionDelegate { - func handleDeepLinkInteraction(_ deepLink: URL) {} + public func handleDeepLinkInteraction(_ deepLink: URL) {} func handleOmetriaNotificationInteraction(_ notification: OmetriaNotification) {} } diff --git a/Sources/Ometria/Ometria.swift b/Sources/Ometria/Ometria.swift index 27d34de..ea1a6b6 100644 --- a/Sources/Ometria/Ometria.swift +++ b/Sources/Ometria/Ometria.swift @@ -512,10 +512,10 @@ extension Ometria: OmetriaNotificationInteractionDelegate { return } - if Ometria.sharedUIApplication()?.canOpenURL(deepLink) == true { + if Ometria.sharedUIApplication()?.canOpenURL(url) == true { Logger.debug(message: "Open URL: \(notification.deepLinkActionUrl)", category: .push) - Ometria.sharedUIApplication()?.open(deepLink) - trackDeepLinkOpenedEvent(link: deepLink.absoluteString, screenName: "Safari") + Ometria.sharedUIApplication()?.open(url) + trackDeepLinkOpenedEvent(link: url.absoluteString, screenName: "Safari") } } }