diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d21b71a18..156f060520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,9 @@ ## unreleased * BraintreePayPal * Add `BTPayPalRequest.userPhoneNumber` optional property - +* BraintreeVenmo + * Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI) + ## 6.24.0 (2024-10-15) * BraintreePayPal * Add `BTPayPalRecurringBillingDetails` and `BTPayPalRecurringBillingPlanType` opt-in request objects. Including these details will provide transparency to users on their billing schedule, dates, and amounts, as well as launch a modernized checkout UI. diff --git a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift index 64b55b6a07..0284172c39 100644 --- a/Sources/BraintreeCore/Analytics/FPTIBatchData.swift +++ b/Sources/BraintreeCore/Analytics/FPTIBatchData.swift @@ -29,6 +29,7 @@ struct FPTIBatchData: Codable { /// Encapsulates a single event by it's name and timestamp. struct Event: Codable { + let appSwitchURL: String? /// UTC millisecond timestamp when a networking task started establishing a TCP connection. See [Apple's docs](https://developer.apple.com/documentation/foundation/urlsessiontasktransactionmetrics#3162615). /// `nil` if a persistent connection is used. let connectionStartTime: Int? @@ -61,6 +62,7 @@ struct FPTIBatchData: Codable { let tenantName: String = "Braintree" init( + appSwitchURL: URL? = nil, connectionStartTime: Int? = nil, correlationID: String? = nil, endpoint: String? = nil, @@ -76,6 +78,7 @@ struct FPTIBatchData: Codable { requestStartTime: Int? = nil, startTime: Int? = nil ) { + self.appSwitchURL = appSwitchURL?.absoluteString self.connectionStartTime = connectionStartTime self.correlationID = correlationID self.endpoint = endpoint @@ -93,6 +96,7 @@ struct FPTIBatchData: Codable { } enum CodingKeys: String, CodingKey { + case appSwitchURL = "url" case connectionStartTime = "connect_start_time" case correlationID = "correlation_id" case errorDescription = "error_desc" diff --git a/Sources/BraintreeCore/BTAPIClient.swift b/Sources/BraintreeCore/BTAPIClient.swift index 5076de18d3..1d3b04f4dc 100644 --- a/Sources/BraintreeCore/BTAPIClient.swift +++ b/Sources/BraintreeCore/BTAPIClient.swift @@ -310,10 +310,12 @@ import Foundation isVaultRequest: Bool? = nil, linkType: LinkType? = nil, paymentMethodsDisplayed: String? = nil, - payPalContextID: String? = nil + payPalContextID: String? = nil, + appSwitchURL: URL? = nil ) { analyticsService.sendAnalyticsEvent( FPTIBatchData.Event( + appSwitchURL: appSwitchURL, correlationID: correlationID, errorDescription: errorDescription, eventName: eventName, diff --git a/Sources/BraintreeVenmo/BTVenmoAnalytics.swift b/Sources/BraintreeVenmo/BTVenmoAnalytics.swift index 3d824b761b..2a937b6e98 100644 --- a/Sources/BraintreeVenmo/BTVenmoAnalytics.swift +++ b/Sources/BraintreeVenmo/BTVenmoAnalytics.swift @@ -9,8 +9,13 @@ enum BTVenmoAnalytics { static let tokenizeSucceeded = "venmo:tokenize:succeeded" static let appSwitchCanceled = "venmo:tokenize:app-switch:canceled" - // MARK: - Additional Detail Events + // MARK: - Additional Conversion events + + static let handleReturnStarted = "venmo:tokenize:handle-return:started" + + // MARK: - App Switch events + static let appSwitchStarted = "venmo:tokenize:app-switch:started" static let appSwitchSucceeded = "venmo:tokenize:app-switch:succeeded" static let appSwitchFailed = "venmo:tokenize:app-switch:failed" } diff --git a/Sources/BraintreeVenmo/BTVenmoClient.swift b/Sources/BraintreeVenmo/BTVenmoClient.swift index da09e31dfc..f5fa291508 100644 --- a/Sources/BraintreeVenmo/BTVenmoClient.swift +++ b/Sources/BraintreeVenmo/BTVenmoClient.swift @@ -286,7 +286,14 @@ import BraintreeCore // MARK: - App Switch Methods + // swiftlint:disable:next function_body_length func handleOpen(_ url: URL) { + apiClient.sendAnalyticsEvent( + BTVenmoAnalytics.handleReturnStarted, + isVaultRequest: shouldVault, + linkType: linkType, + payPalContextID: payPalContextID + ) guard let cleanedURL = URL(string: url.absoluteString.replacingOccurrences(of: "#", with: "?")) else { notifyFailure(with: BTVenmoError.invalidReturnURL(url.absoluteString), completion: appSwitchCompletion) return @@ -364,14 +371,21 @@ import BraintreeCore } func startVenmoFlow(with appSwitchURL: URL, shouldVault vault: Bool, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) { + apiClient.sendAnalyticsEvent( + BTVenmoAnalytics.appSwitchStarted, + isVaultRequest: shouldVault, + linkType: linkType, + payPalContextID: payPalContextID + ) application.open(appSwitchURL) { success in - self.invokedOpenURLSuccessfully(success, shouldVault: vault, completion: completion) + self.invokedOpenURLSuccessfully(success, shouldVault: vault, appSwitchURL: appSwitchURL, completion: completion) } } func invokedOpenURLSuccessfully( _ success: Bool, shouldVault vault: Bool, + appSwitchURL: URL, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void ) { shouldVault = success && vault @@ -381,7 +395,8 @@ import BraintreeCore BTVenmoAnalytics.appSwitchSucceeded, isVaultRequest: shouldVault, linkType: linkType, - payPalContextID: payPalContextID + payPalContextID: payPalContextID, + appSwitchURL: appSwitchURL ) BTVenmoClient.venmoClient = self self.appSwitchCompletion = completion @@ -390,7 +405,8 @@ import BraintreeCore BTVenmoAnalytics.appSwitchFailed, isVaultRequest: shouldVault, linkType: linkType, - payPalContextID: payPalContextID + payPalContextID: payPalContextID, + appSwitchURL: appSwitchURL ) notifyFailure(with: BTVenmoError.appSwitchFailed, completion: completion) } diff --git a/UnitTests/BraintreeTestShared/MockAPIClient.swift b/UnitTests/BraintreeTestShared/MockAPIClient.swift index f796388e84..77ff070ec2 100644 --- a/UnitTests/BraintreeTestShared/MockAPIClient.swift +++ b/UnitTests/BraintreeTestShared/MockAPIClient.swift @@ -17,6 +17,7 @@ public class MockAPIClient: BTAPIClient { public var postedIsVaultRequest = false public var postedMerchantExperiment: String? = nil public var postedPaymentMethodsDisplayed: String? = nil + public var postedAppSwitchURL: [String: String?] = [:] @objc public var cannedConfigurationResponseBody : BTJSON? = nil @objc public var cannedConfigurationResponseError : NSError? = nil @@ -99,13 +100,15 @@ public class MockAPIClient: BTAPIClient { isVaultRequest: Bool? = nil, linkType: LinkType? = nil, paymentMethodsDisplayed: String? = nil, - payPalContextID: String? = nil + payPalContextID: String? = nil, + appSwitchURL: URL? = nil ) { postedPayPalContextID = payPalContextID postedLinkType = linkType postedIsVaultRequest = isVaultRequest ?? false postedMerchantExperiment = experiment postedPaymentMethodsDisplayed = paymentMethodsDisplayed + postedAppSwitchURL[name] = appSwitchURL?.absoluteString postedAnalyticsEvents.append(name) } diff --git a/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift b/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift index 16ef497bd4..e9101a4380 100644 --- a/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift +++ b/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift @@ -783,6 +783,42 @@ class BTVenmoClient_Tests: XCTestCase { XCTAssertFalse(mockAPIClient.postedIsVaultRequest) } + func testHandleOpen_sendsHandleReturnStartedEvent() { + let venmoClient = BTVenmoClient(apiClient: mockAPIClient) + let appSwitchURL = URL(string: "some-url")! + venmoClient.handleOpen(appSwitchURL) + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, BTVenmoAnalytics.handleReturnStarted) + } + + func testStartVenmoFlow_sendsAppSwitchStartedEvent() { + let venmoClient = BTVenmoClient(apiClient: mockAPIClient) + let appSwitchURL = URL(string: "some-url")! + venmoClient.startVenmoFlow(with: appSwitchURL, shouldVault: false) { _, _ in } + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, BTVenmoAnalytics.appSwitchStarted) + } + + func testInvokedOpenURLSuccessfully_whenSuccess_sendsAppSwitchSucceeded_withAppSwitchURL() { + let venmoClient = BTVenmoClient(apiClient: mockAPIClient) + let eventName = BTVenmoAnalytics.appSwitchSucceeded + let appSwitchURL = URL(string: "some-url")! + venmoClient.invokedOpenURLSuccessfully(true, shouldVault: true, appSwitchURL: appSwitchURL) { _, _ in } + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, eventName) + XCTAssertEqual(mockAPIClient.postedAppSwitchURL[eventName], appSwitchURL.absoluteString) + } + + func testInvokedOpenURLSuccessfully_whenFailure_sendsAppSwitchFailed_withAppSwitchURL() { + let venmoClient = BTVenmoClient(apiClient: mockAPIClient) + let eventName = BTVenmoAnalytics.appSwitchFailed + let appSwitchURL = URL(string: "some-url")! + venmoClient.invokedOpenURLSuccessfully(false, shouldVault: true, appSwitchURL: appSwitchURL) { _, _ in } + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first!, eventName) + XCTAssertEqual(mockAPIClient.postedAppSwitchURL[eventName], appSwitchURL.absoluteString) + } + // MARK: - BTAppContextSwitchClient func testIsiOSAppSwitchAvailable_whenApplicationCanOpenVenmoURL_returnsTrue() {