Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d272d2c
Change FPTIBatchData structure
richherrera Feb 26, 2025
304190d
Add NetworkClient and AnalyticsServices files
richherrera Feb 26, 2025
03f9365
Add Sessionable and NetworkError files
richherrera Feb 27, 2025
52fd81c
Add Network errors
richherrera Feb 27, 2025
6224055
Add Sessionable Protocol and Extension
richherrera Feb 27, 2025
4a4e97f
Sort files
richherrera Feb 27, 2025
4f87b20
Add NetworkClient
richherrera Feb 27, 2025
0d71ffb
Implement AnalyticsService class
richherrera Feb 27, 2025
060e7e4
Add Private Mark
richherrera Feb 27, 2025
ddb944c
Add necessary files (TestPlan, mock files and UT files)
richherrera Feb 27, 2025
37ea28a
Setup Test Plan
richherrera Feb 27, 2025
05cf278
Create MockSession class and NonEncodable structure
richherrera Feb 27, 2025
6310175
Add NetworkClient tests
richherrera Feb 27, 2025
b03bdac
Create MockNetworkClient class
richherrera Feb 27, 2025
d6fc4fc
Sort files
richherrera Feb 27, 2025
022530d
Add missing blank space
richherrera Feb 27, 2025
c9e846c
Add UTs to test success and failures
richherrera Feb 27, 2025
292affb
Remove iOS 15 validation
richherrera Feb 27, 2025
5308717
Add analytics calls
richherrera Feb 28, 2025
a268cd0
Set sessionID value
richherrera Mar 3, 2025
9734fb3
Add AnalyticsService property
richherrera Mar 3, 2025
3ba4d21
Add MockAnalyticsService
richherrera Mar 3, 2025
700f695
Fix PopupBridge tests adding mock
richherrera Mar 3, 2025
fd33535
Increase time interval
richherrera Mar 3, 2025
d0a8d8d
Add MockAnalyticsService
richherrera Mar 4, 2025
147dc9b
Add PopupBrdige UTs
richherrera Mar 4, 2025
9a5e980
Strongly type FPTIBatchData
richherrera Mar 5, 2025
a658633
Add catch block
richherrera Mar 6, 2025
70bb98e
Update Sources/PopupBridge/FPTIBatchData.swift
richherrera Mar 10, 2025
c8fd4cf
Add analytics folder
richherrera Mar 14, 2025
5911546
Remove test plan
richherrera Mar 14, 2025
7bc9147
Address feedback
richherrera Mar 18, 2025
39a61a6
Merge branch 'v3' into add-networking
richherrera Mar 18, 2025
ec82e7c
Sort files
richherrera Mar 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Demo/UITests/PopupBridge_DemoUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
38 changes: 35 additions & 3 deletions PopupBridge.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
objects = {

/* Begin PBXBuildFile section */
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 */; };
45CD0C322D6793FB0072C5A4 /* PopupBridgeAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */; };
45FBAF272D6F9CCF000D550B /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */; };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add all of the new files that were specifically added for analytics to an Analytics folder under Sources/PopupBridge? nitpick, but would help navigation of the PopupBridge source files

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated: c8fd4cf

45FBAF292D701AFE000D550B /* Sessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF282D701AF7000D550B /* Sessionable.swift */; };
45FBAF2B2D701B12000D550B /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2A2D701B0D000D550B /* NetworkError.swift */; };
45FBAF2F2D701E24000D550B /* MockSessionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF2E2D701E1D000D550B /* MockSessionable.swift */; };
45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */; };
45FC74C22D8347B500E50035 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.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 */; };
Expand All @@ -31,10 +37,16 @@

/* Begin PBXFileReference section */
28245E4F1AC5126D54985D88 /* Pods_UnitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UnitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAnalyticsService.swift; sourceTree = "<group>"; };
45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPTIBatchData.swift; sourceTree = "<group>"; };
45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+MilisecondTimestamp.swift"; sourceTree = "<group>"; };
45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extension.swift"; sourceTree = "<group>"; };
45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopupBridgeAnalytics.swift; sourceTree = "<group>"; };
45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = "<group>"; };
45FBAF282D701AF7000D550B /* Sessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sessionable.swift; sourceTree = "<group>"; };
45FBAF2A2D701B0D000D550B /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = "<group>"; };
45FBAF2E2D701E1D000D550B /* MockSessionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSessionable.swift; sourceTree = "<group>"; };
45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsService_Test.swift; sourceTree = "<group>"; };
45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+URLOpener.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -96,6 +108,16 @@
path = Pods;
sourceTree = "<group>";
};
45FC74C32D84922F00E50035 /* Analytics */ = {
isa = PBXGroup;
children = (
45FBAF262D6F9CC6000D550B /* AnalyticsService.swift */,
45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.swift */,
45CD0C312D6793F90072C5A4 /* PopupBridgeAnalytics.swift */,
);
path = Analytics;
sourceTree = "<group>";
};
6003F581195388D10070C39A = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -153,8 +175,11 @@
A775A07D1DEE4E7E009E67C2 /* UnitTests */ = {
isa = PBXGroup;
children = (
45FBAF342D70CFB6000D550B /* AnalyticsService_Test.swift */,
A775A0801DEE4E7E009E67C2 /* Info.plist */,
45AE41D12D7649EE00388548 /* MockAnalyticsService.swift */,
BE2524552A17FB9F00168D77 /* MockScriptMessage.swift */,
45FBAF2E2D701E1D000D550B /* MockSessionable.swift */,
BE2524532A17DFCC00168D77 /* MockUserContentController.swift */,
BEF9ED202A2A4896005D54AB /* MockWebAuthenticationSession.swift */,
BE2524512A17DF8200168D77 /* PopupBridge_UnitTests.swift */,
Expand All @@ -165,16 +190,17 @@
A775A08C1DEE4EF0009E67C2 /* PopupBridge */ = {
isa = PBXGroup;
children = (
45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */,
45FC74C32D84922F00E50035 /* Analytics */,
45CD0C2F2D67888F0072C5A4 /* Bundle+Extension.swift */,
45CD0C2D2D664CF50072C5A4 /* Date+MilisecondTimestamp.swift */,
45CD0C2B2D64F0810072C5A4 /* FPTIBatchData.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 */,
45FC74C12D8347AA00E50035 /* UIApplication+URLOpener.swift */,
800E789E29E09A2A00D1B0FC /* URLDetailsPayload.swift */,
BE8E37B52A17B79E00181FDA /* WebAuthenticationSession.swift */,
800E789C29E0958A00D1B0FC /* WebViewMessage.swift */,
Expand Down Expand Up @@ -320,19 +346,25 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
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 */,
45FBAF352D70CFB6000D550B /* AnalyticsService_Test.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
A775A0861DEE4EF0009E67C2 /* Sources */ = {
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 */,
BEF9ED232A2A6A2C005D54AB /* PopupBridgeConstants.swift in Sources */,
45FC74C22D8347B500E50035 /* UIApplication+URLOpener.swift in Sources */,
800A09D82995F143003ED16E /* POPPopupBridge.swift in Sources */,
Expand Down
62 changes: 62 additions & 0 deletions Sources/PopupBridge/Analytics/AnalyticsService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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 session: Sessionable

// MARK: - Initializer

init(session: Sessionable = URLSession.shared) {
self.session = session
}

// MARK: - Internal Methods

func sendAnalyticsEvent(_ eventName: String, sessionID: String) {
Task(priority: .background) {
await performEventRequest(eventName, sessionID: sessionID)
}
}

func performEventRequest(_ eventName: String, sessionID: String) async {
let body = createAnalyticsEvent(eventName: eventName, sessionID: sessionID)
do {
try await post(url: url, body: body)
} catch {
NSLog("[PopupBridge SDK] Failed to send analytics: %@", error.localizedDescription)
}
}

// MARK: - Private Methods

/// Constructs POST params to be sent to FPTI
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])
}

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
}
}
}
111 changes: 111 additions & 0 deletions Sources/PopupBridge/Analytics/FPTIBatchData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import UIKit

struct FPTIBatchData: Codable {

let events: [EventsContainer]

init(metadata: Metadata, events fptiEvents: [Event]) {
self.events = [
EventsContainer(
metadata: metadata,
fptiEvents: fptiEvents
)
]
}

struct EventsContainer: Codable {

let metadata: Metadata
let fptiEvents: [Event]

enum CodingKeys: String, CodingKey {
case metadata = "batch_params"
case fptiEvents = "event_params"
}
}

/// 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"
}
}

/// 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"
}
}
}
78 changes: 0 additions & 78 deletions Sources/PopupBridge/FPTIBatchData.swift

This file was deleted.

5 changes: 5 additions & 0 deletions Sources/PopupBridge/NetworkError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

enum NetworkError: Error {
case invalidResponse

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on passing the statusCode so it can be logged?

case invalidResponse(statusCode: Int)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this way it remains as basic as possible, which is what's expected, since im using this in the NetworkClient, the scope of httpResponse is limited within the guard block, so i cannot directly use it in the throw statement outside of that block. If in the future we need to add more endpoints, I think that would add more value.

}
Loading