Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shopper insights rp2 feature #1502

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
6b3450b
[RP1][iOS] Expose PayPal and Venmo App Installed for Merchants (#1473)
stechiu Dec 6, 2024
cabadbb
Add Shopper Session ID to `BTShopperInsightsClient` [RP1] (#1479)
stechiu Dec 9, 2024
8ef01a7
Shopper insights rp1 feature include session (#1481)
warmkesselj Dec 9, 2024
786d57e
If a shopperSessionID exists, override the value with the one retriev…
warmkesselj Dec 11, 2024
2a00f6e
ShopperInsights - send `shopper_session_id` to FPTI (#1485)
scannillo Dec 16, 2024
7b5d6b6
Merge branch 'main' of https://github.com/braintree/braintree_ios int…
stechiu Dec 16, 2024
b02bb43
Shopper insights rp1 feature fixes from main merge (#1490)
warmkesselj Dec 20, 2024
365a059
Shopper Insights - Added Presentment Details Object and Update Presen…
stechiu Dec 20, 2024
d4c7a21
Shopper Insights - Update `sendSelected` Events (#1486)
stechiu Dec 20, 2024
f23a881
remove paymentMethodsDisplayed (#1494)
jaxdesmarais Jan 9, 2025
264aa27
Merge branch 'main' of https://github.com/braintree/braintree_ios int…
warmkesselj Jan 14, 2025
b3fa502
Merge branch 'shopper-insights-rp1-feature' into shopper-insights-rp2…
warmkesselj Jan 14, 2025
a89c638
PR comments
warmkesselj Jan 15, 2025
aecc495
Update Sources/BraintreeShopperInsights/BTPresentmentDetails.swift
warmkesselj Jan 22, 2025
f6c5710
Update Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift
warmkesselj Jan 22, 2025
a35ef44
Update Sources/BraintreeShopperInsights/BTShopperInsightsClient.swift
warmkesselj Jan 22, 2025
e8c9b98
Update comments
warmkesselj Jan 22, 2025
5df0119
Merge branch 'shopper-insights-rp2-feature' of https://github.com/bra…
warmkesselj Jan 22, 2025
f0d6ec6
Merge branch 'main' of https://github.com/braintree/braintree_ios int…
warmkesselj Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -729,6 +734,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 @@ -1494,8 +1504,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 @@ -3364,11 +3379,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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@
## unreleased
* BraintreePayPal
* Fix bug to ensure that `BTPayPalVaultRequest.userAuthenticationEmail` is not sent as an empty string
* Add `shopperSessionID` to `BTPayPalCheckoutRequest` and `BTPayPalVaultRequest`
* BraintreeThreeDSecure
* Return error if no `dfReferenceId` is returned in the 3D Secure flow
* 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.25.0 (2024-12-11)
* 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: "123456")
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