From a062657fbabdcf8a998499e4cea0968f81d529a3 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Thu, 7 Dec 2023 18:46:11 +0200 Subject: [PATCH 1/5] refactor: add optimobile core --- .../Sources}/AppGroupsHelper.swift | 6 +++--- OptimobileCore/Sources/Credentials.swift | 13 ++++++++++++ .../Sources}/Extensions/Notifications.swift | 2 +- .../Sources}/KeyValPersistenceHelper.swift | 10 +++++----- .../Sources}/MediaHelper.swift | 4 ++-- .../Sources}/OptimobileEvent.swift | 2 +- .../Sources}/OptimobileHelper.swift | 12 +++++------ .../Sources}/OptimobileUserDefaultsKey.swift | 2 +- .../Sources/PendingNotification.swift | 15 ++++++++++++++ .../Sources}/PendingNotificationHelper.swift | 12 +++++------ OptimobileCore/Tests/MediaHelperTests.swift | 20 +++++++++++++++++++ OptimobileShared/Credentials.swift | 8 -------- OptimobileShared/PendingNotification.swift | 15 -------------- .../Sources/CategoryManager.swift | 1 + .../Sources/OptimobileShared | 1 - .../Sources/OptimoveNotificationService.swift | 1 + ...oveNotificationServiceExtensionTests.swift | 14 ------------- .../Classes/Optimobile/AnalyticsHelper.swift | 1 + .../Optimobile/InApp/InAppManager.swift | 1 + .../Optimobile/InApp/InAppPresenter.swift | 1 + .../Network/AuthorizationMediator.swift | 1 + .../Optimobile/Network/UrlBuilder.swift | 1 + .../Optimobile/Optimobile+Analytics.swift | 1 + .../Optimobile/Optimobile+DeepLinking.swift | 1 + .../Optimobile/Optimobile+Location.swift | 1 + .../Classes/Optimobile/Optimobile+Push.swift | 1 + .../Classes/Optimobile/Optimobile+Stats.swift | 1 + .../Classes/Optimobile/Optimobile.swift | 1 + .../Classes/Optimobile/OptimoveInApp.swift | 1 + .../Classes/Optimobile/SessionHelper.swift | 1 + OptimoveSDK/Sources/Classes/OptimobileShared | 1 - .../Sources/Classes/OptimoveConfig.swift | 1 + .../Sources/OptimoveConfigBuilderTests.swift | 1 + Package.swift | 15 ++++++++++++++ 34 files changed, 105 insertions(+), 64 deletions(-) rename {OptimobileShared => OptimobileCore/Sources}/AppGroupsHelper.swift (86%) create mode 100644 OptimobileCore/Sources/Credentials.swift rename {OptimobileShared => OptimobileCore/Sources}/Extensions/Notifications.swift (82%) rename {OptimobileShared => OptimobileCore/Sources}/KeyValPersistenceHelper.swift (86%) rename {OptimobileShared => OptimobileCore/Sources}/MediaHelper.swift (87%) rename {OptimobileShared => OptimobileCore/Sources}/OptimobileEvent.swift (95%) rename {OptimobileShared => OptimobileCore/Sources}/OptimobileHelper.swift (85%) rename {OptimobileShared => OptimobileCore/Sources}/OptimobileUserDefaultsKey.swift (95%) create mode 100644 OptimobileCore/Sources/PendingNotification.swift rename {OptimobileShared => OptimobileCore/Sources}/PendingNotificationHelper.swift (81%) create mode 100644 OptimobileCore/Tests/MediaHelperTests.swift delete mode 100644 OptimobileShared/Credentials.swift delete mode 100644 OptimobileShared/PendingNotification.swift delete mode 120000 OptimoveNotificationServiceExtension/Sources/OptimobileShared delete mode 120000 OptimoveSDK/Sources/Classes/OptimobileShared diff --git a/OptimobileShared/AppGroupsHelper.swift b/OptimobileCore/Sources/AppGroupsHelper.swift similarity index 86% rename from OptimobileShared/AppGroupsHelper.swift rename to OptimobileCore/Sources/AppGroupsHelper.swift index 521bbd66..24dbc808 100644 --- a/OptimobileShared/AppGroupsHelper.swift +++ b/OptimobileCore/Sources/AppGroupsHelper.swift @@ -6,14 +6,14 @@ public enum AppGroupConfig { public static var suffix: String = ".optimove" } -enum AppGroupsHelper { - static func isKumulosAppGroupDefined() -> Bool { +public enum AppGroupsHelper { + public static func isKumulosAppGroupDefined() -> Bool { let containerUrl = getSharedContainerPath() return containerUrl != nil } - static func getSharedContainerPath() -> URL? { + public static func getSharedContainerPath() -> URL? { return FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: getKumulosGroupName()) } diff --git a/OptimobileCore/Sources/Credentials.swift b/OptimobileCore/Sources/Credentials.swift new file mode 100644 index 00000000..6b6774c2 --- /dev/null +++ b/OptimobileCore/Sources/Credentials.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +public struct OptimobileCredentials: Codable { + public let apiKey: String + public let secretKey: String + + public init(apiKey: String, secretKey: String) { + self.apiKey = apiKey + self.secretKey = secretKey + } +} diff --git a/OptimobileShared/Extensions/Notifications.swift b/OptimobileCore/Sources/Extensions/Notifications.swift similarity index 82% rename from OptimobileShared/Extensions/Notifications.swift rename to OptimobileCore/Sources/Extensions/Notifications.swift index dfd89739..c0699e8f 100644 --- a/OptimobileShared/Extensions/Notifications.swift +++ b/OptimobileCore/Sources/Extensions/Notifications.swift @@ -2,6 +2,6 @@ import Foundation -extension Notification.Name { +public extension Notification.Name { static let optimobileInializationFinished = Notification.Name("optimobileInializationFinished") } diff --git a/OptimobileShared/KeyValPersistenceHelper.swift b/OptimobileCore/Sources/KeyValPersistenceHelper.swift similarity index 86% rename from OptimobileShared/KeyValPersistenceHelper.swift rename to OptimobileCore/Sources/KeyValPersistenceHelper.swift index fe1365d2..e3091138 100644 --- a/OptimobileShared/KeyValPersistenceHelper.swift +++ b/OptimobileCore/Sources/KeyValPersistenceHelper.swift @@ -8,8 +8,8 @@ protocol KeyValPersistent { static func removeObject(forKey: String) } -enum KeyValPersistenceHelper { - static func maybeMigrateUserDefaultsToAppGroups() { +public enum KeyValPersistenceHelper { + public static func maybeMigrateUserDefaultsToAppGroups() { let standardDefaults = UserDefaults.standard let haveMigratedKey: String = OptimobileUserDefaultsKey.MIGRATED_TO_GROUPS.rawValue if !AppGroupsHelper.isKumulosAppGroupDefined() { @@ -45,15 +45,15 @@ enum KeyValPersistenceHelper { } extension KeyValPersistenceHelper: KeyValPersistent { - static func set(_ value: Any?, forKey: String) { + public static func set(_ value: Any?, forKey: String) { getUserDefaults().set(value, forKey: forKey) } - static func object(forKey: String) -> Any? { + public static func object(forKey: String) -> Any? { return getUserDefaults().object(forKey: forKey) } - static func removeObject(forKey: String) { + public static func removeObject(forKey: String) { getUserDefaults().removeObject(forKey: forKey) } } diff --git a/OptimobileShared/MediaHelper.swift b/OptimobileCore/Sources/MediaHelper.swift similarity index 87% rename from OptimobileShared/MediaHelper.swift rename to OptimobileCore/Sources/MediaHelper.swift index bbffa4d2..f9a1e575 100644 --- a/OptimobileShared/MediaHelper.swift +++ b/OptimobileCore/Sources/MediaHelper.swift @@ -2,13 +2,13 @@ import Foundation -enum MediaHelper { +public enum MediaHelper { enum Error: LocalizedError { case noMediaUrlFound case invalidPictureUrl(String) } - static func getCompletePictureUrl(pictureUrlString: String, width: UInt) throws -> URL { + public static func getCompletePictureUrl(pictureUrlString: String, width: UInt) throws -> URL { if pictureUrlString.hasPrefix("https://") || pictureUrlString.hasPrefix("http://") { guard let url = URL(string: pictureUrlString) else { throw Error.invalidPictureUrl(pictureUrlString) diff --git a/OptimobileShared/OptimobileEvent.swift b/OptimobileCore/Sources/OptimobileEvent.swift similarity index 95% rename from OptimobileShared/OptimobileEvent.swift rename to OptimobileCore/Sources/OptimobileEvent.swift index 076455cf..77d3555e 100644 --- a/OptimobileShared/OptimobileEvent.swift +++ b/OptimobileCore/Sources/OptimobileEvent.swift @@ -1,6 +1,6 @@ // Copyright © 2023 Optimove. All rights reserved. -enum OptimobileEvent: String, Codable { +public enum OptimobileEvent: String, Codable { case DEEP_LINK_MATCHED = "k.deepLink.matched" case DEVICE_UNSUBSCRIBED = "k.push.deviceUnsubscribed" case ENGAGE_BEACON_ENTERED_PROXIMITY = "k.engage.beaconEnteredProximity" diff --git a/OptimobileShared/OptimobileHelper.swift b/OptimobileCore/Sources/OptimobileHelper.swift similarity index 85% rename from OptimobileShared/OptimobileHelper.swift rename to OptimobileCore/Sources/OptimobileHelper.swift index 11594019..7932235b 100644 --- a/OptimobileShared/OptimobileHelper.swift +++ b/OptimobileCore/Sources/OptimobileHelper.swift @@ -2,13 +2,13 @@ import Foundation -let KS_MESSAGE_TYPE_PUSH = 1 +public let KS_MESSAGE_TYPE_PUSH = 1 -enum OptimobileHelper { +public enum OptimobileHelper { private static let installIdLock = DispatchSemaphore(value: 1) - static let userIdLock = DispatchSemaphore(value: 1) + public static let userIdLock = DispatchSemaphore(value: 1) - static var installId: String { + public static var installId: String { installIdLock.wait() defer { installIdLock.signal() @@ -29,7 +29,7 @@ enum OptimobileHelper { If no user is associated, it returns the Kumulos installation ID */ - static var currentUserIdentifier: String { + public static var currentUserIdentifier: String { userIdLock.wait() defer { userIdLock.signal() } if let userId = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.USER_ID.rawValue) as! String? { @@ -39,7 +39,7 @@ enum OptimobileHelper { return OptimobileHelper.installId } - static func getBadgeFromUserInfo(userInfo: [AnyHashable: Any]) -> NSNumber? { + public static func getBadgeFromUserInfo(userInfo: [AnyHashable: Any]) -> NSNumber? { let custom = userInfo["custom"] as? [AnyHashable: Any] let aps = userInfo["aps"] as? [AnyHashable: Any] diff --git a/OptimobileShared/OptimobileUserDefaultsKey.swift b/OptimobileCore/Sources/OptimobileUserDefaultsKey.swift similarity index 95% rename from OptimobileShared/OptimobileUserDefaultsKey.swift rename to OptimobileCore/Sources/OptimobileUserDefaultsKey.swift index f26e7889..4c290b0a 100644 --- a/OptimobileShared/OptimobileUserDefaultsKey.swift +++ b/OptimobileCore/Sources/OptimobileUserDefaultsKey.swift @@ -2,7 +2,7 @@ import Foundation -enum OptimobileUserDefaultsKey: String { +public enum OptimobileUserDefaultsKey: String { case REGION = "KumulosEventsRegion" case MEDIA_BASE_URL = "KumulosMediaBaseUrl" case INSTALL_UUID = "KumulosUUID" diff --git a/OptimobileCore/Sources/PendingNotification.swift b/OptimobileCore/Sources/PendingNotification.swift new file mode 100644 index 00000000..4afa9cfe --- /dev/null +++ b/OptimobileCore/Sources/PendingNotification.swift @@ -0,0 +1,15 @@ +// Copyright © 2022 Optimove. All rights reserved. + +import Foundation + +public struct PendingNotification: Codable { + public let id: Int + public let deliveredAt: Date + public let identifier: String + + public init(id: Int, deliveredAt: Date = .init(), identifier: String) { + self.id = id + self.deliveredAt = deliveredAt + self.identifier = identifier + } +} diff --git a/OptimobileShared/PendingNotificationHelper.swift b/OptimobileCore/Sources/PendingNotificationHelper.swift similarity index 81% rename from OptimobileShared/PendingNotificationHelper.swift rename to OptimobileCore/Sources/PendingNotificationHelper.swift index dce70fe8..f55356bf 100644 --- a/OptimobileShared/PendingNotificationHelper.swift +++ b/OptimobileCore/Sources/PendingNotificationHelper.swift @@ -2,8 +2,8 @@ import Foundation -enum PendingNotificationHelper { - static func remove(id: Int) { +public enum PendingNotificationHelper { + public static func remove(id: Int) { var pendingNotifications = readAll() if let i = pendingNotifications.firstIndex(where: { $0.id == id }) { @@ -13,7 +13,7 @@ enum PendingNotificationHelper { } } - static func remove(identifier: String) { + public static func remove(identifier: String) { var pendingNotifications = readAll() if let i = pendingNotifications.firstIndex(where: { $0.identifier == identifier }) { @@ -23,7 +23,7 @@ enum PendingNotificationHelper { } } - static func readAll() -> [PendingNotification] { + public static func readAll() -> [PendingNotification] { var pendingNotifications = [PendingNotification]() if let data = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.PENDING_NOTIFICATIONS.rawValue), let decoded = try? JSONDecoder().decode([PendingNotification].self, from: data as! Data) @@ -34,7 +34,7 @@ enum PendingNotificationHelper { return pendingNotifications } - static func add(notification: PendingNotification) { + public static func add(notification: PendingNotification) { var pendingNotifications = readAll() if let _ = pendingNotifications.firstIndex(where: { $0.id == notification.id }) { @@ -46,7 +46,7 @@ enum PendingNotificationHelper { save(pendingNotifications: pendingNotifications) } - fileprivate static func save(pendingNotifications: [PendingNotification]) { + static func save(pendingNotifications: [PendingNotification]) { if let data = try? JSONEncoder().encode(pendingNotifications) { KeyValPersistenceHelper.set(data, forKey: OptimobileUserDefaultsKey.PENDING_NOTIFICATIONS.rawValue) } diff --git a/OptimobileCore/Tests/MediaHelperTests.swift b/OptimobileCore/Tests/MediaHelperTests.swift new file mode 100644 index 00000000..a4dcfbe8 --- /dev/null +++ b/OptimobileCore/Tests/MediaHelperTests.swift @@ -0,0 +1,20 @@ +// Copyright © 2023 Optimove. All rights reserved. + +@testable import OptimobileCore +import XCTest + +final class MediaHelperTests: XCTestCase { + func test_getCompletePictureUrl() throws { + let pictureUrlString = "https://www.optimove.com/wp-content/uploads/2018/12/optimove-logo.png" + let url = try MediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) + XCTAssertEqual(url.absoluteString, pictureUrlString) + } + + func test_getCompletePictureUrl_withMediaUrl() throws { + let pictureUrlString = "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg" + let mediaUrl = "https://www.optimove.com" + KeyValPersistenceHelper.set(mediaUrl, forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) + let url = try MediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) + XCTAssertEqual(url.absoluteString, "\(mediaUrl)/100x/\(pictureUrlString)") + } +} diff --git a/OptimobileShared/Credentials.swift b/OptimobileShared/Credentials.swift deleted file mode 100644 index 4e71a33c..00000000 --- a/OptimobileShared/Credentials.swift +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright © 2023 Optimove. All rights reserved. - -import Foundation - -struct OptimobileCredentials: Codable { - let apiKey: String - let secretKey: String -} diff --git a/OptimobileShared/PendingNotification.swift b/OptimobileShared/PendingNotification.swift deleted file mode 100644 index ffe747e1..00000000 --- a/OptimobileShared/PendingNotification.swift +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright © 2022 Optimove. All rights reserved. - -import Foundation - -struct PendingNotification: Codable { - let id: Int - let deliveredAt: Date - let identifier: String - - init(id: Int, deliveredAt: Date = .init(), identifier: String) { - self.id = id - self.deliveredAt = deliveredAt - self.identifier = identifier - } -} diff --git a/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift b/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift index 19db26c6..4c1354ee 100644 --- a/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift +++ b/OptimoveNotificationServiceExtension/Sources/CategoryManager.swift @@ -1,5 +1,6 @@ // Copyright © 2022 Optimove. All rights reserved. +import OptimobileCore import UserNotifications enum CategoryManager { diff --git a/OptimoveNotificationServiceExtension/Sources/OptimobileShared b/OptimoveNotificationServiceExtension/Sources/OptimobileShared deleted file mode 120000 index 75755936..00000000 --- a/OptimoveNotificationServiceExtension/Sources/OptimobileShared +++ /dev/null @@ -1 +0,0 @@ -../../OptimobileShared \ No newline at end of file diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 4235fcbd..7baac8c3 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore import UIKit import UserNotifications diff --git a/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift b/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift index b9ee619d..fb3d7d02 100644 --- a/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift +++ b/OptimoveNotificationServiceExtension/Tests/Sources/OptimoveNotificationServiceExtensionTests.swift @@ -35,18 +35,4 @@ final class OptimoveNotificationServiceExtensionTests: XCTestCase { XCTAssertNotNil(error) } } - - func test_getCompletePictureUrl() throws { - let pictureUrlString = "https://www.optimove.com/wp-content/uploads/2018/12/optimove-logo.png" - let url = try MediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) - XCTAssertEqual(url.absoluteString, pictureUrlString) - } - - func test_getCompletePictureUrl_withMediaUrl() throws { - let pictureUrlString = "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg" - let mediaUrl = "https://www.optimove.com" - KeyValPersistenceHelper.set(mediaUrl, forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) - let url = try MediaHelper.getCompletePictureUrl(pictureUrlString: pictureUrlString, width: 100) - XCTAssertEqual(url.absoluteString, "\(mediaUrl)/100x/\(pictureUrlString)") - } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift index 4817ca19..52726a6d 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/AnalyticsHelper.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import OptimobileCore class KSEventModel: NSManagedObject { @NSManaged var uuid: String diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift index 02f12b31..d2f54f56 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import OptimobileCore import UIKit public enum InAppMessagePresentationResult: String { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift index ff18a6cf..3cd6318b 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppPresenter.swift @@ -1,5 +1,6 @@ // Copyright © 2022 Optimove. All rights reserved. +import OptimobileCore import StoreKit import UIKit import UserNotifications diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift b/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift index c3fbbae2..8a795fa9 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Network/AuthorizationMediator.swift @@ -1,6 +1,7 @@ // Copyright © 2023 Optimove. All rights reserved. import Foundation +import OptimobileCore enum AuthorizationStrategy { case basic diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift b/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift index 503577a3..48a3d16a 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Network/UrlBuilder.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore public class UrlBuilder { enum Error: LocalizedError { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift index 9a78b74d..66ed0ead 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Analytics.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore extension Optimobile { static func trackEvent(eventType: OptimobileEvent, properties: [String: Any]?, immediateFlush: Bool = false) { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift index f47b984b..3c9d97f7 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+DeepLinking.swift @@ -2,6 +2,7 @@ import Foundation import UIKit +import OptimobileCore public struct DeepLinkContent { public let title: String? diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift index 5f26cf3d..ec954731 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Location.swift @@ -1,5 +1,6 @@ import CoreLocation import Foundation +import OptimobileCore extension Optimobile { static func sendLocationUpdate(location: CLLocation) { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index d5279f64..3cf3d3df 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -2,6 +2,7 @@ import Foundation import ObjectiveC.runtime +import OptimobileCore import UIKit import UserNotifications diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift index 19b0f52f..bd8deee3 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Stats.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore import OptimoveCore import UserNotifications diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift index 5f0b44c4..c5ff7c08 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore import UserNotifications public typealias InAppDeepLinkHandlerBlock = (InAppButtonPress) -> Void diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift index 7c5cc9d3..e4f5e331 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveInApp.swift @@ -2,6 +2,7 @@ import CoreData import Foundation +import OptimobileCore public class InAppInboxItem { public internal(set) var id: Int64 diff --git a/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift b/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift index d22fb931..983a48bd 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/SessionHelper.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore import UIKit class SessionIdleTimer { diff --git a/OptimoveSDK/Sources/Classes/OptimobileShared b/OptimoveSDK/Sources/Classes/OptimobileShared deleted file mode 120000 index f70d4ed1..00000000 --- a/OptimoveSDK/Sources/Classes/OptimobileShared +++ /dev/null @@ -1 +0,0 @@ -../../../OptimobileShared \ No newline at end of file diff --git a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift index 80d599c3..031ce28a 100644 --- a/OptimoveSDK/Sources/Classes/OptimoveConfig.swift +++ b/OptimoveSDK/Sources/Classes/OptimoveConfig.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore /// A set of options for configuring the SDK. /// - Note: The SDK can be configured to support multiple features. diff --git a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift index 80e418bf..6cf358ee 100644 --- a/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift +++ b/OptimoveSDK/Tests/Sources/OptimoveConfigBuilderTests.swift @@ -1,5 +1,6 @@ // Copyright © 2023 Optimove. All rights reserved. +import OptimobileCore @testable import OptimoveSDK import XCTest diff --git a/Package.swift b/Package.swift index 38d46b81..19419730 100644 --- a/Package.swift +++ b/Package.swift @@ -29,6 +29,7 @@ let package = Package( .target( name: "OptimoveSDK", dependencies: [ + "OptimobileCore", "OptimoveCore", ], path: "OptimoveSDK/Sources" @@ -37,8 +38,15 @@ let package = Package( name: "OptimoveCore", path: "OptimoveCore/Sources" ), + .target( + name: "OptimobileCore", + path: "OptimobileCore/Sources" + ), .target( name: "OptimoveNotificationServiceExtension", + dependencies: [ + "OptimobileCore", + ], path: "OptimoveNotificationServiceExtension/Sources" ), .target( @@ -75,6 +83,13 @@ let package = Package( .process("Resources"), ] ), + .testTarget( + name: "OptimobileCoreTests", + dependencies: [ + "OptimobileCore", + ], + path: "OptimobileCore/Tests" + ), .testTarget( name: "OptimoveNotificationServiceExtensionTests", dependencies: [ From 64ead6fda80801bea6cee1929ae7ff3cbc332e3e Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Thu, 7 Dec 2023 21:24:56 +0200 Subject: [PATCH 2/5] chore: move push notification to mobile core --- OptimobileCore/Sources/PushNotification.swift | 92 +++++++ .../Tests}/PushNotificationTests.swift | 18 +- .../Resources/notification-background.json | 0 .../Tests/Resources/notification-badge.json | 0 .../Tests/Resources/notification-buttons.json | 0 .../Resources/notification-deeplink.json | 0 .../Tests/Resources/notification-image.json | 0 .../Tests/Resources/notification-message.json | 0 .../Tests/Resources/notification-url.json | 19 ++ .../Sources/PushNotification.swift | 62 ----- .../Optimobile/InApp/InAppManager.swift | 10 +- .../Classes/Optimobile/Optimobile+Push.swift | 258 +++++++----------- ...timoveUserNotificationCenterDelegate.swift | 29 +- Package.swift | 11 +- 14 files changed, 261 insertions(+), 238 deletions(-) create mode 100644 OptimobileCore/Sources/PushNotification.swift rename {OptimoveNotificationServiceExtension/Tests/Sources => OptimobileCore/Tests}/PushNotificationTests.swift (79%) rename {OptimoveNotificationServiceExtension => OptimobileCore}/Tests/Resources/notification-background.json (100%) rename {OptimoveNotificationServiceExtension => OptimobileCore}/Tests/Resources/notification-badge.json (100%) rename {OptimoveNotificationServiceExtension => OptimobileCore}/Tests/Resources/notification-buttons.json (100%) rename {OptimoveNotificationServiceExtension => OptimobileCore}/Tests/Resources/notification-deeplink.json (100%) rename {OptimoveNotificationServiceExtension => OptimobileCore}/Tests/Resources/notification-image.json (100%) rename {OptimoveNotificationServiceExtension => OptimobileCore}/Tests/Resources/notification-message.json (100%) create mode 100644 OptimobileCore/Tests/Resources/notification-url.json delete mode 100644 OptimoveNotificationServiceExtension/Sources/PushNotification.swift diff --git a/OptimobileCore/Sources/PushNotification.swift b/OptimobileCore/Sources/PushNotification.swift new file mode 100644 index 00000000..37c4bcaa --- /dev/null +++ b/OptimobileCore/Sources/PushNotification.swift @@ -0,0 +1,92 @@ +// Copyright © 2023 Optimove. All rights reserved. + +import Foundation + +/// Represents a push notification received from the server. +public struct PushNotification: Decodable { + public struct Data: Decodable { + public let id: Int + + private enum CodingKeys: String, CodingKey { + case id + case data + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) + self.id = try data.decode(Int.self, forKey: CodingKeys.id) + } + } + + public struct Button: Decodable { + public struct Icon: Decodable { + public enum IconType: String, Decodable { + case custom + case system + } + + public let id: String + public let type: IconType + } + + public let id: String + public let icon: Icon? + public let text: String + } + + public let id: Int + public let deeplink: PushNotification.Data? + public let badge: Int? + public let buttons: [Button]? + public let isBackground: Bool + public let picturePath: String? + public let url: URL? + + private enum CodingKeys: String, CodingKey { + case a + case aps + case attachments + case badge = "badge_inc" + case buttons = "k.buttons" + case custom + case data + case deeplink = "k.deepLink" + case id + case isBackground = "content-available" + case message = "k.message" + case pictureUrl + case u + } + + public init(userInfo: [AnyHashable: Any]) throws { + let data = try JSONSerialization.data(withJSONObject: userInfo) + let decoder = JSONDecoder() + self = try decoder.decode(PushNotification.self, from: data) + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let custom = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .custom) + self.badge = try custom.decodeIfPresent(Int.self, forKey: .badge) + + let a = try custom.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) + self.buttons = try a.decodeIfPresent([Button].self, forKey: .buttons) + + let message = try a.nestedContainer(keyedBy: CodingKeys.self, forKey: .message) + let messageData = try message.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) + self.id = try messageData.decode(Int.self, forKey: .id) + + let aps = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .aps) + let isBackground = try aps.decodeIfPresent(Int.self, forKey: .isBackground) + self.isBackground = isBackground == 1 ? true : false + + let attachments = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: .attachments) + self.picturePath = try attachments?.decodeIfPresent(String.self, forKey: .pictureUrl) + + self.url = try custom.decodeIfPresent(URL.self, forKey: .u) + + self.deeplink = try a.decodeIfPresent(PushNotification.Data.self, forKey: .deeplink) + } +} diff --git a/OptimoveNotificationServiceExtension/Tests/Sources/PushNotificationTests.swift b/OptimobileCore/Tests/PushNotificationTests.swift similarity index 79% rename from OptimoveNotificationServiceExtension/Tests/Sources/PushNotificationTests.swift rename to OptimobileCore/Tests/PushNotificationTests.swift index 829a83bc..db862832 100644 --- a/OptimoveNotificationServiceExtension/Tests/Sources/PushNotificationTests.swift +++ b/OptimobileCore/Tests/PushNotificationTests.swift @@ -1,6 +1,6 @@ // Copyright © 2023 Optimove. All rights reserved. -@testable import OptimoveNotificationServiceExtension +import OptimobileCore import OptimoveTest import XCTest @@ -56,4 +56,20 @@ final class PushNotificationTests: XCTestCase, FileAccessible { XCTAssertEqual(notification.isBackground, true) } + + func test_decode_url() throws { + fileName = "notification-url.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.url?.absoluteString, "https://www.optimove.com") + } + + func test_decode_deeplink() throws { + fileName = "notification-deeplink.json" + let decoder = JSONDecoder() + let notification = try decoder.decode(PushNotification.self, from: data) + + XCTAssertEqual(notification.deeplink?.id, 1) + } } diff --git a/OptimoveNotificationServiceExtension/Tests/Resources/notification-background.json b/OptimobileCore/Tests/Resources/notification-background.json similarity index 100% rename from OptimoveNotificationServiceExtension/Tests/Resources/notification-background.json rename to OptimobileCore/Tests/Resources/notification-background.json diff --git a/OptimoveNotificationServiceExtension/Tests/Resources/notification-badge.json b/OptimobileCore/Tests/Resources/notification-badge.json similarity index 100% rename from OptimoveNotificationServiceExtension/Tests/Resources/notification-badge.json rename to OptimobileCore/Tests/Resources/notification-badge.json diff --git a/OptimoveNotificationServiceExtension/Tests/Resources/notification-buttons.json b/OptimobileCore/Tests/Resources/notification-buttons.json similarity index 100% rename from OptimoveNotificationServiceExtension/Tests/Resources/notification-buttons.json rename to OptimobileCore/Tests/Resources/notification-buttons.json diff --git a/OptimoveNotificationServiceExtension/Tests/Resources/notification-deeplink.json b/OptimobileCore/Tests/Resources/notification-deeplink.json similarity index 100% rename from OptimoveNotificationServiceExtension/Tests/Resources/notification-deeplink.json rename to OptimobileCore/Tests/Resources/notification-deeplink.json diff --git a/OptimoveNotificationServiceExtension/Tests/Resources/notification-image.json b/OptimobileCore/Tests/Resources/notification-image.json similarity index 100% rename from OptimoveNotificationServiceExtension/Tests/Resources/notification-image.json rename to OptimobileCore/Tests/Resources/notification-image.json diff --git a/OptimoveNotificationServiceExtension/Tests/Resources/notification-message.json b/OptimobileCore/Tests/Resources/notification-message.json similarity index 100% rename from OptimoveNotificationServiceExtension/Tests/Resources/notification-message.json rename to OptimobileCore/Tests/Resources/notification-message.json diff --git a/OptimobileCore/Tests/Resources/notification-url.json b/OptimobileCore/Tests/Resources/notification-url.json new file mode 100644 index 00000000..e57f0c19 --- /dev/null +++ b/OptimobileCore/Tests/Resources/notification-url.json @@ -0,0 +1,19 @@ +{ + "aps": { + "alert": { + "title": "title", + "body": "body" + } + }, + "custom": { + "u": "https://www.optimove.com", + "a": { + "k.message": { + "type": 1, + "data": { + "id": 1 + } + } + } + } +} diff --git a/OptimoveNotificationServiceExtension/Sources/PushNotification.swift b/OptimoveNotificationServiceExtension/Sources/PushNotification.swift deleted file mode 100644 index 19e5cfd4..00000000 --- a/OptimoveNotificationServiceExtension/Sources/PushNotification.swift +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright © 2023 Optimove. All rights reserved. - -/// Represents a push notification received from the server. -struct PushNotification: Decodable { - struct Button: Decodable { - struct Icon: Decodable { - enum IconType: String, Decodable { - case custom - case system - } - - let id: String - let type: IconType - } - - let id: String - let icon: Icon? - let text: String - } - - let id: Int - let badge: Int? - let buttons: [Button]? - let isBackground: Bool - let picturePath: String? - - private enum CodingKeys: String, CodingKey { - case a - case aps - case attachments - case badge = "badge_inc" - case buttons = "k.buttons" - case custom - case data - case deeplink = "k.deepLink" - case id - case isBackground = "content-available" - case message = "k.message" - case pictureUrl - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - let custom = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .custom) - self.badge = try custom.decodeIfPresent(Int.self, forKey: .badge) - - let a = try custom.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) - self.buttons = try a.decodeIfPresent([Button].self, forKey: .buttons) - - let message = try a.nestedContainer(keyedBy: CodingKeys.self, forKey: .message) - let data = try message.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) - self.id = try data.decode(Int.self, forKey: .id) - - let aps = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .aps) - let isBackground = try aps.decodeIfPresent(Int.self, forKey: .isBackground) - self.isBackground = isBackground == 1 ? true : false - - let attachments = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: .attachments) - self.picturePath = try attachments?.decodeIfPresent(String.self, forKey: .pictureUrl) - } -} diff --git a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift index d2f54f56..573331de 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/InApp/InAppManager.swift @@ -671,24 +671,20 @@ class InAppManager { } func handlePushOpen(notification: PushNotification) { - let deepLink: [AnyHashable: Any]? = notification.inAppDeepLink() - if !inAppEnabled() || deepLink == nil { + guard let deepLink = notification.deeplink, !inAppEnabled() else { return } DispatchQueue.global(qos: .default).async { - let data = deepLink!["data"] as! [AnyHashable: Any] - let inAppPartId: Int = data["id"] as! Int - objc_sync_enter(self.pendingTickleIds) defer { objc_sync_exit(self.pendingTickleIds) } - self.pendingTickleIds.add(inAppPartId) + self.pendingTickleIds.add(deepLink.id) let messagesToPresent = self.getMessagesToPresent([]) let tickleMessageFound = messagesToPresent.contains(where: { message -> Bool in - message.id == inAppPartId + message.id == deepLink.id }) if !tickleMessageFound { diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index 3cf3d3df..6856ebcb 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -6,79 +6,6 @@ import OptimobileCore import UIKit import UserNotifications -public class PushNotification: NSObject { - static let DeepLinkTypeInApp: Int = 1 - - public internal(set) var id: Int - public internal(set) var aps: [AnyHashable: Any] - public internal(set) var data: [AnyHashable: Any] - public internal(set) var url: URL? - public internal(set) var actionIdentifier: String? - - init(userInfo: [AnyHashable: Any]?) { - id = 0 - self.aps = [:] - self.data = [:] - - guard let userInfo = userInfo else { - return - } - - guard let aps = userInfo["aps"] as? [AnyHashable: Any] else { - return - } - - self.aps = aps - - guard let custom = userInfo["custom"] as? [AnyHashable: Any] else { - return - } - - guard let data = custom["a"] as? [AnyHashable: Any] else { - return - } - - self.data = data - - guard let msg = data["k.message"] as? [AnyHashable: Any] else { - return - } - - let msgData = msg["data"] as! [AnyHashable: Any] - - id = msgData["id"] as! Int - - if let urlStr = custom["u"] as? String { - url = URL(string: urlStr) - } else { - url = nil - } - } - - @available(iOS 10.0, *) - convenience init(userInfo: [AnyHashable: Any]?, response: UNNotificationResponse?) { - self.init(userInfo: userInfo) - - if let notificationResponse = response { - if notificationResponse.actionIdentifier != UNNotificationDefaultActionIdentifier { - actionIdentifier = notificationResponse.actionIdentifier - } - } - } - - public func inAppDeepLink() -> [AnyHashable: Any]? { - guard let deepLink = data["k.deepLink"] as? [AnyHashable: Any] else { - return nil - } - - if deepLink["type"] as? Int != PushNotification.DeepLinkTypeInApp { - return nil - } - - return deepLink - } -} - @available(iOS 10.0, *) public typealias OptimoveUNAuthorizationCheckedHandler = (UNAuthorizationStatus, Error?) -> Void @@ -209,35 +136,44 @@ extension Optimobile { - notification: The notification which triggered the action */ static func pushTrackOpen(notification: PushNotification) { - if notification.id == 0 { - Logger.warn(""" - Ignoring push notification open. - Reason: Invalid notification id (== 0). - Payload: \(notification). - """) - } let params = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.id] Optimobile.trackEvent(eventType: OptimobileEvent.MESSAGE_OPENED, properties: params) } static func pushTrackOpen(userInfo: [AnyHashable: Any]) { - let notification = PushNotification(userInfo: userInfo) - Optimobile.pushTrackOpen(notification: notification) + do { + let notification = try PushNotification(userInfo: userInfo) + Optimobile.pushTrackOpen(notification: notification) + } catch { + Logger.error( + """ + Ignoring push notification open. + Reason: Invalid notification payload. + Payload: \(userInfo). + Error: \(error.localizedDescription). + """ + ) + } } @available(iOS 10.0, *) - func pushHandleOpen(withUserInfo: [AnyHashable: Any]?, response: UNNotificationResponse?) -> Bool { - let notification = PushNotification(userInfo: withUserInfo, response: response) - - if notification.id == 0 { + func pushHandleOpen(withUserInfo userInfo: [AnyHashable: Any]) -> Bool { + do { + let notification = try PushNotification(userInfo: userInfo) + pushHandleOpen(notification: notification) + PendingNotificationHelper.remove(id: notification.id) + return true + } catch { + Logger.error( + """ + Ignoring push notification open. + Reason: Invalid notification payload. + Payload: \(userInfo). + Error: \(error.localizedDescription). + """ + ) return false } - - pushHandleOpen(notification: notification) - - PendingNotificationHelper.remove(id: notification.id) - - return true } private func pushHandleOpen(notification: PushNotification) { @@ -269,16 +205,24 @@ extension Optimobile { // MARK: Dismissed handling @available(iOS 10.0, *) - func pushHandleDismissed(withUserInfo: [AnyHashable: Any]?, response: UNNotificationResponse?) -> Bool { - let notification = PushNotification(userInfo: withUserInfo, response: response) - - if notification.id == 0 { + func pushHandleDismissed(withUserInfo userInfo: [AnyHashable: Any]) -> Bool { + do { + let data = try JSONSerialization.data(withJSONObject: userInfo) + let notification = try JSONDecoder().decode(PushNotification.self, from: data) + pushHandleDismissed(notificationId: notification.id) + PendingNotificationHelper.remove(id: notification.id) + return true + } catch { + Logger.error( + """ + Ignoring push notification dismissed. + Reason: Invalid notification payload. + Payload: \(userInfo). + Error: \(error.localizedDescription). + """ + ) return false } - - pushHandleDismissed(notificationId: notification.id) - - return true } @available(iOS 10.0, *) @@ -303,23 +247,24 @@ extension Optimobile { if !AppGroupsHelper.isKumulosAppGroupDefined() { return } - - UNUserNotificationCenter.current().getDeliveredNotifications { (notifications: [UNNotification]) in - var actualPendingNotificationIds: [Int] = [] - for notification in notifications { - let notification = PushNotification(userInfo: notification.request.content.userInfo) - if notification.id == 0 { - continue + Task { + do { + let notifications = await UNUserNotificationCenter.current().deliveredNotifications() + var actualPendingNotificationIds: [Int] = [] + for notification in notifications { + let notification = try PushNotification(userInfo: notification.request.content.userInfo) + + actualPendingNotificationIds.append(notification.id) } - actualPendingNotificationIds.append(notification.id) - } + let recordedPendingNotifications = PendingNotificationHelper.readAll() - let recordedPendingNotifications = PendingNotificationHelper.readAll() - - let deletions = recordedPendingNotifications.filter { !actualPendingNotificationIds.contains($0.id) } - for deletion in deletions { - self.pushHandleDismissed(notificationId: deletion.id, dismissedAt: deletion.deliveredAt) + let deletions = recordedPendingNotifications.filter { !actualPendingNotificationIds.contains($0.id) } + for deletion in deletions { + pushHandleDismissed(notificationId: deletion.id, dismissedAt: deletion.deliveredAt) + } + } catch { + Logger.error("Failed to track push dismissed events: \(error.localizedDescription)") } } } @@ -400,58 +345,63 @@ class PushHelper { let didReceiveSelector = #selector(UIApplicationDelegate.application(_:didReceiveRemoteNotification:fetchCompletionHandler:)) let receiveType = NSString(string: "v@:@@@?").utf8String let didReceive: didReceiveBlock = { (obj: Any, _ application: UIApplication, userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) in - let notification = PushNotification(userInfo: userInfo) - let hasInApp = notification.inAppDeepLink() != nil + do { + let notification = try PushNotification(userInfo: userInfo) + let hasInApp = notification.deeplink != nil - self.setBadge(userInfo: userInfo) - self.trackPushDelivery(notification: notification) + self.setBadge(userInfo: userInfo) + self.trackPushDelivery(notification: notification) - if existingDidReceive == nil, !hasInApp { - // Nothing to do - completionHandler(.noData) - return - } else if existingDidReceive != nil, !hasInApp { - // Only existing delegate work to do - unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, completionHandler) - return - } + if existingDidReceive == nil, !hasInApp { + // Nothing to do + completionHandler(.noData) + return + } else if existingDidReceive != nil, !hasInApp { + // Only existing delegate work to do + unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, completionHandler) + return + } - var fetchResult: UIBackgroundFetchResult = .noData - let group = DispatchGroup() + var fetchResult: UIBackgroundFetchResult = .noData + let group = DispatchGroup() - if existingDidReceive != nil { - group.enter() - DispatchQueue.main.async { - unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, { (result: UIBackgroundFetchResult) in + if existingDidReceive != nil { + group.enter() + DispatchQueue.main.async { + unsafeBitCast(existingDidReceive, to: kumulos_applicationDidReceiveRemoteNotificationFetchCompletionHandler.self)(obj, didReceiveSelector, application, userInfo, { (result: UIBackgroundFetchResult) in + DispatchQueue.main.async { + if fetchResult == .noData { + fetchResult = result + } + + group.leave() + } + }) + } + } + + if hasInApp { + group.enter() + Optimobile.sharedInstance.inAppManager.sync { (result: Int) in DispatchQueue.main.async { - if fetchResult == .noData { - fetchResult = result + if result < 0 { + fetchResult = .failed + } else if result > 0 { + fetchResult = .newData } + // No data case is default, allow override from other handler group.leave() } - }) - } - } - - if hasInApp { - group.enter() - Optimobile.sharedInstance.inAppManager.sync { (result: Int) in - DispatchQueue.main.async { - if result < 0 { - fetchResult = .failed - } else if result > 0 { - fetchResult = .newData - } - // No data case is default, allow override from other handler - - group.leave() } } - } - group.notify(queue: .main) { - completionHandler(fetchResult) + group.notify(queue: .main) { + completionHandler(fetchResult) + } + } catch { + Logger.error("Failed to parse push notification: \(error.localizedDescription)") + completionHandler(.failed) } } let kumulosDidReceive = imp_implementationWithBlock(unsafeBitCast(didReceive, to: AnyObject.self)) diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift index fb87420f..cf75a93d 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Optimove. All rights reserved. import Foundation +import OptimobileCore import UserNotifications @available(iOS 10.0, *) @@ -12,19 +13,23 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - let push = PushNotification(userInfo: notification.request.content.userInfo, response: nil) + do { + let push = try PushNotification(userInfo: notification.request.content.userInfo) - if push.id == 0 { - chainCenter(center, willPresent: notification, with: completionHandler) - return - } + if push.id == 0 { + chainCenter(center, willPresent: notification, with: completionHandler) + return + } - if Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock == nil { - completionHandler(.alert) - return - } + if Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock == nil { + completionHandler(.alert) + return + } - Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock?(push, completionHandler) + Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock?(push, completionHandler) + } catch { + chainCenter(center, willPresent: notification, with: completionHandler) + } } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { @@ -36,7 +41,7 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter } if response.actionIdentifier == UNNotificationDismissActionIdentifier { - let handled = Optimobile.sharedInstance.pushHandleDismissed(withUserInfo: userInfo, response: response) + let handled = Optimobile.sharedInstance.pushHandleDismissed(withUserInfo: userInfo) if !handled { chainCenter(center, didReceive: response, with: completionHandler) return @@ -46,7 +51,7 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter return } - let handled = Optimobile.sharedInstance.pushHandleOpen(withUserInfo: userInfo, response: response) + let handled = Optimobile.sharedInstance.pushHandleOpen(withUserInfo: userInfo) if !handled { chainCenter(center, didReceive: response, with: completionHandler) diff --git a/Package.swift b/Package.swift index 19419730..fa4253a0 100644 --- a/Package.swift +++ b/Package.swift @@ -40,7 +40,10 @@ let package = Package( ), .target( name: "OptimobileCore", - path: "OptimobileCore/Sources" + path: "OptimobileCore/Sources", + resources: [ + .process("Resources"), + ] ), .target( name: "OptimoveNotificationServiceExtension", @@ -87,8 +90,12 @@ let package = Package( name: "OptimobileCoreTests", dependencies: [ "OptimobileCore", + "OptimoveTest", ], - path: "OptimobileCore/Tests" + path: "OptimobileCore/Tests", + resources: [ + .process("Resources"), + ] ), .testTarget( name: "OptimoveNotificationServiceExtensionTests", From d6e697fd59b70d270a26e38a36901905c60db54d Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 11:44:19 +0200 Subject: [PATCH 3/5] chore: keep message struct in notification --- OptimobileCore/Sources/PushNotification.swift | 10 +++------- OptimobileCore/Tests/PushNotificationTests.swift | 4 ++-- .../Sources/OptimoveNotificationService.swift | 4 ++-- .../Classes/Optimobile/Optimobile+Push.swift | 16 ++++++---------- .../OptimoveUserNotificationCenterDelegate.swift | 5 ----- 5 files changed, 13 insertions(+), 26 deletions(-) diff --git a/OptimobileCore/Sources/PushNotification.swift b/OptimobileCore/Sources/PushNotification.swift index 37c4bcaa..d0d0e8ea 100644 --- a/OptimobileCore/Sources/PushNotification.swift +++ b/OptimobileCore/Sources/PushNotification.swift @@ -35,8 +35,8 @@ public struct PushNotification: Decodable { public let text: String } - public let id: Int public let deeplink: PushNotification.Data? + public let message: PushNotification.Data public let badge: Int? public let buttons: [Button]? public let isBackground: Bool @@ -73,10 +73,8 @@ public struct PushNotification: Decodable { let a = try custom.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) self.buttons = try a.decodeIfPresent([Button].self, forKey: .buttons) - - let message = try a.nestedContainer(keyedBy: CodingKeys.self, forKey: .message) - let messageData = try message.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) - self.id = try messageData.decode(Int.self, forKey: .id) + self.deeplink = try a.decodeIfPresent(PushNotification.Data.self, forKey: .deeplink) + self.message = try a.decode(PushNotification.Data.self, forKey: .message) let aps = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .aps) let isBackground = try aps.decodeIfPresent(Int.self, forKey: .isBackground) @@ -86,7 +84,5 @@ public struct PushNotification: Decodable { self.picturePath = try attachments?.decodeIfPresent(String.self, forKey: .pictureUrl) self.url = try custom.decodeIfPresent(URL.self, forKey: .u) - - self.deeplink = try a.decodeIfPresent(PushNotification.Data.self, forKey: .deeplink) } } diff --git a/OptimobileCore/Tests/PushNotificationTests.swift b/OptimobileCore/Tests/PushNotificationTests.swift index db862832..9d158895 100644 --- a/OptimobileCore/Tests/PushNotificationTests.swift +++ b/OptimobileCore/Tests/PushNotificationTests.swift @@ -7,11 +7,11 @@ import XCTest final class PushNotificationTests: XCTestCase, FileAccessible { var fileName: String = "" - func test_decode_id() throws { + func test_decode_message() throws { fileName = "notification-message.json" let decoder = JSONDecoder() let notification = try decoder.decode(PushNotification.self, from: data) - XCTAssertEqual(notification.id, 1) + XCTAssertEqual(notification.message.id, 1) } func test_decode_badge() throws { diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index 7baac8c3..e3383170 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -46,7 +46,7 @@ public enum OptimoveNotificationService { maybeSetBadge(bestAttemptContent: bestAttemptContent, userInfo: userInfo) PendingNotificationHelper.add( notification: PendingNotification( - id: notification.id, + id: notification.message.id, identifier: request.identifier ) ) @@ -86,7 +86,7 @@ public enum OptimoveNotificationService { } static func buildCategory(notification: PushNotification) async -> String { - let categoryIdentifier = CategoryManager.getCategoryId(messageId: notification.id) + let categoryIdentifier = CategoryManager.getCategoryId(messageId: notification.message.id) let category = UNNotificationCategory( identifier: categoryIdentifier, actions: buildActions(notification: notification), diff --git a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift index 6856ebcb..7c5b5fc4 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/Optimobile+Push.swift @@ -136,7 +136,7 @@ extension Optimobile { - notification: The notification which triggered the action */ static func pushTrackOpen(notification: PushNotification) { - let params = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.id] + let params = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.message.id] Optimobile.trackEvent(eventType: OptimobileEvent.MESSAGE_OPENED, properties: params) } @@ -161,7 +161,7 @@ extension Optimobile { do { let notification = try PushNotification(userInfo: userInfo) pushHandleOpen(notification: notification) - PendingNotificationHelper.remove(id: notification.id) + PendingNotificationHelper.remove(id: notification.message.id) return true } catch { Logger.error( @@ -209,8 +209,8 @@ extension Optimobile { do { let data = try JSONSerialization.data(withJSONObject: userInfo) let notification = try JSONDecoder().decode(PushNotification.self, from: data) - pushHandleDismissed(notificationId: notification.id) - PendingNotificationHelper.remove(id: notification.id) + pushHandleDismissed(notificationId: notification.message.id) + PendingNotificationHelper.remove(id: notification.message.id) return true } catch { Logger.error( @@ -254,7 +254,7 @@ extension Optimobile { for notification in notifications { let notification = try PushNotification(userInfo: notification.request.content.userInfo) - actualPendingNotificationIds.append(notification.id) + actualPendingNotificationIds.append(notification.message.id) } let recordedPendingNotifications = PendingNotificationHelper.readAll() @@ -422,11 +422,7 @@ class PushHelper { } private func trackPushDelivery(notification: PushNotification) { - if notification.id == 0 { - return - } - - let props: [String: Any] = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.id] + let props: [String: Any] = ["type": KS_MESSAGE_TYPE_PUSH, "id": notification.message.id] Optimobile.trackEvent(eventType: OptimobileEvent.MESSAGE_DELIVERED, properties: props, immediateFlush: true) } } diff --git a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift index cf75a93d..101f90d9 100644 --- a/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift +++ b/OptimoveSDK/Sources/Classes/Optimobile/OptimoveUserNotificationCenterDelegate.swift @@ -16,11 +16,6 @@ class OptimoveUserNotificationCenterDelegate: NSObject, UNUserNotificationCenter do { let push = try PushNotification(userInfo: notification.request.content.userInfo) - if push.id == 0 { - chainCenter(center, willPresent: notification, with: completionHandler) - return - } - if Optimobile.sharedInstance.config.pushReceivedInForegroundHandlerBlock == nil { completionHandler(.alert) return From b67069eb9cca50c2e3d6ff2d4557ace6e52a62cc Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 11:50:12 +0200 Subject: [PATCH 4/5] chore: wrap attachments --- OptimobileCore/Sources/PushNotification.swift | 17 ++++++++--------- .../Tests/PushNotificationTests.swift | 2 +- .../Sources/OptimoveNotificationService.swift | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/OptimobileCore/Sources/PushNotification.swift b/OptimobileCore/Sources/PushNotification.swift index d0d0e8ea..fe124e29 100644 --- a/OptimobileCore/Sources/PushNotification.swift +++ b/OptimobileCore/Sources/PushNotification.swift @@ -19,6 +19,10 @@ public struct PushNotification: Decodable { } } + public struct Attachment: Decodable { + public let pictureUrl: String? + } + public struct Button: Decodable { public struct Icon: Decodable { public enum IconType: String, Decodable { @@ -35,12 +39,12 @@ public struct PushNotification: Decodable { public let text: String } + public let attachment: PushNotification.Attachment? + public let badge: Int? + public let buttons: [PushNotification.Button]? public let deeplink: PushNotification.Data? public let message: PushNotification.Data - public let badge: Int? - public let buttons: [Button]? public let isBackground: Bool - public let picturePath: String? public let url: URL? private enum CodingKeys: String, CodingKey { @@ -50,12 +54,9 @@ public struct PushNotification: Decodable { case badge = "badge_inc" case buttons = "k.buttons" case custom - case data case deeplink = "k.deepLink" - case id case isBackground = "content-available" case message = "k.message" - case pictureUrl case u } @@ -69,6 +70,7 @@ public struct PushNotification: Decodable { let container = try decoder.container(keyedBy: CodingKeys.self) let custom = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .custom) + self.attachment = try container.decodeIfPresent(Attachment.self, forKey: .attachments) self.badge = try custom.decodeIfPresent(Int.self, forKey: .badge) let a = try custom.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) @@ -80,9 +82,6 @@ public struct PushNotification: Decodable { let isBackground = try aps.decodeIfPresent(Int.self, forKey: .isBackground) self.isBackground = isBackground == 1 ? true : false - let attachments = try? container.nestedContainer(keyedBy: CodingKeys.self, forKey: .attachments) - self.picturePath = try attachments?.decodeIfPresent(String.self, forKey: .pictureUrl) - self.url = try custom.decodeIfPresent(URL.self, forKey: .u) } } diff --git a/OptimobileCore/Tests/PushNotificationTests.swift b/OptimobileCore/Tests/PushNotificationTests.swift index 9d158895..c486b034 100644 --- a/OptimobileCore/Tests/PushNotificationTests.swift +++ b/OptimobileCore/Tests/PushNotificationTests.swift @@ -46,7 +46,7 @@ final class PushNotificationTests: XCTestCase, FileAccessible { let decoder = JSONDecoder() let notification = try decoder.decode(PushNotification.self, from: data) - XCTAssertEqual(notification.picturePath, "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg") + XCTAssertEqual(notification.attachment?.pictureUrl, "B04wM4Y7/b2f69e254879d69b58c7418468213762.jpeg") } func test_decode_background() throws { diff --git a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift index e3383170..10a13472 100644 --- a/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift +++ b/OptimoveNotificationServiceExtension/Sources/OptimoveNotificationService.swift @@ -99,7 +99,7 @@ public enum OptimoveNotificationService { } static func maybeGetAttachment(notification: PushNotification) async throws -> UNNotificationAttachment? { - guard let picturePath = notification.picturePath else { return nil } + guard let picturePath = notification.attachment?.pictureUrl else { return nil } let url = try await MediaHelper.getCompletePictureUrl( pictureUrlString: picturePath, From 7cd93c38c2d0df7cca8faf0cf53b40cdd746f6c7 Mon Sep 17 00:00:00 2001 From: Eli Gutovsky Date: Fri, 8 Dec 2023 12:08:03 +0200 Subject: [PATCH 5/5] refactor: notification struct --- OptimobileCore/Sources/PushNotification.swift | 60 ++++++++++++++----- .../Tests/PushNotificationTests.swift | 2 +- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/OptimobileCore/Sources/PushNotification.swift b/OptimobileCore/Sources/PushNotification.swift index fe124e29..39d5a366 100644 --- a/OptimobileCore/Sources/PushNotification.swift +++ b/OptimobileCore/Sources/PushNotification.swift @@ -4,18 +4,37 @@ import Foundation /// Represents a push notification received from the server. public struct PushNotification: Decodable { - public struct Data: Decodable { - public let id: Int + public struct Aps: Decodable { + public struct Alert: Decodable { + public let title: String? + public let body: String? + } + + public let alert: Alert? + public let badge: Int? + public let sound: String? + /// The background notification flag. To perform a silent background update, specify the value 1 and don’t include the alert, badge, or sound keys in your payload. If this key is present with a value of 1, the system attempts to initialize your app in the background so that it can make updates to its user interface. If the app is already running in the foreground, this key has no effect. + public let isBackground: Bool + /// The notification service app extension flag. If the value is 1, the system passes the notification to your notification service app extension before delivery. Use your extension to modify the notification’s content. + public let isExtension: Bool private enum CodingKeys: String, CodingKey { - case id - case data + case alert + case badge + case sound + case isBackground = "content-available" + case isExtension = "mutable-content" } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) - self.id = try data.decode(Int.self, forKey: CodingKeys.id) + self.alert = try container.decodeIfPresent(Alert.self, forKey: .alert) + self.badge = try container.decodeIfPresent(Int.self, forKey: .badge) + self.sound = try container.decodeIfPresent(String.self, forKey: .sound) + let isBackground = try container.decodeIfPresent(Int.self, forKey: .isBackground) + self.isBackground = isBackground == 1 + let isExtension = try container.decodeIfPresent(Int.self, forKey: .isExtension) + self.isExtension = isExtension == 1 } } @@ -39,12 +58,28 @@ public struct PushNotification: Decodable { public let text: String } + public struct Data: Decodable { + public let id: Int + + private enum CodingKeys: String, CodingKey { + case id + case data + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) + self.id = try data.decode(Int.self, forKey: CodingKeys.id) + } + } + + public let aps: Aps public let attachment: PushNotification.Attachment? + /// Optimove badge public let badge: Int? public let buttons: [PushNotification.Button]? public let deeplink: PushNotification.Data? public let message: PushNotification.Data - public let isBackground: Bool public let url: URL? private enum CodingKeys: String, CodingKey { @@ -55,7 +90,6 @@ public struct PushNotification: Decodable { case buttons = "k.buttons" case custom case deeplink = "k.deepLink" - case isBackground = "content-available" case message = "k.message" case u } @@ -68,20 +102,16 @@ public struct PushNotification: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.aps = try container.decode(Aps.self, forKey: .aps) + self.attachment = try container.decodeIfPresent(Attachment.self, forKey: .attachments) let custom = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .custom) - self.attachment = try container.decodeIfPresent(Attachment.self, forKey: .attachments) self.badge = try custom.decodeIfPresent(Int.self, forKey: .badge) + self.url = try custom.decodeIfPresent(URL.self, forKey: .u) let a = try custom.nestedContainer(keyedBy: CodingKeys.self, forKey: .a) self.buttons = try a.decodeIfPresent([Button].self, forKey: .buttons) self.deeplink = try a.decodeIfPresent(PushNotification.Data.self, forKey: .deeplink) self.message = try a.decode(PushNotification.Data.self, forKey: .message) - - let aps = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .aps) - let isBackground = try aps.decodeIfPresent(Int.self, forKey: .isBackground) - self.isBackground = isBackground == 1 ? true : false - - self.url = try custom.decodeIfPresent(URL.self, forKey: .u) } } diff --git a/OptimobileCore/Tests/PushNotificationTests.swift b/OptimobileCore/Tests/PushNotificationTests.swift index c486b034..c8d068e5 100644 --- a/OptimobileCore/Tests/PushNotificationTests.swift +++ b/OptimobileCore/Tests/PushNotificationTests.swift @@ -54,7 +54,7 @@ final class PushNotificationTests: XCTestCase, FileAccessible { let decoder = JSONDecoder() let notification = try decoder.decode(PushNotification.self, from: data) - XCTAssertEqual(notification.isBackground, true) + XCTAssertEqual(notification.aps.isBackground, true) } func test_decode_url() throws {