Skip to content

Commit

Permalink
Merge pull request #1502 from braintree/shopper-insights-rp2-feature
Browse files Browse the repository at this point in the history
Shopper insights feature
  • Loading branch information
jaxdesmarais authored Feb 4, 2025
2 parents f5db1d0 + 8002bed commit d33df60
Show file tree
Hide file tree
Showing 21 changed files with 497 additions and 129 deletions.
20 changes: 20 additions & 0 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
objects = {

/* Begin PBXBuildFile section */
04AA31182D07974D0043ACAB /* BTButtonType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AA31172D0797460043ACAB /* BTButtonType.swift */; };
04AA311A2D0797570043ACAB /* BTPresentmentDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AA31192D0797510043ACAB /* BTPresentmentDetails.swift */; };
04AA311E2D0798FC0043ACAB /* BTPageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AA311D2D0798F70043ACAB /* BTPageType.swift */; };
04AA31202D07990E0043ACAB /* BTButtonOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04AA311F2D07990A0043ACAB /* BTButtonOrder.swift */; };
04B001102D0CF46E00C0060D /* BTExperimentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04B0010F2D0CF46900C0060D /* BTExperimentType.swift */; };
0917F6E42A27BDC700ACED2E /* BTVenmoLineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 096C6B2529CCDCEB00912863 /* BTVenmoLineItem.swift */; };
09357DCB2A2FBEC10096D449 /* BTVenmoLineItem_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09357DCA2A2FBEC10096D449 /* BTVenmoLineItem_Tests.swift */; };
1FEB89E614CB6BF0B9858EE4 /* Pods_Tests_IntegrationTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 85BD589D380436A0C9D1DEC1 /* Pods_Tests_IntegrationTests.framework */; };
Expand Down Expand Up @@ -730,6 +735,11 @@
035A59D91EA5DE97002960C8 /* BTLocalPaymentClient_UnitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BTLocalPaymentClient_UnitTests.swift; sourceTree = "<group>"; };
039A8BD91F9E993500D607E7 /* BTAmericanExpressRewardsBalance_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTAmericanExpressRewardsBalance_Tests.swift; sourceTree = "<group>"; };
03F921C1200EBB200076CD80 /* BTThreeDSecurePostalAddress_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTThreeDSecurePostalAddress_Tests.swift; sourceTree = "<group>"; };
04AA31172D0797460043ACAB /* BTButtonType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTButtonType.swift; sourceTree = "<group>"; };
04AA31192D0797510043ACAB /* BTPresentmentDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPresentmentDetails.swift; sourceTree = "<group>"; };
04AA311D2D0798F70043ACAB /* BTPageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPageType.swift; sourceTree = "<group>"; };
04AA311F2D07990A0043ACAB /* BTButtonOrder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTButtonOrder.swift; sourceTree = "<group>"; };
04B0010F2D0CF46900C0060D /* BTExperimentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTExperimentType.swift; sourceTree = "<group>"; };
09357DCA2A2FBEC10096D449 /* BTVenmoLineItem_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTVenmoLineItem_Tests.swift; sourceTree = "<group>"; };
096C6B2529CCDCEB00912863 /* BTVenmoLineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTVenmoLineItem.swift; sourceTree = "<group>"; };
162174E1192D9220008DC35D /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -1497,8 +1507,13 @@
804698292B27C4D70090878E /* BraintreeShopperInsights */ = {
isa = PBXGroup;
children = (
04AA311F2D07990A0043ACAB /* BTButtonOrder.swift */,
04AA31172D0797460043ACAB /* BTButtonType.swift */,
62EA90482B63071800DD79BC /* BTEligiblePaymentMethods.swift */,
800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */,
04B0010F2D0CF46900C0060D /* BTExperimentType.swift */,
04AA311D2D0798F70043ACAB /* BTPageType.swift */,
04AA31192D0797510043ACAB /* BTPresentmentDetails.swift */,
8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */,
8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */,
624B27F62B6AE0C2000AC08A /* BTShopperInsightsError.swift */,
Expand Down Expand Up @@ -3368,11 +3383,16 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
04AA311E2D0798FC0043ACAB /* BTPageType.swift in Sources */,
804698382B27C53B0090878E /* BTShopperInsightsRequest.swift in Sources */,
04B001102D0CF46E00C0060D /* BTExperimentType.swift in Sources */,
04AA31182D07974D0043ACAB /* BTButtonType.swift in Sources */,
624B27F72B6AE0C2000AC08A /* BTShopperInsightsError.swift in Sources */,
804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */,
800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */,
8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */,
04AA311A2D0797570043ACAB /* BTPresentmentDetails.swift in Sources */,
04AA31202D07990E0043ACAB /* BTButtonOrder.swift in Sources */,
62EA90492B63071800DD79BC /* BTEligiblePaymentMethods.swift in Sources */,
804698392B27C53E0090878E /* BTShopperInsightsResult.swift in Sources */,
);
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
## unreleased
* BraintreeVenmo
* Allow universal links to be set without a return URL scheme (fixes #1505)
* BraintreePayPal
* Add `shopperSessionID` to `BTPayPalCheckoutRequest` and `BTPayPalVaultRequest`
* BraintreeShopperInsights (BETA)
* Add `shopperSessionID` to `BTShopperInsightsClient` initializer
* Add `isPayPalAppInstalled()` and/or `isVenmoAppInstalled()`
* Replace `sendPayPalPresentedEvent()` and `sendPayPalPresentedEvent()` with `sendPresentedEvent(for:presentmentDetails:)`
* Add values to the following parameters to `presentmentDetails`:
* `experimentType`
* `pageType`
* `buttonOrder`
* Replace `sendPayPalSelectedEvent()` and `sendPayPalSelectedEvent()` with `sendSelectedEvent(for:)`

## 6.27.0 (2025-01-23)
* BraintreePayPal
Expand Down
59 changes: 43 additions & 16 deletions Demo/Application/Features/ShopperInsightsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import BraintreeShopperInsights

class ShopperInsightsViewController: PaymentButtonBaseViewController {

lazy var shopperInsightsClient = BTShopperInsightsClient(apiClient: apiClient)
lazy var shopperInsightsClient = BTShopperInsightsClient(apiClient: apiClient, shopperSessionID: "test-shopper-session-id")
lazy var payPalClient = BTPayPalClient(apiClient: apiClient)
lazy var venmoClient = BTVenmoClient(apiClient: apiClient)

lazy var payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(payPalVaultButtonTapped))
lazy var venmoButton = createButton(title: "Venmo", action: #selector(venmoButtonTapped))

private var shopperSessionID = "test-shopper-session-id"

lazy var emailView: TextFieldWithLabel = {
let view = TextFieldWithLabel()
view.label.text = "Email"
Expand Down Expand Up @@ -93,32 +95,58 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {
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
venmoButton.isEnabled = result.isVenmoRecommended

togglePayPalVaultButton(enabled: result.isPayPalRecommended)
toggleVenmoButton(enabled: result.isVenmoRecommended)
} catch {
progressBlock("Error: \(error.localizedDescription)")
}
}
}

private func togglePayPalVaultButton(enabled: Bool) {
payPalVaultButton.isEnabled = enabled

guard enabled else { return }

let presentmentDetails = BTPresentmentDetails(
buttonOrder: .first,
experimentType: .control,
pageType: .about
)

shopperInsightsClient.sendPresentedEvent(
for: .payPal,
presentmentDetails: presentmentDetails
)
}

private func toggleVenmoButton(enabled: Bool) {
venmoButton.isEnabled = enabled

guard enabled else { return }

let presentmentDetails = BTPresentmentDetails(
buttonOrder: .second,
experimentType: .control,
pageType: .about
)

shopperInsightsClient.sendPresentedEvent(
for: .venmo,
presentmentDetails: presentmentDetails
)
}

@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()
shopperInsightsClient.sendSelectedEvent(for: .payPal)

button.setTitle("Processing...", for: .disabled)
button.isEnabled = false

let paypalRequest = BTPayPalVaultRequest()
paypalRequest.shopperSessionID = shopperSessionID
paypalRequest.userAuthenticationEmail = emailView.textField.text

payPalClient.tokenize(paypalRequest) { nonce, error in
Expand All @@ -128,9 +156,8 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {
}

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

button.setTitle("Processing...", for: .disabled)
button.isEnabled = false
Expand Down
28 changes: 22 additions & 6 deletions Sources/BraintreeCore/Analytics/FPTIBatchData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,15 @@ struct FPTIBatchData: Codable {
case fptiEvents = "event_params"
}
}

/// Encapsulates a single event by it's name and timestamp.
struct Event: Codable {

let appSwitchURL: String?
/// The order or ranking in which payment buttons appear.
let buttonOrder: String?
/// The type of button displayed or presented
let buttonType: 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 All @@ -48,21 +52,25 @@ struct FPTIBatchData: Codable {
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?
/// The type of page where the payment button is displayed or where an event occured.
let pageType: 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?

/// UTC millisecond timestamp when a networking task started requesting a resource. See [Apple's docs](https://developer.apple.com/documentation/foundation/urlsessiontasktransactionmetrics#3162615).
let requestStartTime: Int?
/// The Shopper Insights customer session ID created by a merchant's server SDK or graphQL integration.
let shopperSessionID: String?
/// UTC millisecond timestamp when a networking task initiated.
let startTime: Int?
let timestamp = String(Date().utcTimestampMilliseconds)
let tenantName: String = "Braintree"

init(
appSwitchURL: URL? = nil,
buttonOrder: String? = nil,
buttonType: String? = nil,
connectionStartTime: Int? = nil,
correlationID: String? = nil,
endpoint: String? = nil,
Expand All @@ -73,12 +81,15 @@ struct FPTIBatchData: Codable {
isVaultRequest: Bool? = nil,
linkType: String? = nil,
merchantExperiment: String? = nil,
paymentMethodsDisplayed: String? = nil,
pageType: String? = nil,
payPalContextID: String? = nil,
requestStartTime: Int? = nil,
shopperSessionID: String? = nil,
startTime: Int? = nil
) {
self.appSwitchURL = appSwitchURL?.absoluteString
self.buttonOrder = buttonOrder
self.buttonType = buttonType
self.connectionStartTime = connectionStartTime
self.correlationID = correlationID
self.endpoint = endpoint
Expand All @@ -89,14 +100,17 @@ struct FPTIBatchData: Codable {
self.isVaultRequest = isVaultRequest
self.linkType = linkType
self.merchantExperiment = merchantExperiment
self.paymentMethodsDisplayed = paymentMethodsDisplayed
self.pageType = pageType
self.payPalContextID = payPalContextID
self.requestStartTime = requestStartTime
self.shopperSessionID = shopperSessionID
self.startTime = startTime
}

enum CodingKeys: String, CodingKey {
case appSwitchURL = "url"
case buttonOrder = "button_position"
case buttonType = "button_type"
case connectionStartTime = "connect_start_time"
case correlationID = "correlation_id"
case errorDescription = "error_desc"
Expand All @@ -105,11 +119,12 @@ struct FPTIBatchData: Codable {
case isVaultRequest = "is_vault"
case linkType = "link_type"
case merchantExperiment = "experiment"
case paymentMethodsDisplayed = "payment_methods_displayed"
case pageType = "page_type"
case payPalContextID = "paypal_context_id"
case requestStartTime = "request_start_time"
case timestamp = "t"
case tenantName = "tenant_name"
case shopperSessionID = "shopper_session_id"
case startTime = "start_time"
case endTime = "end_time"
case endpoint = "endpoint"
Expand Down Expand Up @@ -178,6 +193,7 @@ struct FPTIBatchData: Codable {

let platform = "iOS"

/// Either a randomly generated session ID or the shopper session ID passed in by a merchant
let sessionID: String

let tokenizationKey: String?
Expand Down
14 changes: 10 additions & 4 deletions Sources/BraintreeCore/BTAPIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,28 +303,34 @@ import Foundation
@_documentation(visibility: private)
public func sendAnalyticsEvent(
_ eventName: String,
appSwitchURL: URL? = nil,
buttonOrder: String? = nil,
buttonType: String? = nil,
correlationID: String? = nil,
errorDescription: String? = nil,
merchantExperiment: String? = nil,
isConfigFromCache: Bool? = nil,
isVaultRequest: Bool? = nil,
linkType: LinkType? = nil,
paymentMethodsDisplayed: String? = nil,
pageType: String? = nil,
payPalContextID: String? = nil,
appSwitchURL: URL? = nil
shopperSessionID: String? = nil
) {
analyticsService.sendAnalyticsEvent(
FPTIBatchData.Event(
appSwitchURL: appSwitchURL,
buttonOrder: buttonOrder,
buttonType: buttonType,
correlationID: correlationID,
errorDescription: errorDescription,
eventName: eventName,
isConfigFromCache: isConfigFromCache,
isVaultRequest: isVaultRequest,
linkType: linkType?.rawValue,
merchantExperiment: merchantExperiment,
paymentMethodsDisplayed: paymentMethodsDisplayed,
payPalContextID: payPalContextID
pageType: pageType,
payPalContextID: payPalContextID,
shopperSessionID: shopperSessionID
)
)
}
Expand Down
Loading

0 comments on commit d33df60

Please sign in to comment.