Skip to content

Commit

Permalink
Add App Switch Analytics events (#1435)
Browse files Browse the repository at this point in the history
* Add app-switch started and handle return events

* Add started and handle events

* Add appSwitchURL property

* Pass appSwitchURL on Success and Failure event

* Add UnitTests

* Update AppSwitchUrl coding key

* Update CHANGELOG

* Revert unnecessary change

* Disable lint validation

* Address CHANGELOG feedback

* Address appSwitchUrl type feedback

* Fix UTs

* Update CHANGELOG.md

Co-authored-by: Jax DesMarais-Leder <[email protected]>

---------

Co-authored-by: Jax DesMarais-Leder <[email protected]>
  • Loading branch information
richherrera and jaxdesmarais authored Oct 21, 2024
1 parent e3051d1 commit 227414e
Show file tree
Hide file tree
Showing 7 changed files with 75 additions and 7 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions Sources/BraintreeCore/Analytics/FPTIBatchData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -61,6 +62,7 @@ struct FPTIBatchData: Codable {
let tenantName: String = "Braintree"

init(
appSwitchURL: URL? = nil,
connectionStartTime: Int? = nil,
correlationID: String? = nil,
endpoint: String? = nil,
Expand All @@ -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
Expand All @@ -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"
Expand Down
4 changes: 3 additions & 1 deletion Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion Sources/BraintreeVenmo/BTVenmoAnalytics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
22 changes: 19 additions & 3 deletions Sources/BraintreeVenmo/BTVenmoClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -381,7 +395,8 @@ import BraintreeCore
BTVenmoAnalytics.appSwitchSucceeded,
isVaultRequest: shouldVault,
linkType: linkType,
payPalContextID: payPalContextID
payPalContextID: payPalContextID,
appSwitchURL: appSwitchURL
)
BTVenmoClient.venmoClient = self
self.appSwitchCompletion = completion
Expand All @@ -390,7 +405,8 @@ import BraintreeCore
BTVenmoAnalytics.appSwitchFailed,
isVaultRequest: shouldVault,
linkType: linkType,
payPalContextID: payPalContextID
payPalContextID: payPalContextID,
appSwitchURL: appSwitchURL
)
notifyFailure(with: BTVenmoError.appSwitchFailed, completion: completion)
}
Expand Down
5 changes: 4 additions & 1 deletion UnitTests/BraintreeTestShared/MockAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
36 changes: 36 additions & 0 deletions UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 227414e

Please sign in to comment.