Skip to content

Commit

Permalink
Add Additional Parameters To ShopperInsights Button Analytics (#1415)
Browse files Browse the repository at this point in the history
* add additional analytic params to shopper insights flow for testing

* address pr comments

* address pr feedback

* add analytics tests

* cleanup

* cleanup, add new param, and related unit test

* cleanup docstrings

* add changelog entry

* cleanup

* cleanup

* cleanup

* clarify docstrings

* fix switflint warnings

* code cleanup

* address pr feedback

* cleanup

* address comments

* resolve build failure
  • Loading branch information
agedd authored Sep 24, 2024
1 parent 3bb0b5e commit 80e71e5
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 23 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## unreleased
* BraintreePayPal
* Send `isVaultRequest` for App Switch events to PayPal's analytics service (FPTI)
* BraintreeShopperInsights (BETA)
* For analytics, send `experiment` as a parameter to `getRecommendedPaymentMethods` method
* For analytics, send `experiment` and `paymentMethodsDisplayed` analytic metrics to FPTI via the button presented methods

## 6.23.3 (2024-08-12)
* BraintreeCore
Expand Down
24 changes: 20 additions & 4 deletions Demo/Application/Features/ShopperInsightsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false

shopperInsightsClient.sendPayPalPresentedEvent()
shopperInsightsClient.sendVenmoPresentedEvent()

return stackView
}

Expand All @@ -85,7 +82,15 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {
)
Task {
do {
let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request)
let sampleExperiment =
"""
[
{ "experimentName" : "payment ready conversion" },
{ "experimentID" : "a1b2c3" },
{ "treatmentName" : "control group 1" }
]
"""
let result = try await shopperInsightsClient.getRecommendedPaymentMethods(request: request, experiment: sampleExperiment)
// swiftlint:disable:next line_length
progressBlock("PayPal Recommended: \(result.isPayPalRecommended)\nVenmo Recommended: \(result.isVenmoRecommended)\nEligible in PayPal Network: \(result.isEligibleInPayPalNetwork)")
payPalVaultButton.isEnabled = result.isPayPalRecommended
Expand All @@ -97,6 +102,16 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {
}

@objc func payPalVaultButtonTapped(_ button: UIButton) {
let sampleExperiment =
"""
[
{ "experimentName" : "payment ready conversion experiment" },
{ "experimentID" : "a1b2c3" },
{ "treatmentName" : "treatment group 1" }
]
"""
let paymentMethods = ["Apple Pay", "Card", "PayPal"]
shopperInsightsClient.sendPayPalPresentedEvent(paymentMethodsDisplayed: paymentMethods, experiment: sampleExperiment)
progressBlock("Tapped PayPal Vault")
shopperInsightsClient.sendPayPalSelectedEvent()

Expand All @@ -113,6 +128,7 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {
}

@objc func venmoButtonTapped(_ button: UIButton) {
shopperInsightsClient.sendVenmoPresentedEvent()
progressBlock("Tapped Venmo")
shopperInsightsClient.sendVenmoSelectedEvent()

Expand Down
11 changes: 10 additions & 1 deletion Sources/BraintreeCore/Analytics/FPTIBatchData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ struct FPTIBatchData: Codable {
struct Event: Codable {

/// 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?
let correlationID: String?
Expand All @@ -46,6 +45,10 @@ struct FPTIBatchData: Codable {
let isVaultRequest: Bool?
/// The type of link the SDK will be handling, currently deeplink or universal
let linkType: String?
/// The experiment details associated with a shopper insights flow
let merchantExperiment: String?
/// The list of payment methods displayed, in the same order in which they are rendered on the page, associated with the `BTShopperInsights` flow.
let paymentMethodsDisplayed: String?
/// Used for linking events from the client to server side request
/// This value will be PayPal Order ID, Payment Token, EC token, Billing Agreement, or Venmo Context ID depending on the flow
let payPalContextID: String?
Expand All @@ -67,6 +70,8 @@ struct FPTIBatchData: Codable {
isConfigFromCache: Bool? = nil,
isVaultRequest: Bool? = nil,
linkType: String? = nil,
merchantExperiment: String? = nil,
paymentMethodsDisplayed: String? = nil,
payPalContextID: String? = nil,
requestStartTime: Int? = nil,
startTime: Int? = nil
Expand All @@ -80,6 +85,8 @@ struct FPTIBatchData: Codable {
self.isConfigFromCache = isConfigFromCache
self.isVaultRequest = isVaultRequest
self.linkType = linkType
self.merchantExperiment = merchantExperiment
self.paymentMethodsDisplayed = paymentMethodsDisplayed
self.payPalContextID = payPalContextID
self.requestStartTime = requestStartTime
self.startTime = startTime
Expand All @@ -93,6 +100,8 @@ struct FPTIBatchData: Codable {
case isConfigFromCache = "config_cached"
case isVaultRequest = "is_vault"
case linkType = "link_type"
case merchantExperiment = "experiment"
case paymentMethodsDisplayed = "payment_methods_displayed"
case payPalContextID = "paypal_context_id"
case requestStartTime = "request_start_time"
case timestamp = "t"
Expand Down
4 changes: 4 additions & 0 deletions Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,11 @@ import Foundation
_ eventName: String,
correlationID: String? = nil,
errorDescription: String? = nil,
merchantExperiment: String? = nil,
isConfigFromCache: Bool? = nil,
isVaultRequest: Bool? = nil,
linkType: LinkType? = nil,
paymentMethodsDisplayed: String? = nil,
payPalContextID: String? = nil
) {
analyticsService.sendAnalyticsEvent(
Expand All @@ -318,6 +320,8 @@ import Foundation
isConfigFromCache: isConfigFromCache,
isVaultRequest: isVaultRequest,
linkType: linkType?.rawValue,
merchantExperiment: merchantExperiment,
paymentMethodsDisplayed: paymentMethodsDisplayed,
payPalContextID: payPalContextID
)
)
Expand Down
62 changes: 46 additions & 16 deletions Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,23 @@ public class BTShopperInsightsClient {

/// This method confirms if the customer is a user of PayPal services using their email and phone number.
/// - Parameters:
/// - request: A `BTShopperInsightsRequest` containing the buyer's user information
/// - request: Required: A `BTShopperInsightsRequest` containing the buyer's user information.
/// - experiment: Optional: A `JSONObject` passed in as a string containing details of the merchant experiment.
/// - Returns: A `BTShopperInsightsResult` instance
/// - Warning: This feature is in beta. Its public API may change or be removed in future releases.
/// PayPal recommendation is only available for US, AU, FR, DE, ITA, NED, ESP, Switzerland and UK merchants.
/// Venmo recommendation is only available for US merchants.
public func getRecommendedPaymentMethods(request: BTShopperInsightsRequest) async throws -> BTShopperInsightsResult {
apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsStarted)
public func getRecommendedPaymentMethods(
request: BTShopperInsightsRequest,
experiment: String? = nil
) async throws -> BTShopperInsightsResult {
apiClient.sendAnalyticsEvent(
BTShopperInsightsAnalytics.recommendedPaymentsStarted,
merchantExperiment: experiment
)

if apiClient.authorization.type != .clientToken {
throw notifyFailure(with: BTShopperInsightsError.invalidAuthorization)
throw notifyFailure(with: BTShopperInsightsError.invalidAuthorization, for: experiment)
}

let postParameters = BTEligiblePaymentsRequest(
Expand All @@ -57,7 +64,7 @@ public class BTShopperInsightsClient {
let eligibleMethodsJSON = json?["eligible_methods"].asDictionary(),
eligibleMethodsJSON.count != 0
else {
throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned)
throw self.notifyFailure(with: BTShopperInsightsError.emptyBodyReturned, for: experiment)
}
// swiftlint:enable empty_count

Expand All @@ -69,16 +76,24 @@ public class BTShopperInsightsClient {
isVenmoRecommended: venmo?.recommended ?? false,
isEligibleInPayPalNetwork: payPal?.eligibleInPayPalNetwork ?? false || venmo?.eligibleInPayPalNetwork ?? false
)
return self.notifySuccess(with: result)
return self.notifySuccess(with: result, for: experiment)
} catch {
throw self.notifyFailure(with: error)
throw self.notifyFailure(with: error, for: experiment)
}
}

/// Call this method when the PayPal button has been successfully displayed to the buyer.
/// This method sends analytics to help improve the Shopper Insights feature experience.
public func sendPayPalPresentedEvent() {
apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.payPalPresented)
/// - Parameters:
/// - paymentMethodsDisplayed: Optional: The list of available payment methods, rendered in the same order in which they are displayed i.e. ['Apple Pay', 'PayPal']
/// - experiment: Optional: A `JSONObject` passed in as a string containing details of the merchant experiment.
public func sendPayPalPresentedEvent(paymentMethodsDisplayed: [String?] = [], experiment: String? = nil) {
let paymentMethodsDisplayedString = paymentMethodsDisplayed.compactMap { $0 }.joined(separator: ", ")
apiClient.sendAnalyticsEvent(
BTShopperInsightsAnalytics.payPalPresented,
merchantExperiment: experiment,
paymentMethodsDisplayed: paymentMethodsDisplayedString
)
}

/// Call this method when the PayPal button has been selected/tapped by the buyer.
Expand All @@ -88,9 +103,17 @@ public class BTShopperInsightsClient {
}

/// Call this method when the Venmo button has been successfully displayed to the buyer.
/// This method sends analytics to help improve the Shopper Insights feature experience
public func sendVenmoPresentedEvent() {
apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.venmoPresented)
/// This method sends analytics to help improve the Shopper Insights feature experience.
/// - Parameters:
/// - paymentMethodsDisplayed: Optional: The list of available payment methods, rendered in the same order in which they are displayed.
/// - experiment: Optional: A `JSONObject` passed in as a string containing details of the merchant experiment.
public func sendVenmoPresentedEvent(paymentMethodsDisplayed: [String?] = [], experiment: String? = nil) {
let paymentMethodsDisplayedString = paymentMethodsDisplayed.compactMap { $0 }.joined(separator: ", ")
apiClient.sendAnalyticsEvent(
BTShopperInsightsAnalytics.venmoPresented,
merchantExperiment: experiment,
paymentMethodsDisplayed: paymentMethodsDisplayedString
)
}

/// Call this method when the Venmo button has been selected/tapped by the buyer.
Expand All @@ -101,13 +124,20 @@ public class BTShopperInsightsClient {

// MARK: - Analytics Helper Methods

private func notifySuccess(with result: BTShopperInsightsResult) -> BTShopperInsightsResult {
apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsSucceeded)
private func notifySuccess(with result: BTShopperInsightsResult, for experiment: String?) -> BTShopperInsightsResult {
apiClient.sendAnalyticsEvent(
BTShopperInsightsAnalytics.recommendedPaymentsSucceeded,
merchantExperiment: experiment
)
return result
}

private func notifyFailure(with error: Error) -> Error {
apiClient.sendAnalyticsEvent(BTShopperInsightsAnalytics.recommendedPaymentsFailed, errorDescription: error.localizedDescription)
private func notifyFailure(with error: Error, for experiment: String?) -> Error {
apiClient.sendAnalyticsEvent(
BTShopperInsightsAnalytics.recommendedPaymentsFailed,
errorDescription: error.localizedDescription,
merchantExperiment: experiment
)
return error
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ class BTShopperInsightsClient_Tests: XCTestCase {
)
)

let sampleExperiment =
"""
[
{
"experimentName" : "payment ready conversion",
"experimentID" : "a1b2c3" ,
"treatmentName" : "control group 1",
}
]
"""

override func setUp() {
super.setUp()
mockAPIClient = MockAPIClient(authorization: clientToken)
Expand Down Expand Up @@ -95,7 +106,7 @@ class BTShopperInsightsClient_Tests: XCTestCase {
}
}

func testGetRecommendedPaymentMethods_whenEligibleInPayPalNetworkTrue_returnsOnlyPayPalRecommended() async {
func testGetRecommendedPaymentMethods_whenEligibleInPayPalNetworkTrueANDMerchantExperimentSet_returnsOnlyPayPalRecommended() async {
do {
let mockPayPalRecommendedResponse = BTJSON(
value: [
Expand All @@ -110,11 +121,12 @@ class BTShopperInsightsClient_Tests: XCTestCase {
]
)
mockAPIClient.cannedResponseBody = mockPayPalRecommendedResponse
let result = try await sut.getRecommendedPaymentMethods(request: request)
let result = try await sut.getRecommendedPaymentMethods(request: request, experiment: sampleExperiment)
XCTAssertTrue(result.isPayPalRecommended)
XCTAssertFalse(result.isVenmoRecommended)
XCTAssertTrue(result.isEligibleInPayPalNetwork)
XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last, "shopper-insights:get-recommended-payments:succeeded")
XCTAssertEqual(mockAPIClient.postedMerchantExperiment, sampleExperiment)
} catch {
XCTFail("An error was not expected.")
}
Expand Down Expand Up @@ -194,6 +206,13 @@ class BTShopperInsightsClient_Tests: XCTestCase {
XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:paypal-presented")
}

func testSendPayPalPresentedEvent_whenPaymentMethodsDisplayedNotNil_sendsAnalytic() {
let paymentMethods = ["Apple Pay", "Card", "PayPal"]
sut.sendPayPalPresentedEvent(paymentMethodsDisplayed: paymentMethods)
XCTAssertEqual(mockAPIClient.postedPaymentMethodsDisplayed, paymentMethods.joined(separator: ", "))
XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:paypal-presented")
}

func testSendPayPalSelectedEvent_sendsAnalytic() {
sut.sendPayPalSelectedEvent()
XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first, "shopper-insights:paypal-selected")
Expand Down
6 changes: 6 additions & 0 deletions UnitTests/BraintreeTestShared/MockAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public class MockAPIClient: BTAPIClient {
public var postedPayPalContextID: String? = nil
public var postedLinkType: LinkType? = nil
public var postedIsVaultRequest = false
public var postedMerchantExperiment: String? = nil
public var postedPaymentMethodsDisplayed: String? = nil

@objc public var cannedConfigurationResponseBody : BTJSON? = nil
@objc public var cannedConfigurationResponseError : NSError? = nil
Expand Down Expand Up @@ -92,14 +94,18 @@ public class MockAPIClient: BTAPIClient {
_ name: String,
correlationID: String? = nil,
errorDescription: String? = nil,
merchantExperiment experiment: String? = nil,
isConfigFromCache: Bool? = nil,
isVaultRequest: Bool? = nil,
linkType: LinkType? = nil,
paymentMethodsDisplayed: String? = nil,
payPalContextID: String? = nil
) {
postedPayPalContextID = payPalContextID
postedLinkType = linkType
postedIsVaultRequest = isVaultRequest ?? false
postedMerchantExperiment = experiment
postedPaymentMethodsDisplayed = paymentMethodsDisplayed
postedAnalyticsEvents.append(name)
}

Expand Down

0 comments on commit 80e71e5

Please sign in to comment.