From 0416807d6af70773a84ed0eef71d3ffd1a43087b Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Thu, 18 Jul 2024 08:32:13 -0500 Subject: [PATCH 01/20] Add print statements for no analytics on VC refresh --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 3 +++ Sources/BraintreeCore/BTHTTP.swift | 1 + 2 files changed, 4 insertions(+) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index b73916eb98..44f478eb01 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -29,10 +29,12 @@ class BTAnalyticsService: Equatable { // MARK: - Initializer init(apiClient: BTAPIClient) { + print("⏰ BTAnalyticsService init") self.apiClient = apiClient self.http = BTHTTP(authorization: apiClient.authorization, customBaseURL: Self.url) Self.timer.eventHandler = { [weak self] in + print("⏰ Timer handler called") guard let self else { return } Task { await self.sendQueuedAnalyticsEvents() @@ -44,6 +46,7 @@ class BTAnalyticsService: Equatable { // MARK: - Deinit deinit { + print("⏰ BTAnalyticsService deinit") Self.timer.suspend() } diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index e67b33e256..bf4e6031f1 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -128,6 +128,7 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { headers: headers ) + print("👏 Network request sent: \(request.url!.path)") self.session.dataTask(with: request) { [weak self] data, response, error in guard let self else { completion?(nil, nil, BTHTTPError.deallocated("BTHTTP")) From 2da5b2101374f92a603a4068ddc9611e77caace5 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 11:41:25 -0500 Subject: [PATCH 02/20] Move BTAnalyticsService to proper singleton pattern --- .../PayPalWebCheckoutViewController.swift | 24 +++++-- .../Analytics/BTAnalyticsEventsStorage.swift | 3 +- .../Analytics/BTAnalyticsService.swift | 31 +++++---- Sources/BraintreeCore/BTAPIClient.swift | 11 +--- Sources/BraintreeCore/BTHTTP.swift | 1 - trigger_duplicate_config_calls.patch | 66 +++++++++++++++++++ 6 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 trigger_duplicate_config_calls.patch diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index f42490775a..420e70df9f 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -98,15 +98,25 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { request.offerPayLater = payLaterToggle.isOn request.intent = newPayPalCheckoutToggle.isOn ? .sale : .authorize - payPalClient.tokenize(request) { nonce, error in - sender.isEnabled = true - - guard let nonce else { - self.progressBlock(error?.localizedDescription) + // Creates a new BTAPIClient on each button click. BTAPIClient lifecycle dies when completion handler executed. + BraintreeDemoMerchantAPIClient.shared.createCustomerAndFetchClientToken { clientToken, error in + guard let clientToken else { + self.progressBlock("Error fetching Client Token") return } - - self.completionBlock(nonce) + + let freshAPIClient = BTAPIClient(authorization: clientToken)! + let newPayPalClient = BTPayPalClient(apiClient: freshAPIClient) + newPayPalClient.tokenize(request) { nonce, error in + sender.isEnabled = true + + guard let nonce else { + self.progressBlock(error?.localizedDescription) + return + } + + self.completionBlock(nonce) + } } } diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift index 7d8344dba3..961275f5aa 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift @@ -10,7 +10,8 @@ actor BTAnalyticsEventsStorage { } var allValues: [FPTIBatchData.Event] { - events + print("🔥 Events: \(events.map { $0.eventName })") + return events } init() { diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 44f478eb01..230bceed84 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -3,6 +3,8 @@ import Foundation class BTAnalyticsService: Equatable { // MARK: - Internal Properties + + static let shared = BTAnalyticsService() // swiftlint:disable force_unwrapping /// The FPTI URL to post all analytic events. @@ -20,34 +22,37 @@ class BTAnalyticsService: Equatable { private static let events = BTAnalyticsEventsStorage() /// Amount of time, in seconds, between batch API requests sent to FPTI - private static let timeInterval = 15 + private static let timeInterval = 5 - private static let timer = RepeatingTimer(timeInterval: timeInterval) + private let timer = RepeatingTimer(timeInterval: timeInterval) - private let apiClient: BTAPIClient - + private var apiClient: BTAPIClient? + // MARK: - Initializer - - init(apiClient: BTAPIClient) { + + private init() { } + + /// Used to inject `BTAPIClient` dependency into `BTAnalyticsService` singleton + func setAPIClient(_ apiClient: BTAPIClient) { print("⏰ BTAnalyticsService init") self.apiClient = apiClient self.http = BTHTTP(authorization: apiClient.authorization, customBaseURL: Self.url) - Self.timer.eventHandler = { [weak self] in + self.timer.eventHandler = { [weak self] in print("⏰ Timer handler called") guard let self else { return } Task { await self.sendQueuedAnalyticsEvents() } } - Self.timer.resume() + self.timer.resume() } // MARK: - Deinit deinit { print("⏰ BTAnalyticsService deinit") - Self.timer.suspend() + self.timer.suspend() } // MARK: - Internal Methods @@ -133,7 +138,7 @@ class BTAnalyticsService: Equatable { // MARK: - Helpers func sendQueuedAnalyticsEvents() async { - if await !BTAnalyticsService.events.isEmpty { + if await !BTAnalyticsService.events.isEmpty, let apiClient { do { let configuration = try await apiClient.fetchConfiguration() let postParameters = await createAnalyticsEvent( @@ -152,12 +157,12 @@ class BTAnalyticsService: Equatable { /// Constructs POST params to be sent to FPTI func createAnalyticsEvent(config: BTConfiguration, sessionID: String, events: [FPTIBatchData.Event]) -> Codable { let batchMetadata = FPTIBatchData.Metadata( - authorizationFingerprint: apiClient.authorization.type == .clientToken ? apiClient.authorization.bearer : nil, + authorizationFingerprint: apiClient?.authorization.type == .clientToken ? apiClient?.authorization.bearer : nil, environment: config.fptiEnvironment, - integrationType: apiClient.metadata.integration.stringValue, + integrationType: apiClient?.metadata.integration.stringValue ?? BTClientMetadataIntegration.custom.stringValue, merchantID: config.merchantID, sessionID: sessionID, - tokenizationKey: apiClient.authorization.type == .tokenizationKey ? apiClient.authorization.originalValue : nil + tokenizationKey: apiClient?.authorization.type == .tokenizationKey ? apiClient?.authorization.originalValue : nil ) return FPTIBatchData(metadata: batchMetadata, events: events) diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index cb646b2449..bfa66117a7 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -25,14 +25,7 @@ import Foundation var configurationLoader: ConfigurationLoader /// Exposed for testing analytics - /// By default, the `BTAnalyticsService` instance is static/shared so that only one queue of events exists. - /// The "singleton" is managed here because the analytics service depends on `BTAPIClient`. - weak var analyticsService: BTAnalyticsService? { - get { BTAPIClient._analyticsService } - set { BTAPIClient._analyticsService = newValue } - } - - private static var _analyticsService: BTAnalyticsService? + weak var analyticsService: BTAnalyticsService? = BTAnalyticsService.shared // MARK: - Initializers @@ -65,7 +58,7 @@ import Foundation configurationLoader = ConfigurationLoader(http: btHttp) super.init() - BTAPIClient._analyticsService = BTAnalyticsService(apiClient: self) + analyticsService?.setAPIClient(self) http?.networkTimingDelegate = self // Kickoff the background request to fetch the config diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift index 99a1fd1107..7815e4e310 100644 --- a/Sources/BraintreeCore/BTHTTP.swift +++ b/Sources/BraintreeCore/BTHTTP.swift @@ -130,7 +130,6 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { headers: headers ) - print("👏 Network request sent: \(request.url!.path)") self.session.dataTask(with: request) { [weak self] data, response, error in guard let self else { completion?(nil, nil, BTHTTPError.deallocated("BTHTTP")) diff --git a/trigger_duplicate_config_calls.patch b/trigger_duplicate_config_calls.patch new file mode 100644 index 0000000000..5d1ccec3e0 --- /dev/null +++ b/trigger_duplicate_config_calls.patch @@ -0,0 +1,66 @@ +diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift +index f42490775..f44130189 100644 +--- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift ++++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift +@@ -97,17 +97,41 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { + request.lineItems = [lineItem] + request.offerPayLater = payLaterToggle.isOn + request.intent = newPayPalCheckoutToggle.isOn ? .sale : .authorize +- +- payPalClient.tokenize(request) { nonce, error in +- sender.isEnabled = true +- +- guard let nonce else { +- self.progressBlock(error?.localizedDescription) ++ ++ // Flow 1 - Potential (likely) merchant integration pattern to trigger quick sequence of `v1/config` calls. ++ ++ BraintreeDemoMerchantAPIClient.shared.createCustomerAndFetchClientToken { clientToken, error in ++ guard let clientToken else { ++ self.progressBlock("Error fetching Client Token") + return + } ++ ++ let freshAPIClient = BTAPIClient(authorization: clientToken)! ++ let newPayPalClient = BTPayPalClient(apiClient: freshAPIClient) ++ newPayPalClient.tokenize(request) { nonce, error in ++ sender.isEnabled = true + +- self.completionBlock(nonce) ++ guard let nonce else { ++ self.progressBlock(error?.localizedDescription) ++ return ++ } ++ ++ self.completionBlock(nonce) ++ } + } ++ ++ // Flow 2 - Original Demo app setup. Doesn't trigger the issue as easily. ++ ++// payPalClient.tokenize(request) { nonce, error in ++// sender.isEnabled = true ++// ++// guard let nonce else { ++// self.progressBlock(error?.localizedDescription) ++// return ++// } ++// ++// self.completionBlock(nonce) ++// } + } + + // MARK: - Vault Flows +diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift +index 1cc046507..d702a5e19 100644 +--- a/Sources/BraintreeCore/BTHTTP.swift ++++ b/Sources/BraintreeCore/BTHTTP.swift +@@ -398,6 +398,8 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { + path = mutationName + } + ++ print("🔥Path: \(path)") ++ + networkTimingDelegate?.fetchAPITiming( + path: path, + connectionStartTime: transaction.connectStartDate?.utcTimestampMilliseconds, From 9df6e1d6fdeb9e62c358e09c772ba87886b9891b Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 13:42:49 -0500 Subject: [PATCH 03/20] Fixup unit tests --- .../Analytics/BTAnalyticsService.swift | 85 ++++--------------- .../Analytics/FPTIBatchData.swift | 28 +++++- Sources/BraintreeCore/BTAPIClient.swift | 14 +-- .../Analytics/BTAnalyticsService_Tests.swift | 18 ++-- .../Analytics/FPTIBatchData_Tests.swift | 6 +- .../Analytics/FakeAnalyticsService.swift | 25 ++---- .../BTAPIClient_Tests.swift | 14 +-- 7 files changed, 76 insertions(+), 114 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 230bceed84..0a24913d67 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -1,6 +1,13 @@ import Foundation -class BTAnalyticsService: Equatable { +// For testing +protocol AnalyticsSendable: AnyObject { + + func sendAnalyticsEvent(_ event: FPTIBatchData.Event) + func setAPIClient(_ apiClient: BTAPIClient) +} + +class BTAnalyticsService: AnalyticsSendable { // MARK: - Internal Properties @@ -19,10 +26,10 @@ class BTAnalyticsService: Equatable { // MARK: - Private Properties - private static let events = BTAnalyticsEventsStorage() + private let events = BTAnalyticsEventsStorage() /// Amount of time, in seconds, between batch API requests sent to FPTI - private static let timeInterval = 5 + private static let timeInterval = 10 private let timer = RepeatingTimer(timeInterval: timeInterval) @@ -68,67 +75,15 @@ class BTAnalyticsService: Equatable { /// - linkType: Optional. The type of link the SDK will be handling, currently deeplink or universal. /// - payPalContextID: Optional. PayPal Context ID associated with the checkout session. /// - startTime: Optional. The start time of the networking request. - func sendAnalyticsEvent( - _ eventName: String, - connectionStartTime: Int? = nil, - correlationID: String? = nil, - endpoint: String? = nil, - endTime: Int? = nil, - errorDescription: String? = nil, - isVaultRequest: Bool? = nil, - linkType: String? = nil, - payPalContextID: String? = nil, - requestStartTime: Int? = nil, - startTime: Int? = nil - ) { + func sendAnalyticsEvent(_ event: FPTIBatchData.Event) { Task(priority: .background) { - await performEventRequest( - eventName, - connectionStartTime: connectionStartTime, - correlationID: correlationID, - endpoint: endpoint, - endTime: endTime, - errorDescription: errorDescription, - isVaultRequest: isVaultRequest, - linkType: linkType, - payPalContextID: payPalContextID, - requestStartTime: requestStartTime, - startTime: startTime - ) + await performEventRequest(event) } } /// Exposed to be able to execute this function synchronously in unit tests - func performEventRequest( - _ eventName: String, - connectionStartTime: Int? = nil, - correlationID: String? = nil, - endpoint: String? = nil, - endTime: Int? = nil, - errorDescription: String? = nil, - isVaultRequest: Bool? = nil, - linkType: String? = nil, - payPalContextID: String? = nil, - requestStartTime: Int? = nil, - startTime: Int? = nil - ) async { - let timestampInMilliseconds = Date().utcTimestampMilliseconds - let event = FPTIBatchData.Event( - connectionStartTime: connectionStartTime, - correlationID: correlationID, - endpoint: endpoint, - endTime: endTime, - errorDescription: errorDescription, - eventName: eventName, - isVaultRequest: isVaultRequest, - linkType: linkType, - payPalContextID: payPalContextID, - requestStartTime: requestStartTime, - startTime: startTime, - timestamp: String(timestampInMilliseconds) - ) - - await BTAnalyticsService.events.append(event) + func performEventRequest(_ event: FPTIBatchData.Event) async { + await events.append(event) if shouldBypassTimerQueue { await self.sendQueuedAnalyticsEvents() @@ -138,16 +93,16 @@ class BTAnalyticsService: Equatable { // MARK: - Helpers func sendQueuedAnalyticsEvents() async { - if await !BTAnalyticsService.events.isEmpty, let apiClient { + if await !events.isEmpty, let apiClient { do { let configuration = try await apiClient.fetchConfiguration() let postParameters = await createAnalyticsEvent( config: configuration, sessionID: apiClient.metadata.sessionID, - events: Self.events.allValues + events: events.allValues ) http?.post("v1/tracking/batch/events", parameters: postParameters) { _, _, _ in } - await Self.events.removeAll() + await events.removeAll() } catch { return } @@ -167,10 +122,4 @@ class BTAnalyticsService: Equatable { return FPTIBatchData(metadata: batchMetadata, events: events) } - - // MARK: Equitable Protocol Conformance - - static func == (lhs: BTAnalyticsService, rhs: BTAnalyticsService) -> Bool { - lhs.http == rhs.http && lhs.apiClient == rhs.apiClient - } } diff --git a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift index 0083e3380c..4f0e5680b2 100644 --- a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift +++ b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift @@ -53,9 +53,35 @@ struct FPTIBatchData: Codable { let requestStartTime: Int? /// UTC millisecond timestamp when a networking task initiated. let startTime: Int? - let timestamp: String + let timestamp: String = String(Date().utcTimestampMilliseconds) let tenantName: String = "Braintree" let venmoInstalled: Bool = application.isVenmoAppInstalled() + + init( + connectionStartTime: Int? = nil, + correlationID: String? = nil, + endpoint: String? = nil, + endTime: Int? = nil, + errorDescription: String? = nil, + eventName: String, + isVaultRequest: Bool? = nil, + linkType: String? = nil, + payPalContextID: String? = nil, + requestStartTime: Int? = nil, + startTime: Int? = nil + ) { + self.connectionStartTime = connectionStartTime + self.correlationID = correlationID + self.endpoint = endpoint + self.endTime = endTime + self.errorDescription = errorDescription + self.eventName = eventName + self.isVaultRequest = isVaultRequest + self.linkType = linkType + self.payPalContextID = payPalContextID + self.requestStartTime = requestStartTime + self.startTime = startTime + } enum CodingKeys: String, CodingKey { case connectionStartTime = "connect_start_time" diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index bfa66117a7..e58a578af8 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -25,7 +25,7 @@ import Foundation var configurationLoader: ConfigurationLoader /// Exposed for testing analytics - weak var analyticsService: BTAnalyticsService? = BTAnalyticsService.shared + weak var analyticsService: AnalyticsSendable? = BTAnalyticsService.shared // MARK: - Initializers @@ -312,14 +312,14 @@ import Foundation linkType: String? = nil, payPalContextID: String? = nil ) { - analyticsService?.sendAnalyticsEvent( - eventName, + analyticsService?.sendAnalyticsEvent(FPTIBatchData.Event( correlationID: correlationID, errorDescription: errorDescription, + eventName: eventName, isVaultRequest: isVaultRequest, linkType: linkType, payPalContextID: payPalContextID - ) + )) } // MARK: Analytics Internal Methods @@ -397,14 +397,14 @@ import Foundation let cleanedPath = path.replacingOccurrences(of: "/merchants/([A-Za-z0-9]+)/client_api", with: "", options: .regularExpression) if cleanedPath != "/v1/tracking/batch/events" { - analyticsService?.sendAnalyticsEvent( - BTCoreAnalytics.apiRequestLatency, + analyticsService?.sendAnalyticsEvent(FPTIBatchData.Event( connectionStartTime: connectionStartTime, endpoint: cleanedPath, endTime: endTime, + eventName: BTCoreAnalytics.apiRequestLatency, requestStartTime: requestStartTime, startTime: startTime - ) + )) } } } diff --git a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift index f10e5113f7..b8b9c52aa5 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift @@ -15,23 +15,25 @@ final class BTAnalyticsService_Tests: XCTestCase { func testSendAnalyticsEvent_whenConfigFetchCompletes_setsUpAnalyticsHTTPToUseBaseURL() async { let stubAPIClient: MockAPIClient = stubbedAPIClientWithAnalyticsURL("test://do-not-send.url") - let analyticsService = BTAnalyticsService(apiClient: stubAPIClient) + let sut = BTAnalyticsService.shared + sut.setAPIClient(stubAPIClient) - await analyticsService.performEventRequest("any.analytics.event") + await sut.performEventRequest(FPTIBatchData.Event(eventName: "any.analytics.event")) - XCTAssertEqual(analyticsService.http?.customBaseURL?.absoluteString, "https://api.paypal.com") + XCTAssertEqual(sut.http?.customBaseURL?.absoluteString, "https://api.paypal.com") } func testSendAnalyticsEvent_sendsAnalyticsEvent() async { let stubAPIClient: MockAPIClient = stubbedAPIClientWithAnalyticsURL("test://do-not-send.url") let mockAnalyticsHTTP = FakeHTTP.fakeHTTP() - let analyticsService = BTAnalyticsService(apiClient: stubAPIClient) - analyticsService.shouldBypassTimerQueue = true + let sut = BTAnalyticsService.shared + sut.setAPIClient(stubAPIClient) + sut.shouldBypassTimerQueue = true - analyticsService.http = mockAnalyticsHTTP - - await analyticsService.performEventRequest("any.analytics.event") + sut.http = mockAnalyticsHTTP + await sut.performEventRequest(FPTIBatchData.Event(eventName: "any.analytics.event")) + XCTAssertEqual(mockAnalyticsHTTP.lastRequestEndpoint, "v1/tracking/batch/events") let timestamp = parseTimestamp(mockAnalyticsHTTP.lastRequestParameters)! diff --git a/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift index 24d0eaea6f..b44a1d3041 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift @@ -27,8 +27,7 @@ final class FPTIBatchData_Tests: XCTestCase { linkType: "universal", payPalContextID: "fake-order-id", requestStartTime: 456, - startTime: 999888777666, - timestamp: "fake-time-1" + startTime: 999888777666 ), FPTIBatchData.Event( connectionStartTime: nil, @@ -41,8 +40,7 @@ final class FPTIBatchData_Tests: XCTestCase { linkType: nil, payPalContextID: "fake-order-id-2", requestStartTime: nil, - startTime: nil, - timestamp: "fake-time-2" + startTime: nil ) ] diff --git a/UnitTests/BraintreeCoreTests/Analytics/FakeAnalyticsService.swift b/UnitTests/BraintreeCoreTests/Analytics/FakeAnalyticsService.swift index 13a2318144..7688468394 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/FakeAnalyticsService.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/FakeAnalyticsService.swift @@ -1,24 +1,17 @@ import Foundation @testable import BraintreeCore -class FakeAnalyticsService: BTAnalyticsService { +class FakeAnalyticsService: AnalyticsSendable { + var lastEvent: String? = nil var endpoint: String? = nil + + func setAPIClient(_ apiClient: BraintreeCore.BTAPIClient) { + // No-Op + } - override func sendAnalyticsEvent( - _ eventName: String, - connectionStartTime: Int? = nil, - correlationID: String? = nil, - endpoint: String? = nil, - endTime: Int? = nil, - errorDescription: String? = nil, - isVaultRequest: Bool? = nil, - linkType: String? = nil, - payPalContextID: String? = nil, - requestStartTime: Int? = nil, - startTime: Int? = nil - ) { - self.lastEvent = eventName - self.endpoint = endpoint + func sendAnalyticsEvent(_ event: FPTIBatchData.Event) { + self.lastEvent = event.eventName + self.endpoint = event.endpoint } } diff --git a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift index dd770fe504..7a8661367d 100644 --- a/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTAPIClient_Tests.swift @@ -288,12 +288,6 @@ class BTAPIClient_Tests: XCTestCase { // MARK: - Analytics - func testAnalyticsService_byDefault_isASingleton() { - let firstAPIClient = BTAPIClient(authorization: "development_testing_integration_merchant_id") - let secondAPIClient = BTAPIClient(authorization: "development_testing_integration_merchant_id") - XCTAssertEqual(firstAPIClient?.analyticsService, secondAPIClient?.analyticsService) - } - func testAnalyticsService_isCreatedDuringInitialization() { let apiClient = BTAPIClient(authorization: "development_tokenization_key") XCTAssertTrue(apiClient?.analyticsService is BTAnalyticsService) @@ -301,7 +295,7 @@ class BTAPIClient_Tests: XCTestCase { func testSendAnalyticsEvent_whenCalled_callsAnalyticsService() { let apiClient = BTAPIClient(authorization: "development_tokenization_key")! - let mockAnalyticsService = FakeAnalyticsService(apiClient: apiClient) + let mockAnalyticsService = FakeAnalyticsService() apiClient.analyticsService = mockAnalyticsService apiClient.sendAnalyticsEvent("blahblah") @@ -311,7 +305,7 @@ class BTAPIClient_Tests: XCTestCase { func testFetchAPITiming_whenConfigurationPathIsValid_sendsLatencyEvent() { let apiClient = BTAPIClient(authorization: "development_tokenization_key")! - let mockAnalyticsService = FakeAnalyticsService(apiClient: apiClient) + let mockAnalyticsService = FakeAnalyticsService() apiClient.analyticsService = mockAnalyticsService apiClient.fetchAPITiming( @@ -328,7 +322,7 @@ class BTAPIClient_Tests: XCTestCase { func testFetchAPITiming_whenPathIsBatchEvents_doesNotSendLatencyEvent() { let apiClient = BTAPIClient(authorization: "development_tokenization_key")! - let mockAnalyticsService = FakeAnalyticsService(apiClient: apiClient) + let mockAnalyticsService = FakeAnalyticsService() apiClient.analyticsService = mockAnalyticsService apiClient.fetchAPITiming( @@ -345,7 +339,7 @@ class BTAPIClient_Tests: XCTestCase { func testFetchAPITiming_whenPathIsNotBatchEvents_sendLatencyEvent() { let apiClient = BTAPIClient(authorization: "development_tokenization_key")! - let mockAnalyticsService = FakeAnalyticsService(apiClient: apiClient) + let mockAnalyticsService = FakeAnalyticsService() apiClient.analyticsService = mockAnalyticsService apiClient.fetchAPITiming( From cb2cf6686e3f3b8222b7ceddd5465cc732dcebea Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 13:55:37 -0500 Subject: [PATCH 04/20] Cleanup --- Braintree.xcodeproj/project.pbxproj | 4 ++++ .../BraintreeCore/Analytics/AnalyticsSendable.swift | 7 +++++++ .../Analytics/BTAnalyticsEventsStorage.swift | 3 +-- .../BraintreeCore/Analytics/BTAnalyticsService.swift | 12 +----------- 4 files changed, 13 insertions(+), 13 deletions(-) create mode 100644 Sources/BraintreeCore/Analytics/AnalyticsSendable.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index cbb4ee2e84..da061a903b 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 8075CBEE2B1B735200CA6265 /* BTAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */; }; 80842DA72B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */; }; 8087C10F2BFBACCA0020FC2E /* TokenizationKey_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */; }; + 808E4A162C581CD40006A737 /* AnalyticsSendable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 808E4A152C581CD40006A737 /* AnalyticsSendable.swift */; }; 80A6C6192B21205900416D50 /* UIApplication+URLOpener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */; }; 80AB424F2B27CAC200249218 /* BraintreeTestShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A903E1A624F9D34000C314E1 /* BraintreeTestShared.framework */; platformFilter = ios; }; 80AB42542B27DF5B00249218 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; platformFilter = ios; }; @@ -860,6 +861,7 @@ 8075CBED2B1B735200CA6265 /* BTAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAPIRequest.swift; sourceTree = ""; }; 80842DA62B8E49EF00A5CD92 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 8087C10E2BFBACCA0020FC2E /* TokenizationKey_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationKey_Tests.swift; sourceTree = ""; }; + 808E4A152C581CD40006A737 /* AnalyticsSendable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsSendable.swift; sourceTree = ""; }; 80A1EE3D2236AAC600F6218B /* BTThreeDSecureAdditionalInformation_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecureAdditionalInformation_Tests.swift; sourceTree = ""; }; 80A6C6182B21205900416D50 /* UIApplication+URLOpener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+URLOpener.swift"; sourceTree = ""; }; 80AD35F12BFBB1DD00BF890E /* TokenizationKeyError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenizationKeyError.swift; sourceTree = ""; }; @@ -1591,6 +1593,7 @@ 80F4F4D529F8A628003EACB1 /* Analytics */ = { isa = PBXGroup; children = ( + 808E4A152C581CD40006A737 /* AnalyticsSendable.swift */, BE549F132BF6576300B6F441 /* BTAnalyticsEventsStorage.swift */, BEE2E4E329007FF100C03FDD /* BTAnalyticsService.swift */, BEE2E4E5290080BD00C03FDD /* BTAnalyticsServiceError.swift */, @@ -3318,6 +3321,7 @@ 574891E9286F7D300020DA36 /* BTClientMetadataSource.swift in Sources */, BED7493628579BAC0074C818 /* BTURLUtils.swift in Sources */, BEBD05222A1FE8BE0003C15C /* BTWebAuthenticationSession.swift in Sources */, + 808E4A162C581CD40006A737 /* AnalyticsSendable.swift in Sources */, BE698EA028AA8DCB001D9B10 /* BTHTTP.swift in Sources */, BC17F9B428D23C5C004B18CC /* BTGraphQLMultiErrorNode.swift in Sources */, 5708E0A828809BC6007946B9 /* BTJSONError.swift in Sources */, diff --git a/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift b/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift new file mode 100644 index 0000000000..5e7fe13474 --- /dev/null +++ b/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift @@ -0,0 +1,7 @@ +/// Describes a class that batches and sends analytics events. +/// Note: - Specifically created to be able to mock the BTAnalyticsService singleton. +protocol AnalyticsSendable: AnyObject { + + func sendAnalyticsEvent(_ event: FPTIBatchData.Event) + func setAPIClient(_ apiClient: BTAPIClient) +} diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift index 961275f5aa..7d8344dba3 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsEventsStorage.swift @@ -10,8 +10,7 @@ actor BTAnalyticsEventsStorage { } var allValues: [FPTIBatchData.Event] { - print("🔥 Events: \(events.map { $0.eventName })") - return events + events } init() { diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 0a24913d67..9526c6b099 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -1,12 +1,5 @@ import Foundation -// For testing -protocol AnalyticsSendable: AnyObject { - - func sendAnalyticsEvent(_ event: FPTIBatchData.Event) - func setAPIClient(_ apiClient: BTAPIClient) -} - class BTAnalyticsService: AnalyticsSendable { // MARK: - Internal Properties @@ -29,7 +22,7 @@ class BTAnalyticsService: AnalyticsSendable { private let events = BTAnalyticsEventsStorage() /// Amount of time, in seconds, between batch API requests sent to FPTI - private static let timeInterval = 10 + private static let timeInterval = 15 private let timer = RepeatingTimer(timeInterval: timeInterval) @@ -41,12 +34,10 @@ class BTAnalyticsService: AnalyticsSendable { /// Used to inject `BTAPIClient` dependency into `BTAnalyticsService` singleton func setAPIClient(_ apiClient: BTAPIClient) { - print("⏰ BTAnalyticsService init") self.apiClient = apiClient self.http = BTHTTP(authorization: apiClient.authorization, customBaseURL: Self.url) self.timer.eventHandler = { [weak self] in - print("⏰ Timer handler called") guard let self else { return } Task { await self.sendQueuedAnalyticsEvents() @@ -58,7 +49,6 @@ class BTAnalyticsService: AnalyticsSendable { // MARK: - Deinit deinit { - print("⏰ BTAnalyticsService deinit") self.timer.suspend() } From 7b36004a3f3e785af7785c3063a9b4749b09c61e Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 13:56:37 -0500 Subject: [PATCH 05/20] Reset Demo --- .../PayPalWebCheckoutViewController.swift | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index 420e70df9f..f42490775a 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -98,25 +98,15 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { request.offerPayLater = payLaterToggle.isOn request.intent = newPayPalCheckoutToggle.isOn ? .sale : .authorize - // Creates a new BTAPIClient on each button click. BTAPIClient lifecycle dies when completion handler executed. - BraintreeDemoMerchantAPIClient.shared.createCustomerAndFetchClientToken { clientToken, error in - guard let clientToken else { - self.progressBlock("Error fetching Client Token") + payPalClient.tokenize(request) { nonce, error in + sender.isEnabled = true + + guard let nonce else { + self.progressBlock(error?.localizedDescription) return } - - let freshAPIClient = BTAPIClient(authorization: clientToken)! - let newPayPalClient = BTPayPalClient(apiClient: freshAPIClient) - newPayPalClient.tokenize(request) { nonce, error in - sender.isEnabled = true - - guard let nonce else { - self.progressBlock(error?.localizedDescription) - return - } - - self.completionBlock(nonce) - } + + self.completionBlock(nonce) } } From 7eb5c6ee13a63d3a5db741c81ef81753f0fc8f31 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 13:59:30 -0500 Subject: [PATCH 06/20] Cleanup docstrings --- .../BraintreeCore/Analytics/AnalyticsSendable.swift | 2 +- .../BraintreeCore/Analytics/BTAnalyticsService.swift | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift b/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift index 5e7fe13474..fa6d6c9337 100644 --- a/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift +++ b/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift @@ -1,5 +1,5 @@ /// Describes a class that batches and sends analytics events. -/// Note: - Specifically created to be able to mock the BTAnalyticsService singleton. +/// - Note: Specifically created to mock the BTAnalyticsService singleton. protocol AnalyticsSendable: AnyObject { func sendAnalyticsEvent(_ event: FPTIBatchData.Event) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 9526c6b099..c07f09fecc 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -55,16 +55,7 @@ class BTAnalyticsService: AnalyticsSendable { // MARK: - Internal Methods /// Sends analytics event to https://api.paypal.com/v1/tracking/batch/events/ via a background task. - /// - Parameters: - /// - eventName: Name of analytic event. - /// - correlationID: Optional. CorrelationID associated with the checkout session. - /// - endpoint: Optional. The endpoint of the API request send during networking requests. - /// - endTime: Optional. The end time of the roundtrip networking request. - /// - errorDescription: Optional. Full error description returned to merchant. - /// - isVaultRequest: Optional. If the Venmo or PayPal request is being vaulted. - /// - linkType: Optional. The type of link the SDK will be handling, currently deeplink or universal. - /// - payPalContextID: Optional. PayPal Context ID associated with the checkout session. - /// - startTime: Optional. The start time of the networking request. + /// - Parameter event: A single `FPTIBatchData.Event` func sendAnalyticsEvent(_ event: FPTIBatchData.Event) { Task(priority: .background) { await performEventRequest(event) From 593da6f0d765ed20fb6fafa7c900efb518df8e49 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Mon, 29 Jul 2024 14:00:10 -0500 Subject: [PATCH 07/20] Delete trigger_duplicate_config_calls.patch --- trigger_duplicate_config_calls.patch | 66 ---------------------------- 1 file changed, 66 deletions(-) delete mode 100644 trigger_duplicate_config_calls.patch diff --git a/trigger_duplicate_config_calls.patch b/trigger_duplicate_config_calls.patch deleted file mode 100644 index 5d1ccec3e0..0000000000 --- a/trigger_duplicate_config_calls.patch +++ /dev/null @@ -1,66 +0,0 @@ -diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift -index f42490775..f44130189 100644 ---- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift -+++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift -@@ -97,17 +97,41 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { - request.lineItems = [lineItem] - request.offerPayLater = payLaterToggle.isOn - request.intent = newPayPalCheckoutToggle.isOn ? .sale : .authorize -- -- payPalClient.tokenize(request) { nonce, error in -- sender.isEnabled = true -- -- guard let nonce else { -- self.progressBlock(error?.localizedDescription) -+ -+ // Flow 1 - Potential (likely) merchant integration pattern to trigger quick sequence of `v1/config` calls. -+ -+ BraintreeDemoMerchantAPIClient.shared.createCustomerAndFetchClientToken { clientToken, error in -+ guard let clientToken else { -+ self.progressBlock("Error fetching Client Token") - return - } -+ -+ let freshAPIClient = BTAPIClient(authorization: clientToken)! -+ let newPayPalClient = BTPayPalClient(apiClient: freshAPIClient) -+ newPayPalClient.tokenize(request) { nonce, error in -+ sender.isEnabled = true - -- self.completionBlock(nonce) -+ guard let nonce else { -+ self.progressBlock(error?.localizedDescription) -+ return -+ } -+ -+ self.completionBlock(nonce) -+ } - } -+ -+ // Flow 2 - Original Demo app setup. Doesn't trigger the issue as easily. -+ -+// payPalClient.tokenize(request) { nonce, error in -+// sender.isEnabled = true -+// -+// guard let nonce else { -+// self.progressBlock(error?.localizedDescription) -+// return -+// } -+// -+// self.completionBlock(nonce) -+// } - } - - // MARK: - Vault Flows -diff --git a/Sources/BraintreeCore/BTHTTP.swift b/Sources/BraintreeCore/BTHTTP.swift -index 1cc046507..d702a5e19 100644 ---- a/Sources/BraintreeCore/BTHTTP.swift -+++ b/Sources/BraintreeCore/BTHTTP.swift -@@ -398,6 +398,8 @@ class BTHTTP: NSObject, URLSessionTaskDelegate { - path = mutationName - } - -+ print("🔥Path: \(path)") -+ - networkTimingDelegate?.fetchAPITiming( - path: path, - connectionStartTime: transaction.connectStartDate?.utcTimestampMilliseconds, From 1ba1867cb19341cc111bcd5ed9a10a33b12d0ceb Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 14:01:03 -0500 Subject: [PATCH 08/20] Cleanup - docstring formatting --- Sources/BraintreeCore/Analytics/AnalyticsSendable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift b/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift index fa6d6c9337..feb56b4c51 100644 --- a/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift +++ b/Sources/BraintreeCore/Analytics/AnalyticsSendable.swift @@ -1,5 +1,5 @@ /// Describes a class that batches and sends analytics events. -/// - Note: Specifically created to mock the BTAnalyticsService singleton. +/// - Note: Specifically created to mock the `BTAnalyticsService` singleton. protocol AnalyticsSendable: AnyObject { func sendAnalyticsEvent(_ event: FPTIBatchData.Event) From 15f5faa88895fce1600980c879391624d090b2d1 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 14:02:42 -0500 Subject: [PATCH 09/20] Fixup - Lint error on CI --- Sources/BraintreeCore/Analytics/FPTIBatchData.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift index 4f0e5680b2..a430ff1113 100644 --- a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift +++ b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift @@ -53,7 +53,7 @@ struct FPTIBatchData: Codable { let requestStartTime: Int? /// UTC millisecond timestamp when a networking task initiated. let startTime: Int? - let timestamp: String = String(Date().utcTimestampMilliseconds) + let timestamp = String(Date().utcTimestampMilliseconds) let tenantName: String = "Braintree" let venmoInstalled: Bool = application.isVenmoAppInstalled() From cb18f4944b09e961cfac311c8f4d4cdbd3b632e9 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 14:25:48 -0500 Subject: [PATCH 10/20] Fixup - failing unit test --- .../BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift index b44a1d3041..9ed00d9f8d 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/FPTIBatchData_Tests.swift @@ -89,8 +89,8 @@ final class FPTIBatchData_Tests: XCTestCase { XCTAssertEqual(batchParams["tokenization_key"] as! String, "fake-auth") // Verify event-level parameters - XCTAssertEqual(eventParams[0]["t"] as? String, "fake-time-1") - XCTAssertEqual(eventParams[1]["t"] as? String, "fake-time-2") + XCTAssertNotNil(eventParams[0]["t"] as? String) + XCTAssertNotNil(eventParams[1]["t"] as? String) XCTAssertEqual(eventParams[0]["event_name"] as? String, "fake-event-1") XCTAssertEqual(eventParams[1]["event_name"] as? String, "fake-event-2") XCTAssertEqual(eventParams[0]["tenant_name"] as? String, "Braintree") From 05152948ec592b5a4b956d7678015ae6b6e3e1e0 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Mon, 29 Jul 2024 14:53:59 -0500 Subject: [PATCH 11/20] Add CHANGELOG entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ce08a0564..bb4af3b806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Braintree iOS SDK Release Notes ## unreleased +* BraintreeCore + * Fix bug where analytics wouldn't send on `BTAPIClient` instantiation on button click + * Update `endpoint` syntax sent to FPTI for 3D Secure and Venmo flows + +## 6.23.1 (2024-07-24) * BraintreeThreeDSecure * Add error code and error message for `exceededTimeoutLimit` * BraintreeCore From 292bdb8c79933e8831fa993b25ae8f4f13d57046 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:12:06 -0500 Subject: [PATCH 12/20] Update Sources/BraintreeCore/Analytics/BTAnalyticsService.swift Co-authored-by: Jax DesMarais-Leder --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index c07f09fecc..8045baee5c 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -37,7 +37,7 @@ class BTAnalyticsService: AnalyticsSendable { self.apiClient = apiClient self.http = BTHTTP(authorization: apiClient.authorization, customBaseURL: Self.url) - self.timer.eventHandler = { [weak self] in + timer.eventHandler = { [weak self] in guard let self else { return } Task { await self.sendQueuedAnalyticsEvents() From 3a7c253b2ed9031c34f2d273276f19a4ed952529 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:12:12 -0500 Subject: [PATCH 13/20] Update Sources/BraintreeCore/Analytics/BTAnalyticsService.swift Co-authored-by: Jax DesMarais-Leder --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 8045baee5c..d4597cb05c 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -43,7 +43,8 @@ class BTAnalyticsService: AnalyticsSendable { await self.sendQueuedAnalyticsEvents() } } - self.timer.resume() + + timer.resume() } // MARK: - Deinit From a1017491a2bf292d2b802c5bb202fb1fe5c16b1f Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 30 Jul 2024 08:14:04 -0500 Subject: [PATCH 14/20] PR Feedback - styling --- .../Analytics/BTAnalyticsService.swift | 2 +- Sources/BraintreeCore/BTAPIClient.swift | 38 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index d4597cb05c..07fa9cdab4 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -50,7 +50,7 @@ class BTAnalyticsService: AnalyticsSendable { // MARK: - Deinit deinit { - self.timer.suspend() + timer.suspend() } // MARK: - Internal Methods diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index b97378471a..7b5241d91a 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -312,14 +312,16 @@ import Foundation linkType: String? = nil, payPalContextID: String? = nil ) { - analyticsService?.sendAnalyticsEvent(FPTIBatchData.Event( - correlationID: correlationID, - errorDescription: errorDescription, - eventName: eventName, - isVaultRequest: isVaultRequest, - linkType: linkType, - payPalContextID: payPalContextID - )) + analyticsService?.sendAnalyticsEvent( + FPTIBatchData.Event( + correlationID: correlationID, + errorDescription: errorDescription, + eventName: eventName, + isVaultRequest: isVaultRequest, + linkType: linkType, + payPalContextID: payPalContextID + ) + ) } // MARK: Analytics Internal Methods @@ -400,16 +402,18 @@ import Foundation with: "payment_methods/three_d_secure", options: .regularExpression ) - + if cleanedPath != "/v1/tracking/batch/events" { - analyticsService?.sendAnalyticsEvent(FPTIBatchData.Event( - connectionStartTime: connectionStartTime, - endpoint: cleanedPath, - endTime: endTime, - eventName: BTCoreAnalytics.apiRequestLatency, - requestStartTime: requestStartTime, - startTime: startTime - )) + analyticsService?.sendAnalyticsEvent( + FPTIBatchData.Event( + connectionStartTime: connectionStartTime, + endpoint: cleanedPath, + endTime: endTime, + eventName: BTCoreAnalytics.apiRequestLatency, + requestStartTime: requestStartTime, + startTime: startTime + ) + ) } } } From 0d5bc7481a6fbf58af63e1d772732ca4d24a5690 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 30 Jul 2024 08:15:11 -0500 Subject: [PATCH 15/20] PR Feedback - method naming --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 4 ++-- .../Analytics/BTAnalyticsService_Tests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 07fa9cdab4..e015460abc 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -59,12 +59,12 @@ class BTAnalyticsService: AnalyticsSendable { /// - Parameter event: A single `FPTIBatchData.Event` func sendAnalyticsEvent(_ event: FPTIBatchData.Event) { Task(priority: .background) { - await performEventRequest(event) + await performEventRequest(with: event) } } /// Exposed to be able to execute this function synchronously in unit tests - func performEventRequest(_ event: FPTIBatchData.Event) async { + func performEventRequest(with event: FPTIBatchData.Event) async { await events.append(event) if shouldBypassTimerQueue { diff --git a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift index b8b9c52aa5..da402f5378 100644 --- a/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift +++ b/UnitTests/BraintreeCoreTests/Analytics/BTAnalyticsService_Tests.swift @@ -18,7 +18,7 @@ final class BTAnalyticsService_Tests: XCTestCase { let sut = BTAnalyticsService.shared sut.setAPIClient(stubAPIClient) - await sut.performEventRequest(FPTIBatchData.Event(eventName: "any.analytics.event")) + await sut.performEventRequest(with: FPTIBatchData.Event(eventName: "any.analytics.event")) XCTAssertEqual(sut.http?.customBaseURL?.absoluteString, "https://api.paypal.com") } @@ -32,7 +32,7 @@ final class BTAnalyticsService_Tests: XCTestCase { sut.http = mockAnalyticsHTTP - await sut.performEventRequest(FPTIBatchData.Event(eventName: "any.analytics.event")) + await sut.performEventRequest(with: FPTIBatchData.Event(eventName: "any.analytics.event")) XCTAssertEqual(mockAnalyticsHTTP.lastRequestEndpoint, "v1/tracking/batch/events") From b2715cdf7cbe11a49df87e0760429990a1b005a8 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 30 Jul 2024 08:17:01 -0500 Subject: [PATCH 16/20] Cleanup - make methods private where possible & make BTAnalyticsService final --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index e015460abc..b11cd2f046 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -72,9 +72,9 @@ class BTAnalyticsService: AnalyticsSendable { } } - // MARK: - Helpers + // MARK: - Private Methods - func sendQueuedAnalyticsEvents() async { + private func sendQueuedAnalyticsEvents() async { if await !events.isEmpty, let apiClient { do { let configuration = try await apiClient.fetchConfiguration() @@ -92,7 +92,7 @@ class BTAnalyticsService: AnalyticsSendable { } /// Constructs POST params to be sent to FPTI - func createAnalyticsEvent(config: BTConfiguration, sessionID: String, events: [FPTIBatchData.Event]) -> Codable { + private func createAnalyticsEvent(config: BTConfiguration, sessionID: String, events: [FPTIBatchData.Event]) -> Codable { let batchMetadata = FPTIBatchData.Metadata( authorizationFingerprint: apiClient?.authorization.type == .clientToken ? apiClient?.authorization.bearer : nil, environment: config.fptiEnvironment, From fa0c3ac69a58fd274ed0be00c1ed86f29da16735 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:19:07 -0500 Subject: [PATCH 17/20] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb4af3b806..1dd21fe6ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## unreleased * BraintreeCore - * Fix bug where analytics wouldn't send on `BTAPIClient` instantiation on button click + * Fix bug where analytics wouldn't send if `BTAPIClient` instantiated on button click * Update `endpoint` syntax sent to FPTI for 3D Secure and Venmo flows ## 6.23.1 (2024-07-24) From 2fb2cf2864600a9e2bf0200d58f1d5cffb9d33e1 Mon Sep 17 00:00:00 2001 From: scannillo <35243507+scannillo@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:19:26 -0500 Subject: [PATCH 18/20] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dd21fe6ab..c9a403f4a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## unreleased * BraintreeCore - * Fix bug where analytics wouldn't send if `BTAPIClient` instantiated on button click + * Fix bug where some analytics wouldn't send if `BTAPIClient` instantiated on button click * Update `endpoint` syntax sent to FPTI for 3D Secure and Venmo flows ## 6.23.1 (2024-07-24) From cdc823fc6fb6014cff7c0f88d2783fe7ae43e044 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 30 Jul 2024 09:43:33 -0500 Subject: [PATCH 19/20] Make BTAnalyticsService final --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index b11cd2f046..094c090db3 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -1,6 +1,6 @@ import Foundation -class BTAnalyticsService: AnalyticsSendable { +final class BTAnalyticsService: AnalyticsSendable { // MARK: - Internal Properties From 691cca3fc8321b49c5b75e28bbdd637e48e79a58 Mon Sep 17 00:00:00 2001 From: Sammy Cannillo Date: Tue, 30 Jul 2024 13:47:43 -0500 Subject: [PATCH 20/20] PR Feedback - move weak reference into BTAnalyticsService --- Sources/BraintreeCore/Analytics/BTAnalyticsService.swift | 2 +- Sources/BraintreeCore/BTAPIClient.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift index 094c090db3..e4eea21d31 100644 --- a/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift +++ b/Sources/BraintreeCore/Analytics/BTAnalyticsService.swift @@ -26,7 +26,7 @@ final class BTAnalyticsService: AnalyticsSendable { private let timer = RepeatingTimer(timeInterval: timeInterval) - private var apiClient: BTAPIClient? + private weak var apiClient: BTAPIClient? // MARK: - Initializer diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 4cc7ce39de..4479a94a05 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -25,7 +25,7 @@ import Foundation var configurationLoader: ConfigurationLoader /// Exposed for testing analytics - weak var analyticsService: AnalyticsSendable? = BTAnalyticsService.shared + var analyticsService: AnalyticsSendable = BTAnalyticsService.shared // MARK: - Initializers @@ -58,7 +58,7 @@ import Foundation configurationLoader = ConfigurationLoader(http: btHttp) super.init() - analyticsService?.setAPIClient(self) + analyticsService.setAPIClient(self) http?.networkTimingDelegate = self // Kickoff the background request to fetch the config @@ -313,7 +313,7 @@ import Foundation linkType: String? = nil, payPalContextID: String? = nil ) { - analyticsService?.sendAnalyticsEvent( + analyticsService.sendAnalyticsEvent( FPTIBatchData.Event( correlationID: correlationID, errorDescription: errorDescription, @@ -406,7 +406,7 @@ import Foundation ) if cleanedPath != "/v1/tracking/batch/events" { - analyticsService?.sendAnalyticsEvent( + analyticsService.sendAnalyticsEvent( FPTIBatchData.Event( connectionStartTime: connectionStartTime, endpoint: cleanedPath,