From d272d2c552dd9576b92f1b68e46108201ab451f2 Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 15:28:51 -0600 Subject: [PATCH 01/33] Change FPTIBatchData structure --- Sources/PopupBridge/FPTIBatchData.swift | 169 ++++++++++++++---------- 1 file changed, 101 insertions(+), 68 deletions(-) diff --git a/Sources/PopupBridge/FPTIBatchData.swift b/Sources/PopupBridge/FPTIBatchData.swift index 69943f4..418cc5c 100644 --- a/Sources/PopupBridge/FPTIBatchData.swift +++ b/Sources/PopupBridge/FPTIBatchData.swift @@ -2,77 +2,110 @@ import UIKit struct FPTIBatchData: Codable { - let appID: String = Bundle.main.infoDictionary?[kCFBundleIdentifierKey as String] as? String ?? "N/A" - - let appName: String = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "N/A" - - let clientSDKVersion: String = Bundle.clientSDKVersion - - let clientOS: String = UIDevice.current.systemName + " " + UIDevice.current.systemVersion - - let component: String = "popupbridgesdk" - - let deviceManufacturer: String = "Apple" + let events: [EventsContainer] + + init(metadata: Metadata, events fptiEvents: [Event]) { + self.events = [ + EventsContainer( + metadata: metadata, + fptiEvents: fptiEvents + ) + ] + } - let deviceModel: String = { - var systemInfo = utsname() - uname(&systemInfo) - let machineMirror = Mirror(reflecting: systemInfo.machine) - let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } - return identifier + String(UnicodeScalar(UInt8(value))) + struct EventsContainer: Codable { + + let metadata: Metadata + let fptiEvents: [Event] + + enum CodingKeys: String, CodingKey { + case metadata = "batch_params" + case fptiEvents = "event_params" } - return identifier - }() - - let eventName: String - - let eventSource: String = "mobile-native" - - let isSimulator: Bool = { - #if targetEnvironment(simulator) - true - #else - false - #endif - }() - - let merchantAppVersion: String = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? "N/A" - - let packageManager: String = { - #if COCOAPODS - "CocoaPods" - #elseif SWIFT_PACKAGE - "Swift Package Manager" - #else - "Carthage or Other" - #endif - }() - - let platform: String = "iOS" - - let sessionID: String - - let timestamp: String = String(Date().utcTimestampMilliseconds) + } - let tenantName: String = "Braintree" + /// Encapsulates a single event by it's name and timestamp. + struct Event: Codable { + + let eventName: String + + let timestamp: String = String(Date().utcTimestampMilliseconds) + + let tenantName: String = "Braintree" + + enum CodingKeys: String, CodingKey { + case eventName = "event_name" + case timestamp = "t" + case tenantName = "tenant_name" + } + } - enum CodingKeys: String, CodingKey { - case appID = "app_id" - case appName = "app_name" - case clientSDKVersion = "c_sdk_ver" - case clientOS = "client_os" - case component = "comp" - case deviceManufacturer = "device_manufacturer" - case deviceModel = "mobile_device_model" - case eventName = "event_name" - case eventSource = "event_source" - case isSimulator = "is_simulator" - case merchantAppVersion = "mapv" - case packageManager = "ios_package_manager" - case platform - case sessionID = "session_id" - case timestamp = "t" - case tenantName = "tenant_name" + /// The FPTI tags/ metadata applicable to all events in the batch upload. + struct Metadata: Codable { + + let appID: String = Bundle.main.infoDictionary?[kCFBundleIdentifierKey as String] as? String ?? "N/A" + + let appName: String = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "N/A" + + let clientSDKVersion: String = Bundle.clientSDKVersion + + let clientOS: String = UIDevice.current.systemName + " " + UIDevice.current.systemVersion + + let component: String = "popupbridgesdk" + + let deviceManufacturer: String = "Apple" + + let deviceModel: String = { + var systemInfo = utsname() + uname(&systemInfo) + let machineMirror = Mirror(reflecting: systemInfo.machine) + let identifier = machineMirror.children.reduce("") { identifier, element in + guard let value = element.value as? Int8, value != 0 else { return identifier } + return identifier + String(UnicodeScalar(UInt8(value))) + } + return identifier + }() + + let eventSource: String = "mobile-native" + + let isSimulator: Bool = { + #if targetEnvironment(simulator) + true + #else + false + #endif + }() + + let merchantAppVersion: String = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as? String ?? "N/A" + + let packageManager: String = { + #if COCOAPODS + "CocoaPods" + #elseif SWIFT_PACKAGE + "Swift Package Manager" + #else + "Carthage or Other" + #endif + }() + + let platform: String = "iOS" + + let sessionID: String + + enum CodingKeys: String, CodingKey { + case appID = "app_id" + case appName = "app_name" + case clientSDKVersion = "c_sdk_ver" + case clientOS = "client_os" + case component = "comp" + case deviceManufacturer = "device_manufacturer" + case deviceModel = "mobile_device_model" + case eventSource = "event_source" + case isSimulator = "is_simulator" + case merchantAppVersion = "mapv" + case packageManager = "ios_package_manager" + case platform + case sessionID = "session_id" + } } } From 304190d0301900ff55c2b3703f7eed02bf46264a Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 15:35:16 -0600 Subject: [PATCH 02/33] Add NetworkClient and AnalyticsServices files --- PopupBridge.xcodeproj/project.pbxproj | 8 ++++++++ Sources/PopupBridge/AnalyticsService.swift | 1 + Sources/PopupBridge/NetworkClient.swift | 1 + 3 files changed, 10 insertions(+) create mode 100644 Sources/PopupBridge/AnalyticsService.swift create mode 100644 Sources/PopupBridge/NetworkClient.swift diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 4e847fc..63c1298 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -7,10 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 454C7EC32D6E351F00CA3191 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C7EC22D6E351900CA3191 /* NetworkClient.swift */; }; 45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */; }; 45CD0C2E2D664D140072C5A4 /* Date+MilisecondTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */; }; 45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */; }; 45CD0C322D6793FB0072C5A4 /* PopupBridgeAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */; }; + 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; }; 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */; }; 79DB9F7F53319F206CDE119E /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */; }; 800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800A09D72995F143003ED16E /* POPPopupBridge.swift */; }; @@ -30,10 +32,12 @@ /* Begin PBXFileReference section */ 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 454C7EC22D6E351900CA3191 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+MilisecondTimestamp.swift"; sourceTree = ""; }; 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = ""; }; 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupBridgeAnalytics.swift; sourceTree = ""; }; + 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 4EF7C7DDAB0B99FF28DD6541 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -163,9 +167,11 @@ A775A08C1DEE4EF0009E67C2 /* PopupBridge */ = { isa = PBXGroup; children = ( + 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */, 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */, 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */, 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */, + 454C7EC22D6E351900CA3191 /* NetworkClient.swift */, 800A09D72995F143003ED16E /* POPPopupBridge.swift */, A79330F01DF0F98F00EE479D /* PopupBridge-Framework-Info.plist */, 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */, @@ -330,7 +336,9 @@ files = ( 800E789F29E09A2A00D1B0FC /* URLDetailsPayload.swift in Sources */, 45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */, + 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */, BEF9ED232A2A6A2C005D54AB /* PopupBridgeConstants.swift in Sources */, + 454C7EC32D6E351F00CA3191 /* NetworkClient.swift in Sources */, 800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */, BE8E37B62A17B79E00181FDA /* WebAuthenticationSession.swift in Sources */, 45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */, diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/AnalyticsService.swift new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sources/PopupBridge/AnalyticsService.swift @@ -0,0 +1 @@ + diff --git a/Sources/PopupBridge/NetworkClient.swift b/Sources/PopupBridge/NetworkClient.swift new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sources/PopupBridge/NetworkClient.swift @@ -0,0 +1 @@ + From 03f936521c8276ce5bdf79847062d2fea4d85e95 Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 22:01:52 -0600 Subject: [PATCH 03/33] Add Sessionable and NetworkError files --- PopupBridge.xcodeproj/project.pbxproj | 8 ++++++++ Sources/PopupBridge/NetworkError.swift | 1 + 2 files changed, 9 insertions(+) create mode 100644 Sources/PopupBridge/NetworkError.swift diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 63c1298..7870281 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */; }; 45CD0C322D6793FB0072C5A4 /* PopupBridgeAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */; }; 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; }; + 45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF282D701AF7000D550B /* Sessionable.swift */; }; + 45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2A2D701B0D000D550B /* NetworkError.swift */; }; 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */; }; 79DB9F7F53319F206CDE119E /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */; }; 800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800A09D72995F143003ED16E /* POPPopupBridge.swift */; }; @@ -38,6 +40,8 @@ 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = ""; }; 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupBridgeAnalytics.swift; sourceTree = ""; }; 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; + 45FBAF282D701AF7000D550B /* Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sessionable.swift; sourceTree = ""; }; + 45FBAF2A2D701B0D000D550B /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 4EF7C7DDAB0B99FF28DD6541 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -167,6 +171,8 @@ A775A08C1DEE4EF0009E67C2 /* PopupBridge */ = { isa = PBXGroup; children = ( + 45FBAF282D701AF7000D550B /* Sessionable.swift */, + 45FBAF2A2D701B0D000D550B /* NetworkError.swift */, 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */, 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */, 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */, @@ -334,6 +340,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */, + 45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */, 800E789F29E09A2A00D1B0FC /* URLDetailsPayload.swift in Sources */, 45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */, 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */, diff --git a/Sources/PopupBridge/NetworkError.swift b/Sources/PopupBridge/NetworkError.swift new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/Sources/PopupBridge/NetworkError.swift @@ -0,0 +1 @@ + From 52fd81c61b917b3e555df5ae1313891739d4e51e Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 22:02:57 -0600 Subject: [PATCH 04/33] Add Network errors --- Sources/PopupBridge/NetworkError.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/PopupBridge/NetworkError.swift b/Sources/PopupBridge/NetworkError.swift index 8b13789..8a4c38f 100644 --- a/Sources/PopupBridge/NetworkError.swift +++ b/Sources/PopupBridge/NetworkError.swift @@ -1 +1,6 @@ +import Foundation +enum NetworkError: Error { + case invalidResponse + case encodingError(Error) +} From 6224055279ef933f1ac775aa1a07ee6af7a36e52 Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 22:03:15 -0600 Subject: [PATCH 05/33] Add Sessionable Protocol and Extension --- Sources/PopupBridge/Sessionable.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 Sources/PopupBridge/Sessionable.swift diff --git a/Sources/PopupBridge/Sessionable.swift b/Sources/PopupBridge/Sessionable.swift new file mode 100644 index 0000000..f6909df --- /dev/null +++ b/Sources/PopupBridge/Sessionable.swift @@ -0,0 +1,14 @@ +import Foundation + +protocol Sessionable { + func data(for request: URLRequest, delegate: (any URLSessionTaskDelegate)?) async throws -> (Data, URLResponse) +} + +extension Sessionable { + + func data(for request: URLRequest, delegate: (any URLSessionTaskDelegate)? = nil) async throws -> (Data, URLResponse) { + try await data(for: request, delegate: delegate) + } +} + +extension URLSession: Sessionable { } From 4a4e97f12df336700c05a1353a24200dc8782a05 Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 22:03:32 -0600 Subject: [PATCH 06/33] Sort files --- PopupBridge.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 7870281..1329189 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -171,19 +171,19 @@ A775A08C1DEE4EF0009E67C2 /* PopupBridge */ = { isa = PBXGroup; children = ( - 45FBAF282D701AF7000D550B /* Sessionable.swift */, - 45FBAF2A2D701B0D000D550B /* NetworkError.swift */, 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */, 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */, 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */, 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */, 454C7EC22D6E351900CA3191 /* NetworkClient.swift */, + 45FBAF2A2D701B0D000D550B /* NetworkError.swift */, 800A09D72995F143003ED16E /* POPPopupBridge.swift */, A79330F01DF0F98F00EE479D /* PopupBridge-Framework-Info.plist */, 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */, BEF9ED222A2A6A2C005D54AB /* PopupBridgeConstants.swift */, 8079D7192996F6C200A2E336 /* PopupBridgeUserScript.swift */, 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */, + 45FBAF282D701AF7000D550B /* Sessionable.swift */, 800E789E29E09A2A00D1B0FC /* URLDetailsPayload.swift */, BE8E37B52A17B79E00181FDA /* WebAuthenticationSession.swift */, 800E789C29E0958A00D1B0FC /* WebViewMessage.swift */, From 4f87b205cb1633c0f2df07ac46f45a068dae1a1a Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 22:05:05 -0600 Subject: [PATCH 07/33] Add NetworkClient --- Sources/PopupBridge/NetworkClient.swift | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Sources/PopupBridge/NetworkClient.swift b/Sources/PopupBridge/NetworkClient.swift index 8b13789..7f33a41 100644 --- a/Sources/PopupBridge/NetworkClient.swift +++ b/Sources/PopupBridge/NetworkClient.swift @@ -1 +1,40 @@ +import Foundation +protocol Networkable { + func post(url: URL, body: T) async throws +} + +final class NetworkClient: Networkable { + + // MARK: - Private Properties + + private let session: Sessionable + + // MARK: - Initializer + + init(session: Sessionable = URLSession.shared) { + self.session = session + } + + // MARK: - Internal Methods + + func post(url: URL, body: T) async throws { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = ["Content-Type": "application/json"] + + do { + let encodedBody = try JSONEncoder().encode(body) + request.httpBody = encodedBody + } catch let encodingError { + throw NetworkError.encodingError(encodingError) + } + + let (_, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw NetworkError.invalidResponse + } + } +} From 0d71ffbb7977a2a89695287fa5e5dd86d5ae3a23 Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 26 Feb 2025 22:05:44 -0600 Subject: [PATCH 08/33] Implement AnalyticsService class --- Sources/PopupBridge/AnalyticsService.swift | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/AnalyticsService.swift index 8b13789..fa88290 100644 --- a/Sources/PopupBridge/AnalyticsService.swift +++ b/Sources/PopupBridge/AnalyticsService.swift @@ -1 +1,36 @@ +import Foundation +protocol AnalyticsServiceable { + func sendAnalyticsEvent(_ eventName: String, sessionID: String) +} + +final class AnalyticsService: AnalyticsServiceable { + + // MARK: - Private Properties + + /// The FPTI URL to post all analytic events. + private let url = URL(string: "https://api.paypal.com/v1/tracking/batch/events")! + private let networkClient: Networkable + + // MARK: - Initializer + + init(networkClient: Networkable = NetworkClient()) { + self.networkClient = networkClient + } + + // MARK: - Internal Methods + + func sendAnalyticsEvent(_ eventName: String, sessionID: String) { + Task(priority: .background) { + let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID) + try? await networkClient.post(url: url, body: body) + } + } + + /// Constructs POST params to be sent to FPTI + private func createAnalyticsEvent(eventName: String, sessionID: String) -> Codable { + let batchMetadata = FPTIBatchData.Metadata(sessionID: sessionID) + let event = FPTIBatchData.Event(eventName: eventName) + return FPTIBatchData(metadata: batchMetadata, events: [event]) + } +} From 060e7e40007b209997ac87c77ea6ab506458b6ec Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:02:48 -0600 Subject: [PATCH 09/33] Add Private Mark --- Sources/PopupBridge/AnalyticsService.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/AnalyticsService.swift index fa88290..afe0b6c 100644 --- a/Sources/PopupBridge/AnalyticsService.swift +++ b/Sources/PopupBridge/AnalyticsService.swift @@ -27,6 +27,8 @@ final class AnalyticsService: AnalyticsServiceable { } } + // MARK: - Private Methods + /// Constructs POST params to be sent to FPTI private func createAnalyticsEvent(eventName: String, sessionID: String) -> Codable { let batchMetadata = FPTIBatchData.Metadata(sessionID: sessionID) From ddb944c6ca6a89777815d6c3b0fc0e693b0a6a9e Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:03:39 -0600 Subject: [PATCH 10/33] Add necessary files (TestPlan, mock files and UT files) --- PopupBridge.xcodeproj/project.pbxproj | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 1329189..b8f9373 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -15,6 +15,11 @@ 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; }; 45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF282D701AF7000D550B /* Sessionable.swift */; }; 45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2A2D701B0D000D550B /* NetworkError.swift */; }; + 45FBAF2D2D701DD2000D550B /* NetworkClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */; }; + 45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */; }; + 45FBAF312D70C27A000D550B /* TestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 45FBAF302D70C27A000D550B /* TestPlan.xctestplan */; }; + 45FBAF332D70CE9E000D550B /* MockNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */; }; + 45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */; }; 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */; }; 79DB9F7F53319F206CDE119E /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */; }; 800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800A09D72995F143003ED16E /* POPPopupBridge.swift */; }; @@ -42,6 +47,11 @@ 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 45FBAF282D701AF7000D550B /* Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sessionable.swift; sourceTree = ""; }; 45FBAF2A2D701B0D000D550B /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; + 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient_Tests.swift; sourceTree = ""; }; + 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionable.swift; sourceTree = ""; }; + 45FBAF302D70C27A000D550B /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; + 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkClient.swift; sourceTree = ""; }; + 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService_Test.swift; sourceTree = ""; }; 4EF7C7DDAB0B99FF28DD6541 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -152,6 +162,7 @@ isa = PBXGroup; children = ( A775A08C1DEE4EF0009E67C2 /* PopupBridge */, + 45FBAF302D70C27A000D550B /* TestPlan.xctestplan */, ); path = Sources; sourceTree = ""; @@ -160,10 +171,14 @@ isa = PBXGroup; children = ( A775A0801DEE4E7E009E67C2 /* Info.plist */, + 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */, BE2524552A17FB9F00168D77 /* MockScriptMessage.swift */, + 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */, BE2524532A17DFCC00168D77 /* MockUserContentController.swift */, BEF9ED202A2A4896005D54AB /* MockWebAuthenticationSession.swift */, + 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */, BE2524512A17DF8200168D77 /* PopupBridge_UnitTests.swift */, + 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */, ); path = UnitTests; sourceTree = ""; @@ -293,6 +308,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 45FBAF312D70C27A000D550B /* TestPlan.xctestplan in Resources */, 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -329,10 +345,14 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */, BE2524542A17DFCC00168D77 /* MockUserContentController.swift in Sources */, BE2524562A17FB9F00168D77 /* MockScriptMessage.swift in Sources */, BEF9ED212A2A4896005D54AB /* MockWebAuthenticationSession.swift in Sources */, BE2524522A17DF8200168D77 /* PopupBridge_UnitTests.swift in Sources */, + 45FBAF332D70CE9E000D550B /* MockNetworkClient.swift in Sources */, + 45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */, + 45FBAF2D2D701DD2000D550B /* NetworkClient_Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 37ea28abedfa784440b1147c8a62a4bf713fea30 Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:03:52 -0600 Subject: [PATCH 11/33] Setup Test Plan --- Sources/TestPlan.xctestplan | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Sources/TestPlan.xctestplan diff --git a/Sources/TestPlan.xctestplan b/Sources/TestPlan.xctestplan new file mode 100644 index 0000000..bdc3101 --- /dev/null +++ b/Sources/TestPlan.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "333AB1A9-787A-4C49-9F7C-135ED29D74E5", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "testTimeoutsEnabled" : true + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:..\/PopupBridge.xcodeproj", + "identifier" : "A775A07B1DEE4E7E009E67C2", + "name" : "UnitTests" + } + } + ], + "version" : 1 +} From 05cf278f1f3c7deb02dedf891f632a277517ab70 Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:06:19 -0600 Subject: [PATCH 12/33] Create MockSession class and NonEncodable structure --- UnitTests/MockSessionable.swift | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 UnitTests/MockSessionable.swift diff --git a/UnitTests/MockSessionable.swift b/UnitTests/MockSessionable.swift new file mode 100644 index 0000000..faed8bf --- /dev/null +++ b/UnitTests/MockSessionable.swift @@ -0,0 +1,19 @@ +import Foundation +@testable import PopupBridge + +class MockSession: Sessionable { + + var mockData: Data? + var mockResponse: URLResponse? + + func data(for request: URLRequest, delegate: (any URLSessionTaskDelegate)?) async throws -> (Data, URLResponse) { + (mockData ?? Data(), mockResponse ?? URLResponse()) + } +} + +struct NonEncodable: Encodable { + + func encode(to encoder: Encoder) throws { + throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "Non-Encodable type")) + } +} From 631017532fefa4621bcae20aaf4976a90792808c Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:07:12 -0600 Subject: [PATCH 13/33] Add NetworkClient tests --- UnitTests/NetworkClient_Tests.swift | 68 +++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 UnitTests/NetworkClient_Tests.swift diff --git a/UnitTests/NetworkClient_Tests.swift b/UnitTests/NetworkClient_Tests.swift new file mode 100644 index 0000000..2d2ab88 --- /dev/null +++ b/UnitTests/NetworkClient_Tests.swift @@ -0,0 +1,68 @@ +@testable import PopupBridge +import XCTest + +class NetworkClient_Tests: XCTestCase { + + var sut: NetworkClient! + var mockSession: MockSession! + + override func setUp() { + super.setUp() + mockSession = MockSession() + sut = NetworkClient(session: mockSession) + } + + override func tearDown() { + sut = nil + mockSession = nil + super.tearDown() + } + + func testPost_success() async throws { + let url = URL(string: "https://example.com/api/post")! + let body = ["key": "value"] + let expectedData = Data("success response".utf8) + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + + mockSession.mockData = expectedData + mockSession.mockResponse = response + + do { + try await sut.post(url: url, body: body) + } catch { + XCTFail("Post should succeed but failed with error: \(error)") + } + } + + func testPost_failureDueToEncodingError() async { + let url = URL(string: "https://example.com/api/post")! + let body = NonEncodable() + + do { + try await sut.post(url: url, body: body) + XCTFail("Post should have failed due to encoding error") + } catch NetworkError.encodingError { + XCTAssert(true) + } catch { + XCTFail("Unexpected error type: \(error)") + } + } + + func testPost_failureWithInvalidResponse() async throws { + let url = URL(string: "https://example.com/api/post")! + let body = ["key": "value"] + let response = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)! + + mockSession.mockData = Data() + mockSession.mockResponse = response + + do { + try await sut.post(url: url, body: body) + XCTFail("Post should have failed due to invalid HTTP response status") + } catch NetworkError.invalidResponse { + XCTAssert(true) + } catch { + XCTFail("Unexpected error type: \(error)") + } + } +} From b03bdacb524696e195cf7da780908043536985ad Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:08:40 -0600 Subject: [PATCH 14/33] Create MockNetworkClient class --- UnitTests/MockNetworkClient.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 UnitTests/MockNetworkClient.swift diff --git a/UnitTests/MockNetworkClient.swift b/UnitTests/MockNetworkClient.swift new file mode 100644 index 0000000..b4d764b --- /dev/null +++ b/UnitTests/MockNetworkClient.swift @@ -0,0 +1,16 @@ +import Foundation +@testable import PopupBridge + +class MockNetworkClient: Networkable { + var lastURL: URL? + var lastBody: Encodable? + var throwError: Error? + + func post(url: URL, body: T) async throws where T : Encodable { + lastURL = url + lastBody = body + if let error = throwError { + throw error + } + } +} From d6fc4fc4c4806928b05ed1a0620ff02d350aa2b4 Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:09:05 -0600 Subject: [PATCH 15/33] Sort files --- PopupBridge.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index b8f9373..a9767c1 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -170,6 +170,7 @@ A775A07D1DEE4E7E009E67C2 /* UnitTests */ = { isa = PBXGroup; children = ( + 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */, A775A0801DEE4E7E009E67C2 /* Info.plist */, 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */, BE2524552A17FB9F00168D77 /* MockScriptMessage.swift */, @@ -178,7 +179,6 @@ BEF9ED202A2A4896005D54AB /* MockWebAuthenticationSession.swift */, 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */, BE2524512A17DF8200168D77 /* PopupBridge_UnitTests.swift */, - 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */, ); path = UnitTests; sourceTree = ""; From 022530ddf6a8b3f5c9061632b81cc9ab5fb2967a Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 11:09:38 -0600 Subject: [PATCH 16/33] Add missing blank space --- UnitTests/MockNetworkClient.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/UnitTests/MockNetworkClient.swift b/UnitTests/MockNetworkClient.swift index b4d764b..467964e 100644 --- a/UnitTests/MockNetworkClient.swift +++ b/UnitTests/MockNetworkClient.swift @@ -2,6 +2,7 @@ import Foundation @testable import PopupBridge class MockNetworkClient: Networkable { + var lastURL: URL? var lastBody: Encodable? var throwError: Error? From c9e846c67a4a0dd6852ef53cd1d955ae6e8767ce Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 16:01:58 -0600 Subject: [PATCH 17/33] Add UTs to test success and failures --- Sources/PopupBridge/AnalyticsService.swift | 8 +++-- UnitTests/AnalyticsService_Test.swift | 36 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 UnitTests/AnalyticsService_Test.swift diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/AnalyticsService.swift index afe0b6c..93781aa 100644 --- a/Sources/PopupBridge/AnalyticsService.swift +++ b/Sources/PopupBridge/AnalyticsService.swift @@ -22,11 +22,15 @@ final class AnalyticsService: AnalyticsServiceable { func sendAnalyticsEvent(_ eventName: String, sessionID: String) { Task(priority: .background) { - let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID) - try? await networkClient.post(url: url, body: body) + await performEventRequest(eventName, sessionID: sessionID) } } + func performEventRequest(_ eventName: String, sessionID: String) async { + let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID) + try? await networkClient.post(url: url, body: body) + } + // MARK: - Private Methods /// Constructs POST params to be sent to FPTI diff --git a/UnitTests/AnalyticsService_Test.swift b/UnitTests/AnalyticsService_Test.swift new file mode 100644 index 0000000..82b3d00 --- /dev/null +++ b/UnitTests/AnalyticsService_Test.swift @@ -0,0 +1,36 @@ +@testable import PopupBridge +import XCTest + +class AnalyticsService_Test: XCTestCase { + + var sut: AnalyticsService! + var mockNetworkClient: MockNetworkClient! + + override func setUp() { + super.setUp() + mockNetworkClient = MockNetworkClient() + sut = AnalyticsService(networkClient: mockNetworkClient) + } + + override func tearDown() { + sut = nil + mockNetworkClient = nil + super.tearDown() + } + + func testPerformEventRequest() async { + await sut.performEventRequest("some-event", sessionID: "some-session-id") + + XCTAssertEqual(mockNetworkClient.lastURL, URL(string: "https://api.paypal.com/v1/tracking/batch/events")) + XCTAssertNotNil(mockNetworkClient.lastBody) + } + + func testPerformEventRequest_handlesError() async { + mockNetworkClient.throwError = NetworkError.invalidResponse + + await sut.performEventRequest("some-event", sessionID: "some-session-id") + + XCTAssertEqual(mockNetworkClient.lastURL, URL(string: "https://api.paypal.com/v1/tracking/batch/events")) + XCTAssertNotNil(mockNetworkClient.lastBody) + } +} From 292affb870904e0b94e9be0ccfa73cd33cf4f912 Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 17:05:39 -0600 Subject: [PATCH 18/33] Remove iOS 15 validation --- Sources/PopupBridge/POPPopupBridge.swift | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Sources/PopupBridge/POPPopupBridge.swift b/Sources/PopupBridge/POPPopupBridge.swift index 8531366..a730e2b 100644 --- a/Sources/PopupBridge/POPPopupBridge.swift +++ b/Sources/PopupBridge/POPPopupBridge.swift @@ -153,13 +153,8 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { extension POPPopupBridge: ASWebAuthenticationPresentationContextProviding { public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { - if #available(iOS 15, *) { - let firstScene = UIApplication.shared.connectedScenes.first as? UIWindowScene - let window = firstScene?.windows.first { $0.isKeyWindow } - return window ?? ASPresentationAnchor() - } else { - let window = UIApplication.shared.windows.first { $0.isKeyWindow } - return window ?? ASPresentationAnchor() - } + let firstScene = UIApplication.shared.connectedScenes.first as? UIWindowScene + let window = firstScene?.windows.first { $0.isKeyWindow } + return window ?? ASPresentationAnchor() } } From 5308717792c99938d0d913005d160d6d931c4775 Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 27 Feb 2025 19:10:58 -0600 Subject: [PATCH 19/33] Add analytics calls --- Sources/PopupBridge/POPPopupBridge.swift | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Sources/PopupBridge/POPPopupBridge.swift b/Sources/PopupBridge/POPPopupBridge.swift index a730e2b..d24996d 100644 --- a/Sources/PopupBridge/POPPopupBridge.swift +++ b/Sources/PopupBridge/POPPopupBridge.swift @@ -14,6 +14,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { private let hostName = "popupbridgev1" private let webView: WKWebView private var webAuthenticationSession: WebAuthenticationSession = WebAuthenticationSession() + private let analyticsService: AnalyticsServiceable = AnalyticsService() private var returnBlock: ((URL) -> Void)? = nil @@ -30,6 +31,8 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { super.init() + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: "some-session-id") + configureWebView() webAuthenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession @@ -44,7 +47,10 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { } /// Exposed for testing - convenience init(webView: WKWebView, webAuthenticationSession: WebAuthenticationSession) { + convenience init( + webView: WKWebView, + webAuthenticationSession: WebAuthenticationSession + ) { self.init(webView: webView) self.webAuthenticationSession = webAuthenticationSession } @@ -76,10 +82,12 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { if let payloadData = try? JSONEncoder().encode(payload), let payload = String(data: payloadData, encoding: .utf8) { + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.succeeded, sessionID: "some-session-id") return "window.popupBridge.onComplete(null, \(payload));" } else { let errorMessage = "Failed to parse query items from return URL." let errorResponse = "new Error(\"\(errorMessage)\")" + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.failed, sessionID: "some-session-id") return "window.popupBridge.onComplete(\(errorResponse), null);" } } @@ -139,7 +147,9 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { window.popupBridge.onComplete(null, null);\ } """ - + + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.canceled, sessionID: "some-session-id") + injectWebView(webView: webView, withJavaScript: script) return } From a268cd0f4cdf6331eab966fe5d9c6d0a6aaa47da Mon Sep 17 00:00:00 2001 From: richherrera Date: Mon, 3 Mar 2025 13:56:13 -0600 Subject: [PATCH 20/33] Set sessionID value --- Sources/PopupBridge/POPPopupBridge.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Sources/PopupBridge/POPPopupBridge.swift b/Sources/PopupBridge/POPPopupBridge.swift index d24996d..1d1ce9e 100644 --- a/Sources/PopupBridge/POPPopupBridge.swift +++ b/Sources/PopupBridge/POPPopupBridge.swift @@ -11,11 +11,12 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { // MARK: - Private Properties private let messageHandlerName = "POPPopupBridge" - private let hostName = "popupbridgev1" + private let hostName = "popupbridgev1" + private let sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "") private let webView: WKWebView - private var webAuthenticationSession: WebAuthenticationSession = WebAuthenticationSession() private let analyticsService: AnalyticsServiceable = AnalyticsService() + private var webAuthenticationSession: WebAuthenticationSession = WebAuthenticationSession() private var returnBlock: ((URL) -> Void)? = nil // MARK: - Initializers @@ -30,8 +31,8 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { self.webView = webView super.init() - - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: "some-session-id") + + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: sessionID) configureWebView() webAuthenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession @@ -82,12 +83,12 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { if let payloadData = try? JSONEncoder().encode(payload), let payload = String(data: payloadData, encoding: .utf8) { - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.succeeded, sessionID: "some-session-id") + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.succeeded, sessionID: sessionID) return "window.popupBridge.onComplete(null, \(payload));" } else { let errorMessage = "Failed to parse query items from return URL." let errorResponse = "new Error(\"\(errorMessage)\")" - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.failed, sessionID: "some-session-id") + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.failed, sessionID: sessionID) return "window.popupBridge.onComplete(\(errorResponse), null);" } } @@ -148,7 +149,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { } """ - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.canceled, sessionID: "some-session-id") + analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.canceled, sessionID: sessionID) injectWebView(webView: webView, withJavaScript: script) return From 9734fb31af187a879cef107b8152c7c876fcfc7b Mon Sep 17 00:00:00 2001 From: richherrera Date: Mon, 3 Mar 2025 14:47:51 -0600 Subject: [PATCH 21/33] Add AnalyticsService property --- Sources/PopupBridge/POPPopupBridge.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/PopupBridge/POPPopupBridge.swift b/Sources/PopupBridge/POPPopupBridge.swift index 1d1ce9e..7cb968a 100644 --- a/Sources/PopupBridge/POPPopupBridge.swift +++ b/Sources/PopupBridge/POPPopupBridge.swift @@ -14,8 +14,8 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { private let hostName = "popupbridgev1" private let sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "") private let webView: WKWebView - private let analyticsService: AnalyticsServiceable = AnalyticsService() + private var analyticsService: AnalyticsServiceable = AnalyticsService() private var webAuthenticationSession: WebAuthenticationSession = WebAuthenticationSession() private var returnBlock: ((URL) -> Void)? = nil @@ -50,9 +50,11 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { /// Exposed for testing convenience init( webView: WKWebView, - webAuthenticationSession: WebAuthenticationSession + webAuthenticationSession: WebAuthenticationSession, + analyticsService: AnalyticsServiceable ) { self.init(webView: webView) + self.analyticsService = AnalyticsService() self.webAuthenticationSession = webAuthenticationSession } From 3ba4d210c4afd6c0dc572acbfc8dd5873c19a560 Mon Sep 17 00:00:00 2001 From: richherrera Date: Mon, 3 Mar 2025 14:48:14 -0600 Subject: [PATCH 22/33] Add MockAnalyticsService --- PopupBridge.xcodeproj/project.pbxproj | 4 ++++ UnitTests/MockAnalyticsService.swift | 9 +++++++++ 2 files changed, 13 insertions(+) create mode 100644 UnitTests/MockAnalyticsService.swift diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index a9767c1..306b185 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 454C7EC32D6E351F00CA3191 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C7EC22D6E351900CA3191 /* NetworkClient.swift */; }; + 45AE41D22D7649F800388548 /* MockAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */; }; 45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */; }; 45CD0C2E2D664D140072C5A4 /* Date+MilisecondTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */; }; 45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */; }; @@ -40,6 +41,7 @@ /* Begin PBXFileReference section */ 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 454C7EC22D6E351900CA3191 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; + 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalyticsService.swift; sourceTree = ""; }; 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+MilisecondTimestamp.swift"; sourceTree = ""; }; 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = ""; }; @@ -172,6 +174,7 @@ children = ( 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */, A775A0801DEE4E7E009E67C2 /* Info.plist */, + 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */, 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */, BE2524552A17FB9F00168D77 /* MockScriptMessage.swift */, 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */, @@ -347,6 +350,7 @@ files = ( 45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */, BE2524542A17DFCC00168D77 /* MockUserContentController.swift in Sources */, + 45AE41D22D7649F800388548 /* MockAnalyticsService.swift in Sources */, BE2524562A17FB9F00168D77 /* MockScriptMessage.swift in Sources */, BEF9ED212A2A4896005D54AB /* MockWebAuthenticationSession.swift in Sources */, BE2524522A17DF8200168D77 /* PopupBridge_UnitTests.swift in Sources */, diff --git a/UnitTests/MockAnalyticsService.swift b/UnitTests/MockAnalyticsService.swift new file mode 100644 index 0000000..77bfe1a --- /dev/null +++ b/UnitTests/MockAnalyticsService.swift @@ -0,0 +1,9 @@ +import Foundation +@testable import PopupBridge + +class MockAnalyticsService: AnalyticsServiceable { + + func sendAnalyticsEvent(_ eventName: String, sessionID: String) { + // TODO: Add mock validations + } +} From 700f695d9f1fe279379f3463b923ba32cccf22a4 Mon Sep 17 00:00:00 2001 From: richherrera Date: Mon, 3 Mar 2025 14:48:46 -0600 Subject: [PATCH 23/33] Fix PopupBridge tests adding mock --- UnitTests/PopupBridge_UnitTests.swift | 31 ++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/UnitTests/PopupBridge_UnitTests.swift b/UnitTests/PopupBridge_UnitTests.swift index e74f2d9..8adf9e8 100644 --- a/UnitTests/PopupBridge_UnitTests.swift +++ b/UnitTests/PopupBridge_UnitTests.swift @@ -7,6 +7,7 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { let scriptMessageHandlerName: String = "POPPopupBridge" let returnURL: String = "com.braintreepayments.popupbridgeexample" let mockWebAuthenticationSession = MockWebAuthenticationSession() + let mockAnalyticsService = MockAnalyticsService() var webViewReadyBlock: (Void)? @@ -52,7 +53,11 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { configuration.userContentController = mockUserContentController let webView = WKWebView(frame: CGRect(), configuration: configuration) - let pub = POPPopupBridge(webView: webView, webAuthenticationSession: mockWebAuthenticationSession) + let pub = POPPopupBridge( + webView: webView, + webAuthenticationSession: mockWebAuthenticationSession, + analyticsService: mockAnalyticsService + ) mockWebAuthenticationSession.cannedResponseURL = URL(string: "http://example.com/?hello=world") pub.userContentController(WKUserContentController(), didReceive: stubMessage) @@ -135,7 +140,11 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { configuration.userContentController = mockUserContentController let webView = WKWebView(frame: CGRect(), configuration: configuration) - let pub = POPPopupBridge(webView: webView, webAuthenticationSession: mockWebAuthenticationSession) + let pub = POPPopupBridge( + webView: webView, + webAuthenticationSession: mockWebAuthenticationSession, + analyticsService: mockAnalyticsService + ) let mockURL = URL(string: "sdk.ios.popup-bridge://popupbridgev1/return?something=foo&other=bar")! mockWebAuthenticationSession.cannedResponseURL = mockURL @@ -161,7 +170,11 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { configuration.userContentController = mockUserContentController let webView = WKWebView(frame: CGRect(), configuration: configuration) - let pub = POPPopupBridge(webView: webView, webAuthenticationSession: mockWebAuthenticationSession) + let pub = POPPopupBridge( + webView: webView, + webAuthenticationSession: mockWebAuthenticationSession, + analyticsService: mockAnalyticsService + ) let mockURL = URL(string: "sdk.ios.popup-bridge://popupbridgev1/return")! mockWebAuthenticationSession.cannedResponseURL = mockURL @@ -181,7 +194,11 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { stubMessage.body = stubMessageBody stubMessage.name = stubMessageName - let pub = POPPopupBridge(webView: WKWebView(), webAuthenticationSession: mockWebAuthenticationSession) + let pub = POPPopupBridge( + webView: WKWebView(), + webAuthenticationSession: mockWebAuthenticationSession, + analyticsService: mockAnalyticsService + ) pub.userContentController(WKUserContentController(), didReceive: stubMessage) @@ -204,7 +221,11 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { stubMessage.body = stubMessageBody stubMessage.name = stubMessageName - let pub = POPPopupBridge(webView: WKWebView(), webAuthenticationSession: mockWebAuthenticationSession) + let pub = POPPopupBridge( + webView: WKWebView(), + webAuthenticationSession: mockWebAuthenticationSession, + analyticsService: mockAnalyticsService + ) pub.userContentController(WKUserContentController(), didReceive: stubMessage) From fd335357e4b2082762fa6ba5d1cf7bd300610186 Mon Sep 17 00:00:00 2001 From: richherrera Date: Mon, 3 Mar 2025 14:49:15 -0600 Subject: [PATCH 24/33] Increase time interval --- Demo/UITests/PopupBridge_DemoUITests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Demo/UITests/PopupBridge_DemoUITests.swift b/Demo/UITests/PopupBridge_DemoUITests.swift index c4db7ff..f640f18 100644 --- a/Demo/UITests/PopupBridge_DemoUITests.swift +++ b/Demo/UITests/PopupBridge_DemoUITests.swift @@ -56,7 +56,7 @@ final class PopupBridge_DemoUITests: XCTestCase { // MARK: - Helpers - func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 10) { + func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 15) { expectation(for: NSPredicate(format: "exists ==1"), evaluatedWith: element) waitForExpectations(timeout: timeout) } From d0a8d8deaef82ade5832310f4883a4f690b345dd Mon Sep 17 00:00:00 2001 From: richherrera Date: Tue, 4 Mar 2025 11:29:13 -0600 Subject: [PATCH 25/33] Add MockAnalyticsService --- Sources/PopupBridge/POPPopupBridge.swift | 2 +- UnitTests/MockAnalyticsService.swift | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/PopupBridge/POPPopupBridge.swift b/Sources/PopupBridge/POPPopupBridge.swift index 7cb968a..d318eff 100644 --- a/Sources/PopupBridge/POPPopupBridge.swift +++ b/Sources/PopupBridge/POPPopupBridge.swift @@ -54,7 +54,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { analyticsService: AnalyticsServiceable ) { self.init(webView: webView) - self.analyticsService = AnalyticsService() + self.analyticsService = analyticsService self.webAuthenticationSession = webAuthenticationSession } diff --git a/UnitTests/MockAnalyticsService.swift b/UnitTests/MockAnalyticsService.swift index 77bfe1a..036eb43 100644 --- a/UnitTests/MockAnalyticsService.swift +++ b/UnitTests/MockAnalyticsService.swift @@ -3,7 +3,13 @@ import Foundation class MockAnalyticsService: AnalyticsServiceable { + var lastEventName: String? + var lastSessionID: String? + var eventCount = 0 + func sendAnalyticsEvent(_ eventName: String, sessionID: String) { - // TODO: Add mock validations + lastEventName = eventName + lastSessionID = sessionID + eventCount += 1 } } From 147dc9b9bc76ab58f6623f87eb24cd80d2267b42 Mon Sep 17 00:00:00 2001 From: richherrera Date: Tue, 4 Mar 2025 12:04:56 -0600 Subject: [PATCH 26/33] Add PopupBrdige UTs --- Sources/PopupBridge/POPPopupBridge.swift | 15 ++-- UnitTests/MockWebAuthenticationSession.swift | 7 +- UnitTests/PopupBridge_UnitTests.swift | 92 +++++++++++++++++--- 3 files changed, 95 insertions(+), 19 deletions(-) diff --git a/Sources/PopupBridge/POPPopupBridge.swift b/Sources/PopupBridge/POPPopupBridge.swift index d318eff..a87bfc1 100644 --- a/Sources/PopupBridge/POPPopupBridge.swift +++ b/Sources/PopupBridge/POPPopupBridge.swift @@ -8,6 +8,8 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { /// Exposed for testing var returnedWithURL: Bool = false + static var analyticsService: AnalyticsServiceable = AnalyticsService() + // MARK: - Private Properties private let messageHandlerName = "POPPopupBridge" @@ -15,7 +17,6 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { private let sessionID = UUID().uuidString.replacingOccurrences(of: "-", with: "") private let webView: WKWebView - private var analyticsService: AnalyticsServiceable = AnalyticsService() private var webAuthenticationSession: WebAuthenticationSession = WebAuthenticationSession() private var returnBlock: ((URL) -> Void)? = nil @@ -32,7 +33,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { super.init() - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: sessionID) + Self.analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.started, sessionID: sessionID) configureWebView() webAuthenticationSession.prefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession @@ -50,11 +51,9 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { /// Exposed for testing convenience init( webView: WKWebView, - webAuthenticationSession: WebAuthenticationSession, - analyticsService: AnalyticsServiceable + webAuthenticationSession: WebAuthenticationSession ) { self.init(webView: webView) - self.analyticsService = analyticsService self.webAuthenticationSession = webAuthenticationSession } @@ -85,12 +84,12 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { if let payloadData = try? JSONEncoder().encode(payload), let payload = String(data: payloadData, encoding: .utf8) { - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.succeeded, sessionID: sessionID) + Self.analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.succeeded, sessionID: sessionID) return "window.popupBridge.onComplete(null, \(payload));" } else { let errorMessage = "Failed to parse query items from return URL." let errorResponse = "new Error(\"\(errorMessage)\")" - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.failed, sessionID: sessionID) + Self.analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.failed, sessionID: sessionID) return "window.popupBridge.onComplete(\(errorResponse), null);" } } @@ -151,7 +150,7 @@ public class POPPopupBridge: NSObject, WKScriptMessageHandler { } """ - analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.canceled, sessionID: sessionID) + Self.analyticsService.sendAnalyticsEvent(PopupBridgeAnalytics.canceled, sessionID: sessionID) injectWebView(webView: webView, withJavaScript: script) return diff --git a/UnitTests/MockWebAuthenticationSession.swift b/UnitTests/MockWebAuthenticationSession.swift index 4f8b539..62608d7 100644 --- a/UnitTests/MockWebAuthenticationSession.swift +++ b/UnitTests/MockWebAuthenticationSession.swift @@ -5,6 +5,7 @@ import AuthenticationServices class MockWebAuthenticationSession: WebAuthenticationSession { var cannedResponseURL: URL? var cannedErrorResponse: Error? + var shouldCancel: Bool = false override func start( url: URL, @@ -12,6 +13,10 @@ class MockWebAuthenticationSession: WebAuthenticationSession { sessionDidComplete: @escaping (URL?, Error?) -> Void, sessionDidCancel: @escaping () -> Void ) { - sessionDidComplete(cannedResponseURL, cannedErrorResponse) + if shouldCancel { + sessionDidCancel() + } else { + sessionDidComplete(cannedResponseURL, cannedErrorResponse) + } } } diff --git a/UnitTests/PopupBridge_UnitTests.swift b/UnitTests/PopupBridge_UnitTests.swift index 8adf9e8..07ab7ea 100644 --- a/UnitTests/PopupBridge_UnitTests.swift +++ b/UnitTests/PopupBridge_UnitTests.swift @@ -55,8 +55,7 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { let webView = WKWebView(frame: CGRect(), configuration: configuration) let pub = POPPopupBridge( webView: webView, - webAuthenticationSession: mockWebAuthenticationSession, - analyticsService: mockAnalyticsService + webAuthenticationSession: mockWebAuthenticationSession ) mockWebAuthenticationSession.cannedResponseURL = URL(string: "http://example.com/?hello=world") @@ -142,8 +141,7 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { let webView = WKWebView(frame: CGRect(), configuration: configuration) let pub = POPPopupBridge( webView: webView, - webAuthenticationSession: mockWebAuthenticationSession, - analyticsService: mockAnalyticsService + webAuthenticationSession: mockWebAuthenticationSession ) let mockURL = URL(string: "sdk.ios.popup-bridge://popupbridgev1/return?something=foo&other=bar")! mockWebAuthenticationSession.cannedResponseURL = mockURL @@ -172,8 +170,7 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { let webView = WKWebView(frame: CGRect(), configuration: configuration) let pub = POPPopupBridge( webView: webView, - webAuthenticationSession: mockWebAuthenticationSession, - analyticsService: mockAnalyticsService + webAuthenticationSession: mockWebAuthenticationSession ) let mockURL = URL(string: "sdk.ios.popup-bridge://popupbridgev1/return")! mockWebAuthenticationSession.cannedResponseURL = mockURL @@ -196,8 +193,7 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { let pub = POPPopupBridge( webView: WKWebView(), - webAuthenticationSession: mockWebAuthenticationSession, - analyticsService: mockAnalyticsService + webAuthenticationSession: mockWebAuthenticationSession ) pub.userContentController(WKUserContentController(), didReceive: stubMessage) @@ -223,8 +219,7 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { let pub = POPPopupBridge( webView: WKWebView(), - webAuthenticationSession: mockWebAuthenticationSession, - analyticsService: mockAnalyticsService + webAuthenticationSession: mockWebAuthenticationSession ) pub.userContentController(WKUserContentController(), didReceive: stubMessage) @@ -292,4 +287,81 @@ final class PopupBridge_UnitTests: XCTestCase, WKNavigationDelegate { return nil } + + func testInit_sendsAnalytics() { + XCTAssertEqual(mockAnalyticsService.eventCount, 0) + POPPopupBridge.analyticsService = mockAnalyticsService + + let _ = POPPopupBridge( + webView: WKWebView(), + webAuthenticationSession: mockWebAuthenticationSession + ) + + XCTAssertEqual(mockAnalyticsService.eventCount, 1) + XCTAssertEqual(mockAnalyticsService.lastEventName, PopupBridgeAnalytics.started) + XCTAssertNotNil(mockAnalyticsService.lastSessionID) + } + + func testConstructJavaScriptCompletionResult_whenReturnURL_sendsAnalytics() { + XCTAssertEqual(mockAnalyticsService.eventCount, 0) + let configuration = WKWebViewConfiguration() + let mockUserContentController = MockUserContentController() + + configuration.userContentController = mockUserContentController + + let webView = WKWebView(frame: CGRect(), configuration: configuration) + POPPopupBridge.analyticsService = mockAnalyticsService + let pub = POPPopupBridge( + webView: webView, + webAuthenticationSession: mockWebAuthenticationSession + ) + let mockURL = URL(string: "sdk.ios.popup-bridge://popupbridgev1/return?something=foo&other=bar")! + mockWebAuthenticationSession.cannedResponseURL = mockURL + + let expectedResult = "window.popupBridge.onComplete(null, {\"path\":\"\\/return\",\"queryItems\":{\"other\":\"bar\",\"something\":\"foo\"}});" + let result = pub.constructJavaScriptCompletionResult(returnURL: mockURL) + + let expectedJSON = extractJSON(from: expectedResult) + let actualJSON = extractJSON(from: result!) + + XCTAssertEqual(actualJSON, expectedJSON) + XCTAssertEqual(mockAnalyticsService.eventCount, 2) + XCTAssertEqual(mockAnalyticsService.lastEventName, PopupBridgeAnalytics.succeeded) + XCTAssertNotNil(mockAnalyticsService.lastSessionID) + } + + func testPopupBridge_whenCancelButtonTappedOnSafariViewController_sendsAnalytics() { + XCTAssertEqual(mockAnalyticsService.eventCount, 0) + let stubMessageBody: [String: String] = ["url": "http://example.com/?hello=world"] + let stubMessageName = scriptMessageHandlerName + let stubMessage = MockScriptMessage() + stubMessage.body = stubMessageBody + stubMessage.name = stubMessageName + + let configuration = WKWebViewConfiguration() + let mockUserContentController = MockUserContentController() + + configuration.userContentController = mockUserContentController + + POPPopupBridge.analyticsService = mockAnalyticsService + let webView = WKWebView(frame: CGRect(), configuration: configuration) + let pub = POPPopupBridge( + webView: webView, + webAuthenticationSession: mockWebAuthenticationSession + ) + mockWebAuthenticationSession.shouldCancel = true + pub.userContentController(WKUserContentController(), didReceive: stubMessage) + + webView.evaluateJavaScript(""" + "if (typeof window.popupBridge.onCancel === 'function') {" + " window.popupBridge.onCancel();" + "} else {" + " window.popupBridge.onComplete(null, null);" + "}" + """) + + XCTAssertEqual(mockAnalyticsService.eventCount, 2) + XCTAssertEqual(mockAnalyticsService.lastEventName, PopupBridgeAnalytics.canceled) + XCTAssertNotNil(mockAnalyticsService.lastSessionID) + } } From 9a5e980c1a902a7c03f32083c25e6f3428cc4b30 Mon Sep 17 00:00:00 2001 From: richherrera Date: Wed, 5 Mar 2025 14:47:18 -0600 Subject: [PATCH 27/33] Strongly type FPTIBatchData --- Sources/PopupBridge/AnalyticsService.swift | 2 +- Sources/PopupBridge/NetworkClient.swift | 14 +++++--------- Sources/PopupBridge/NetworkError.swift | 1 - UnitTests/NetworkClient_Tests.swift | 21 +++++---------------- 4 files changed, 11 insertions(+), 27 deletions(-) diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/AnalyticsService.swift index 93781aa..3aa2fda 100644 --- a/Sources/PopupBridge/AnalyticsService.swift +++ b/Sources/PopupBridge/AnalyticsService.swift @@ -34,7 +34,7 @@ final class AnalyticsService: AnalyticsServiceable { // MARK: - Private Methods /// Constructs POST params to be sent to FPTI - private func createAnalyticsEvent(eventName: String, sessionID: String) -> Codable { + private func createAnalyticsEvent(eventName: String, sessionID: String) -> FPTIBatchData { let batchMetadata = FPTIBatchData.Metadata(sessionID: sessionID) let event = FPTIBatchData.Event(eventName: eventName) return FPTIBatchData(metadata: batchMetadata, events: [event]) diff --git a/Sources/PopupBridge/NetworkClient.swift b/Sources/PopupBridge/NetworkClient.swift index 7f33a41..701312e 100644 --- a/Sources/PopupBridge/NetworkClient.swift +++ b/Sources/PopupBridge/NetworkClient.swift @@ -1,7 +1,7 @@ import Foundation protocol Networkable { - func post(url: URL, body: T) async throws + func post(url: URL, body: FPTIBatchData) async throws } final class NetworkClient: Networkable { @@ -18,18 +18,14 @@ final class NetworkClient: Networkable { // MARK: - Internal Methods - func post(url: URL, body: T) async throws { + func post(url: URL, body: FPTIBatchData) async throws { var request = URLRequest(url: url) request.httpMethod = "POST" request.allHTTPHeaderFields = ["Content-Type": "application/json"] - do { - let encodedBody = try JSONEncoder().encode(body) - request.httpBody = encodedBody - } catch let encodingError { - throw NetworkError.encodingError(encodingError) - } - + let encodedBody = try JSONEncoder().encode(body) + request.httpBody = encodedBody + let (_, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse, diff --git a/Sources/PopupBridge/NetworkError.swift b/Sources/PopupBridge/NetworkError.swift index 8a4c38f..ab07a1b 100644 --- a/Sources/PopupBridge/NetworkError.swift +++ b/Sources/PopupBridge/NetworkError.swift @@ -2,5 +2,4 @@ import Foundation enum NetworkError: Error { case invalidResponse - case encodingError(Error) } diff --git a/UnitTests/NetworkClient_Tests.swift b/UnitTests/NetworkClient_Tests.swift index 2d2ab88..cb19e77 100644 --- a/UnitTests/NetworkClient_Tests.swift +++ b/UnitTests/NetworkClient_Tests.swift @@ -6,6 +6,11 @@ class NetworkClient_Tests: XCTestCase { var sut: NetworkClient! var mockSession: MockSession! + let body = FPTIBatchData( + metadata: .init(sessionID: "some-session-id"), + events: [.init(eventName: "some-event-name")] + ) + override func setUp() { super.setUp() mockSession = MockSession() @@ -20,7 +25,6 @@ class NetworkClient_Tests: XCTestCase { func testPost_success() async throws { let url = URL(string: "https://example.com/api/post")! - let body = ["key": "value"] let expectedData = Data("success response".utf8) let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! @@ -34,23 +38,8 @@ class NetworkClient_Tests: XCTestCase { } } - func testPost_failureDueToEncodingError() async { - let url = URL(string: "https://example.com/api/post")! - let body = NonEncodable() - - do { - try await sut.post(url: url, body: body) - XCTFail("Post should have failed due to encoding error") - } catch NetworkError.encodingError { - XCTAssert(true) - } catch { - XCTFail("Unexpected error type: \(error)") - } - } - func testPost_failureWithInvalidResponse() async throws { let url = URL(string: "https://example.com/api/post")! - let body = ["key": "value"] let response = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)! mockSession.mockData = Data() From a658633a92b817d8eb13ab60665a5f832d043e2e Mon Sep 17 00:00:00 2001 From: richherrera Date: Thu, 6 Mar 2025 12:16:08 -0600 Subject: [PATCH 28/33] Add catch block --- Sources/PopupBridge/AnalyticsService.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/AnalyticsService.swift index 3aa2fda..98ceed3 100644 --- a/Sources/PopupBridge/AnalyticsService.swift +++ b/Sources/PopupBridge/AnalyticsService.swift @@ -28,7 +28,11 @@ final class AnalyticsService: AnalyticsServiceable { func performEventRequest(_ eventName: String, sessionID: String) async { let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID) - try? await networkClient.post(url: url, body: body) + do { + try await networkClient.post(url: url, body: body) + } catch { + NSLog("[PopupBridge SDK] Failed to send analytics: %@", error.localizedDescription) + } } // MARK: - Private Methods From 70bb98e396ae7f09bb23b34f16f1173f473985e0 Mon Sep 17 00:00:00 2001 From: Rich Herrera Date: Mon, 10 Mar 2025 14:26:40 -0600 Subject: [PATCH 29/33] Update Sources/PopupBridge/FPTIBatchData.swift Co-authored-by: Jax DesMarais-Leder --- Sources/PopupBridge/FPTIBatchData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/PopupBridge/FPTIBatchData.swift b/Sources/PopupBridge/FPTIBatchData.swift index 418cc5c..41d04e3 100644 --- a/Sources/PopupBridge/FPTIBatchData.swift +++ b/Sources/PopupBridge/FPTIBatchData.swift @@ -40,7 +40,7 @@ struct FPTIBatchData: Codable { } } - /// The FPTI tags/ metadata applicable to all events in the batch upload. + /// The FPTI tags/metadata applicable to all events in the batch upload. struct Metadata: Codable { let appID: String = Bundle.main.infoDictionary?[kCFBundleIdentifierKey as String] as? String ?? "N/A" From c8fd4cfeae501954350b8081c66e2e04f5f5e95e Mon Sep 17 00:00:00 2001 From: richherrera Date: Fri, 14 Mar 2025 11:06:32 -0600 Subject: [PATCH 30/33] Add analytics folder --- PopupBridge.xcodeproj/project.pbxproj | 14 +++++++++++--- .../{ => Analytics}/AnalyticsService.swift | 0 .../{ => Analytics}/FPTIBatchData.swift | 0 .../{ => Analytics}/PopupBridgeAnalytics.swift | 0 4 files changed, 11 insertions(+), 3 deletions(-) rename Sources/PopupBridge/{ => Analytics}/AnalyticsService.swift (100%) rename Sources/PopupBridge/{ => Analytics}/FPTIBatchData.swift (100%) rename Sources/PopupBridge/{ => Analytics}/PopupBridgeAnalytics.swift (100%) diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 306b185..f826ced 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -114,6 +114,16 @@ path = Pods; sourceTree = ""; }; + 45FC74C32D84922F00E50035 /* Analytics */ = { + isa = PBXGroup; + children = ( + 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */, + 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */, + 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */, + ); + path = Analytics; + sourceTree = ""; + }; 6003F581195388D10070C39A = { isa = PBXGroup; children = ( @@ -189,15 +199,13 @@ A775A08C1DEE4EF0009E67C2 /* PopupBridge */ = { isa = PBXGroup; children = ( - 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */, + 45FC74C32D84922F00E50035 /* Analytics */, 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */, 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */, - 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */, 454C7EC22D6E351900CA3191 /* NetworkClient.swift */, 45FBAF2A2D701B0D000D550B /* NetworkError.swift */, 800A09D72995F143003ED16E /* POPPopupBridge.swift */, A79330F01DF0F98F00EE479D /* PopupBridge-Framework-Info.plist */, - 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */, BEF9ED222A2A6A2C005D54AB /* PopupBridgeConstants.swift */, 8079D7192996F6C200A2E336 /* PopupBridgeUserScript.swift */, 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */, diff --git a/Sources/PopupBridge/AnalyticsService.swift b/Sources/PopupBridge/Analytics/AnalyticsService.swift similarity index 100% rename from Sources/PopupBridge/AnalyticsService.swift rename to Sources/PopupBridge/Analytics/AnalyticsService.swift diff --git a/Sources/PopupBridge/FPTIBatchData.swift b/Sources/PopupBridge/Analytics/FPTIBatchData.swift similarity index 100% rename from Sources/PopupBridge/FPTIBatchData.swift rename to Sources/PopupBridge/Analytics/FPTIBatchData.swift diff --git a/Sources/PopupBridge/PopupBridgeAnalytics.swift b/Sources/PopupBridge/Analytics/PopupBridgeAnalytics.swift similarity index 100% rename from Sources/PopupBridge/PopupBridgeAnalytics.swift rename to Sources/PopupBridge/Analytics/PopupBridgeAnalytics.swift From 591154671a7e84f429bd4d099c715fc2bb03637c Mon Sep 17 00:00:00 2001 From: richherrera Date: Fri, 14 Mar 2025 11:18:42 -0600 Subject: [PATCH 31/33] Remove test plan --- PopupBridge.xcodeproj/project.pbxproj | 4 ---- Sources/TestPlan.xctestplan | 24 ------------------------ 2 files changed, 28 deletions(-) delete mode 100644 Sources/TestPlan.xctestplan diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index f826ced..34dcd79 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2A2D701B0D000D550B /* NetworkError.swift */; }; 45FBAF2D2D701DD2000D550B /* NetworkClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */; }; 45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */; }; - 45FBAF312D70C27A000D550B /* TestPlan.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 45FBAF302D70C27A000D550B /* TestPlan.xctestplan */; }; 45FBAF332D70CE9E000D550B /* MockNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */; }; 45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */; }; 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */; }; @@ -51,7 +50,6 @@ 45FBAF2A2D701B0D000D550B /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient_Tests.swift; sourceTree = ""; }; 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionable.swift; sourceTree = ""; }; - 45FBAF302D70C27A000D550B /* TestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = TestPlan.xctestplan; sourceTree = ""; }; 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkClient.swift; sourceTree = ""; }; 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService_Test.swift; sourceTree = ""; }; 4EF7C7DDAB0B99FF28DD6541 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; @@ -174,7 +172,6 @@ isa = PBXGroup; children = ( A775A08C1DEE4EF0009E67C2 /* PopupBridge */, - 45FBAF302D70C27A000D550B /* TestPlan.xctestplan */, ); path = Sources; sourceTree = ""; @@ -319,7 +316,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 45FBAF312D70C27A000D550B /* TestPlan.xctestplan in Resources */, 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/TestPlan.xctestplan b/Sources/TestPlan.xctestplan deleted file mode 100644 index bdc3101..0000000 --- a/Sources/TestPlan.xctestplan +++ /dev/null @@ -1,24 +0,0 @@ -{ - "configurations" : [ - { - "id" : "333AB1A9-787A-4C49-9F7C-135ED29D74E5", - "name" : "Configuration 1", - "options" : { - - } - } - ], - "defaultOptions" : { - "testTimeoutsEnabled" : true - }, - "testTargets" : [ - { - "target" : { - "containerPath" : "container:..\/PopupBridge.xcodeproj", - "identifier" : "A775A07B1DEE4E7E009E67C2", - "name" : "UnitTests" - } - } - ], - "version" : 1 -} From 7bc9147146efdde1c6fb0f03a15b1f9302fd0bb2 Mon Sep 17 00:00:00 2001 From: richherrera Date: Tue, 18 Mar 2025 13:08:39 -0600 Subject: [PATCH 32/33] Address feedback --- PopupBridge.xcodeproj/project.pbxproj | 12 ---- .../Analytics/AnalyticsService.swift | 24 ++++++-- Sources/PopupBridge/NetworkClient.swift | 36 ------------ UnitTests/AnalyticsService_Test.swift | 42 +++++++++----- UnitTests/MockNetworkClient.swift | 17 ------ UnitTests/MockSessionable.swift | 29 ++++++---- UnitTests/NetworkClient_Tests.swift | 57 ------------------- 7 files changed, 66 insertions(+), 151 deletions(-) delete mode 100644 Sources/PopupBridge/NetworkClient.swift delete mode 100644 UnitTests/MockNetworkClient.swift delete mode 100644 UnitTests/NetworkClient_Tests.swift diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 34dcd79..a49d78e 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 454C7EC32D6E351F00CA3191 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 454C7EC22D6E351900CA3191 /* NetworkClient.swift */; }; 45AE41D22D7649F800388548 /* MockAnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */; }; 45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */; }; 45CD0C2E2D664D140072C5A4 /* Date+MilisecondTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */; }; @@ -16,9 +15,7 @@ 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; }; 45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF282D701AF7000D550B /* Sessionable.swift */; }; 45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2A2D701B0D000D550B /* NetworkError.swift */; }; - 45FBAF2D2D701DD2000D550B /* NetworkClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */; }; 45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */; }; - 45FBAF332D70CE9E000D550B /* MockNetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */; }; 45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */; }; 62D5EC522B9F753100D09C5D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */; }; 79DB9F7F53319F206CDE119E /* Pods_UnitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */; }; @@ -39,7 +36,6 @@ /* Begin PBXFileReference section */ 28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 454C7EC22D6E351900CA3191 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalyticsService.swift; sourceTree = ""; }; 45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = ""; }; 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+MilisecondTimestamp.swift"; sourceTree = ""; }; @@ -48,9 +44,7 @@ 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; 45FBAF282D701AF7000D550B /* Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sessionable.swift; sourceTree = ""; }; 45FBAF2A2D701B0D000D550B /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; - 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient_Tests.swift; sourceTree = ""; }; 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionable.swift; sourceTree = ""; }; - 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkClient.swift; sourceTree = ""; }; 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService_Test.swift; sourceTree = ""; }; 4EF7C7DDAB0B99FF28DD6541 /* Pods-UnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UnitTests.debug.xcconfig"; path = "Target Support Files/Pods-UnitTests/Pods-UnitTests.debug.xcconfig"; sourceTree = ""; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; @@ -182,12 +176,10 @@ 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */, A775A0801DEE4E7E009E67C2 /* Info.plist */, 45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */, - 45FBAF322D70CE94000D550B /* MockNetworkClient.swift */, BE2524552A17FB9F00168D77 /* MockScriptMessage.swift */, 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */, BE2524532A17DFCC00168D77 /* MockUserContentController.swift */, BEF9ED202A2A4896005D54AB /* MockWebAuthenticationSession.swift */, - 45FBAF2C2D701DD2000D550B /* NetworkClient_Tests.swift */, BE2524512A17DF8200168D77 /* PopupBridge_UnitTests.swift */, ); path = UnitTests; @@ -199,7 +191,6 @@ 45FC74C32D84922F00E50035 /* Analytics */, 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */, 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */, - 454C7EC22D6E351900CA3191 /* NetworkClient.swift */, 45FBAF2A2D701B0D000D550B /* NetworkError.swift */, 800A09D72995F143003ED16E /* POPPopupBridge.swift */, A79330F01DF0F98F00EE479D /* PopupBridge-Framework-Info.plist */, @@ -358,9 +349,7 @@ BE2524562A17FB9F00168D77 /* MockScriptMessage.swift in Sources */, BEF9ED212A2A4896005D54AB /* MockWebAuthenticationSession.swift in Sources */, BE2524522A17DF8200168D77 /* PopupBridge_UnitTests.swift in Sources */, - 45FBAF332D70CE9E000D550B /* MockNetworkClient.swift in Sources */, 45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */, - 45FBAF2D2D701DD2000D550B /* NetworkClient_Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -374,7 +363,6 @@ 45CD0C302D6788A10072C5A4 /* Bundle+Extension.swift in Sources */, 45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */, BEF9ED232A2A6A2C005D54AB /* PopupBridgeConstants.swift in Sources */, - 454C7EC32D6E351F00CA3191 /* NetworkClient.swift in Sources */, 800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */, BE8E37B62A17B79E00181FDA /* WebAuthenticationSession.swift in Sources */, 45CD0C2C2D64F08F0072C5A4 /* FPTIBatchData.swift in Sources */, diff --git a/Sources/PopupBridge/Analytics/AnalyticsService.swift b/Sources/PopupBridge/Analytics/AnalyticsService.swift index 98ceed3..c5ad439 100644 --- a/Sources/PopupBridge/Analytics/AnalyticsService.swift +++ b/Sources/PopupBridge/Analytics/AnalyticsService.swift @@ -10,12 +10,12 @@ final class AnalyticsService: AnalyticsServiceable { /// The FPTI URL to post all analytic events. private let url = URL(string: "https://api.paypal.com/v1/tracking/batch/events")! - private let networkClient: Networkable + private let session: Sessionable // MARK: - Initializer - init(networkClient: Networkable = NetworkClient()) { - self.networkClient = networkClient + init(session: Sessionable = URLSession.shared) { + self.session = session } // MARK: - Internal Methods @@ -29,7 +29,7 @@ final class AnalyticsService: AnalyticsServiceable { func performEventRequest(_ eventName: String, sessionID: String) async { let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID) do { - try await networkClient.post(url: url, body: body) + try await post(url: url, body: body) } catch { NSLog("[PopupBridge SDK] Failed to send analytics: %@", error.localizedDescription) } @@ -43,4 +43,20 @@ final class AnalyticsService: AnalyticsServiceable { let event = FPTIBatchData.Event(eventName: eventName) return FPTIBatchData(metadata: batchMetadata, events: [event]) } + + private func post(url: URL, body: FPTIBatchData) async throws { + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.allHTTPHeaderFields = ["Content-Type": "application/json"] + + let encodedBody = try JSONEncoder().encode(body) + request.httpBody = encodedBody + + let (_, response) = try await session.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw NetworkError.invalidResponse + } + } } diff --git a/Sources/PopupBridge/NetworkClient.swift b/Sources/PopupBridge/NetworkClient.swift deleted file mode 100644 index 701312e..0000000 --- a/Sources/PopupBridge/NetworkClient.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation - -protocol Networkable { - func post(url: URL, body: FPTIBatchData) async throws -} - -final class NetworkClient: Networkable { - - // MARK: - Private Properties - - private let session: Sessionable - - // MARK: - Initializer - - init(session: Sessionable = URLSession.shared) { - self.session = session - } - - // MARK: - Internal Methods - - func post(url: URL, body: FPTIBatchData) async throws { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.allHTTPHeaderFields = ["Content-Type": "application/json"] - - let encodedBody = try JSONEncoder().encode(body) - request.httpBody = encodedBody - - let (_, response) = try await session.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - throw NetworkError.invalidResponse - } - } -} diff --git a/UnitTests/AnalyticsService_Test.swift b/UnitTests/AnalyticsService_Test.swift index 82b3d00..1d2aea3 100644 --- a/UnitTests/AnalyticsService_Test.swift +++ b/UnitTests/AnalyticsService_Test.swift @@ -4,33 +4,45 @@ import XCTest class AnalyticsService_Test: XCTestCase { var sut: AnalyticsService! - var mockNetworkClient: MockNetworkClient! + var mockSession: MockSession! + + let eventName = "some-event-name" + let sessionID = "some-session-id" + let testURL = URL(string: "https://api.paypal.com/v1/tracking/batch/events")! override func setUp() { super.setUp() - mockNetworkClient = MockNetworkClient() - sut = AnalyticsService(networkClient: mockNetworkClient) + mockSession = MockSession() + sut = AnalyticsService(session: mockSession) } override func tearDown() { sut = nil - mockNetworkClient = nil + mockSession = nil super.tearDown() } - func testPerformEventRequest() async { - await sut.performEventRequest("some-event", sessionID: "some-session-id") + func testPerformEventRequest_handleSuccess() async { + mockSession.response = (data: Data(), response: HTTPURLResponse(url: testURL, statusCode: 200, httpVersion: nil, headerFields: nil)!) + + await sut.performEventRequest(eventName, sessionID: sessionID) + + let decodedResponse = decodeData(mockSession.requestedBody!) - XCTAssertEqual(mockNetworkClient.lastURL, URL(string: "https://api.paypal.com/v1/tracking/batch/events")) - XCTAssertNotNil(mockNetworkClient.lastBody) + XCTAssertEqual(mockSession.requestedURL, testURL) + XCTAssertEqual(mockSession.requestHttpMethod, "POST") + XCTAssertEqual(mockSession.requestAllHTTPHeaderFields, ["Content-Type": "application/json"]) + XCTAssertEqual(decodedResponse?.events.first?.fptiEvents.first?.eventName, eventName) + XCTAssertEqual(decodedResponse?.events.first?.metadata.sessionID, sessionID) } - func testPerformEventRequest_handlesError() async { - mockNetworkClient.throwError = NetworkError.invalidResponse - - await sut.performEventRequest("some-event", sessionID: "some-session-id") - - XCTAssertEqual(mockNetworkClient.lastURL, URL(string: "https://api.paypal.com/v1/tracking/batch/events")) - XCTAssertNotNil(mockNetworkClient.lastBody) + func decodeData(_ data: Data) -> FPTIBatchData? { + let decoder = JSONDecoder() + do { + let event = try decoder.decode(FPTIBatchData.self, from: data) + return event + } catch { + return nil + } } } diff --git a/UnitTests/MockNetworkClient.swift b/UnitTests/MockNetworkClient.swift deleted file mode 100644 index 467964e..0000000 --- a/UnitTests/MockNetworkClient.swift +++ /dev/null @@ -1,17 +0,0 @@ -import Foundation -@testable import PopupBridge - -class MockNetworkClient: Networkable { - - var lastURL: URL? - var lastBody: Encodable? - var throwError: Error? - - func post(url: URL, body: T) async throws where T : Encodable { - lastURL = url - lastBody = body - if let error = throwError { - throw error - } - } -} diff --git a/UnitTests/MockSessionable.swift b/UnitTests/MockSessionable.swift index faed8bf..860689e 100644 --- a/UnitTests/MockSessionable.swift +++ b/UnitTests/MockSessionable.swift @@ -3,17 +3,26 @@ import Foundation class MockSession: Sessionable { - var mockData: Data? - var mockResponse: URLResponse? + var requestedURL: URL? + var requestedBody: Data? + var requestHttpMethod: String? + var requestAllHTTPHeaderFields: [String: String] = [:] + var response: (data: Data, response: URLResponse)? + var error: Error? func data(for request: URLRequest, delegate: (any URLSessionTaskDelegate)?) async throws -> (Data, URLResponse) { - (mockData ?? Data(), mockResponse ?? URLResponse()) - } -} - -struct NonEncodable: Encodable { - - func encode(to encoder: Encoder) throws { - throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: [], debugDescription: "Non-Encodable type")) + + requestedURL = request.url + requestedBody = request.httpBody + requestHttpMethod = request.httpMethod + requestAllHTTPHeaderFields = request.allHTTPHeaderFields ?? [:] + + if let error { + throw error + } else if let response { + return response + } else { + throw NetworkError.invalidResponse + } } } diff --git a/UnitTests/NetworkClient_Tests.swift b/UnitTests/NetworkClient_Tests.swift deleted file mode 100644 index cb19e77..0000000 --- a/UnitTests/NetworkClient_Tests.swift +++ /dev/null @@ -1,57 +0,0 @@ -@testable import PopupBridge -import XCTest - -class NetworkClient_Tests: XCTestCase { - - var sut: NetworkClient! - var mockSession: MockSession! - - let body = FPTIBatchData( - metadata: .init(sessionID: "some-session-id"), - events: [.init(eventName: "some-event-name")] - ) - - override func setUp() { - super.setUp() - mockSession = MockSession() - sut = NetworkClient(session: mockSession) - } - - override func tearDown() { - sut = nil - mockSession = nil - super.tearDown() - } - - func testPost_success() async throws { - let url = URL(string: "https://example.com/api/post")! - let expectedData = Data("success response".utf8) - let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! - - mockSession.mockData = expectedData - mockSession.mockResponse = response - - do { - try await sut.post(url: url, body: body) - } catch { - XCTFail("Post should succeed but failed with error: \(error)") - } - } - - func testPost_failureWithInvalidResponse() async throws { - let url = URL(string: "https://example.com/api/post")! - let response = HTTPURLResponse(url: url, statusCode: 404, httpVersion: nil, headerFields: nil)! - - mockSession.mockData = Data() - mockSession.mockResponse = response - - do { - try await sut.post(url: url, body: body) - XCTFail("Post should have failed due to invalid HTTP response status") - } catch NetworkError.invalidResponse { - XCTAssert(true) - } catch { - XCTFail("Unexpected error type: \(error)") - } - } -} From ec82e7c2a8f73d2ea95771ad59d9d93853a241f5 Mon Sep 17 00:00:00 2001 From: richherrera Date: Tue, 18 Mar 2025 13:11:43 -0600 Subject: [PATCH 33/33] Sort files --- PopupBridge.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PopupBridge.xcodeproj/project.pbxproj b/PopupBridge.xcodeproj/project.pbxproj index 18cd529..ca5dc08 100644 --- a/PopupBridge.xcodeproj/project.pbxproj +++ b/PopupBridge.xcodeproj/project.pbxproj @@ -191,7 +191,6 @@ isa = PBXGroup; children = ( 45FC74C32D84922F00E50035 /* Analytics */, - 45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */, 45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */, 45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */, 45FBAF2A2D701B0D000D550B /* NetworkError.swift */, @@ -201,6 +200,7 @@ 8079D7192996F6C200A2E336 /* PopupBridgeUserScript.swift */, 62D5EC512B9F753100D09C5D /* PrivacyInfo.xcprivacy */, 45FBAF282D701AF7000D550B /* Sessionable.swift */, + 45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */, 800E789E29E09A2A00D1B0FC /* URLDetailsPayload.swift */, BE8E37B52A17B79E00181FDA /* WebAuthenticationSession.swift */, 800E789C29E0958A00D1B0FC /* WebViewMessage.swift */,