Skip to content

Commit 74627f4

Browse files
NastassiaRodziknrodzik
andauthored
Add Billing With Purchase analytics tags (#1749)
* Billing with purchase analytics * Add tests * Update changelog * Pass a parameter * Swift Lint exception for BTAPIClient * Fix comment * Fix comments * Add more analytics tags * Update Changelog * Add doc-strings for new properties * Clean up * Fix spacing * Fix spacing * Update Changelog * Improve is_purchase logic * Refactor parameter names * Fix comments * Fix Changelog --------- Co-authored-by: nrodzik <nrodzik@paypal.com>
1 parent 3356a0f commit 74627f4

6 files changed

Lines changed: 109 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Braintree iOS SDK Release Notes
22

33
## unreleased
4+
* BraintreePayPal
5+
* Pass `is_billing_agreement` and `billing_plan_type` to analytics events in order to track the billing with purchase flows.
46
* BraintreeCore
57
* Make `BTPostalAddress` properties publicly accessible (fixes #1751)
68

Sources/BraintreeCore/Analytics/FPTIBatchData.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,12 @@ struct FPTIBatchData: Codable {
6868
let pageType: String?
6969
/// UTC millisecond timestamp when a networking task started requesting a resource. See [Apple's docs](https://developer.apple.com/documentation/foundation/urlsessiontasktransactionmetrics#3162615).
7070
let requestStartTime: Int?
71+
/// Recurring billing plan type, or charge pattern.
72+
let recurringBillingPlanType: String?
7173
/// The Shopper Insights customer session ID created by a merchant's server SDK or graphQL integration.
7274
let shopperSessionID: String?
75+
/// Whether or not billing agreement will be created - customer opted to save PayPal for future purchases and a vaulted billing agreement was created with the charge.
76+
let shouldRequestBillingAgreement: Bool?
7377
/// UTC millisecond timestamp when a networking task initiated.
7478
let startTime: Int?
7579
let timestamp = String(Date().utcTimestampMilliseconds)
@@ -97,8 +101,10 @@ struct FPTIBatchData: Codable {
97101
linkType: String? = nil,
98102
merchantExperiment: String? = nil,
99103
pageType: String? = nil,
104+
recurringBillingPlanType: String? = nil,
100105
requestStartTime: Int? = nil,
101106
shopperSessionID: String? = nil,
107+
shouldRequestBillingAgreement: Bool? = nil,
102108
startTime: Int? = nil
103109
) {
104110
self.applicationState = applicationState
@@ -120,8 +126,10 @@ struct FPTIBatchData: Codable {
120126
self.linkType = linkType
121127
self.merchantExperiment = merchantExperiment
122128
self.pageType = pageType
129+
self.recurringBillingPlanType = recurringBillingPlanType
123130
self.requestStartTime = requestStartTime
124131
self.shopperSessionID = shopperSessionID
132+
self.shouldRequestBillingAgreement = shouldRequestBillingAgreement
125133
self.startTime = startTime
126134
self.fundingSource = fundingSource
127135
}
@@ -144,10 +152,12 @@ struct FPTIBatchData: Codable {
144152
case linkType = "link_type"
145153
case merchantExperiment = "experiment"
146154
case pageType = "page_type"
155+
case recurringBillingPlanType = "billing_plan_type"
147156
case requestStartTime = "request_start_time"
148157
case timestamp = "t"
149158
case tenantName = "tenant_name"
150159
case shopperSessionID = "shopper_session_id"
160+
case shouldRequestBillingAgreement = "is_billing_agreement"
151161
case startTime = "start_time"
152162
case endTime = "end_time"
153163
case endpoint = "endpoint"

Sources/BraintreeCore/BTAPIClient.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import UIKit
22

3-
// swiftlint:disable type_body_length
3+
// swiftlint:disable type_body_length file_length
44
/// This class acts as the entry point for accessing the Braintree APIs via common HTTP methods performed on API endpoints.
55
/// - Note: It also manages authentication via tokenization key and provides access to a merchant's gateway configuration.
66
@objcMembers public class BTAPIClient: NSObject, BTHTTPNetworkTiming {
@@ -269,7 +269,9 @@ import UIKit
269269
linkType: LinkType? = nil,
270270
merchantExperiment: String? = nil,
271271
pageType: String? = nil,
272-
shopperSessionID: String? = nil
272+
recurringBillingPlanType: String? = nil,
273+
shopperSessionID: String? = nil,
274+
shouldRequestBillingAgreement: Bool? = nil
273275
) {
274276
analyticsService?.sendAnalyticsEvent(
275277
FPTIBatchData.Event(
@@ -290,7 +292,9 @@ import UIKit
290292
linkType: linkType?.rawValue,
291293
merchantExperiment: merchantExperiment,
292294
pageType: pageType,
293-
shopperSessionID: shopperSessionID
295+
recurringBillingPlanType: recurringBillingPlanType,
296+
shopperSessionID: shopperSessionID,
297+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
294298
)
295299
)
296300
}

Sources/BraintreePayPal/BTPayPalClient.swift

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ import BraintreeDataCollector
7676
/// Used for analytics purposes, to determine if browser-presentation event is associated with a locally cached, or remotely fetched `BTConfiguration`
7777
private var isConfigFromCache: Bool?
7878

79+
/// Used for analytics purpose to determine if user opted to save PayPal for future purchases and a vaulted billing agreement was created with the charge.
80+
private var shouldRequestBillingAgreement: Bool?
81+
82+
/// Used for analytics purpose to determine if there is a recurring billing plan type, or charge pattern.
83+
private var recurringBillingPlanType: String?
84+
7985
/// Used for analytics purpose to determine if the context type is `BA-TOKEN` or `EC-TOKEN`
8086
private var contextType: String?
8187

@@ -139,6 +145,8 @@ import BraintreeDataCollector
139145
isVaultRequest = true
140146
contextType = "BA-TOKEN"
141147
fundingSource = getFundingSource(from: request)
148+
shouldRequestBillingAgreement = false
149+
recurringBillingPlanType = request.recurringBillingPlanType?.rawValue
142150
tokenize(request: request, completion: completion)
143151
}
144152

@@ -184,6 +192,8 @@ import BraintreeDataCollector
184192
isVaultRequest = false
185193
contextType = "EC-TOKEN"
186194
fundingSource = getFundingSource(from: request)
195+
shouldRequestBillingAgreement = request.requestBillingAgreement
196+
recurringBillingPlanType = request.recurringBillingPlanType?.rawValue
187197
tokenize(request: request, completion: completion)
188198
}
189199

@@ -231,7 +241,9 @@ import BraintreeDataCollector
231241
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
232242
fundingSource: fundingSource?.rawValue,
233243
isVaultRequest: isVaultRequest,
234-
shopperSessionID: payPalRequest?.shopperSessionID
244+
recurringBillingPlanType: recurringBillingPlanType,
245+
shopperSessionID: payPalRequest?.shopperSessionID,
246+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
235247
)
236248

237249
guard
@@ -314,7 +326,9 @@ import BraintreeDataCollector
314326
didEnablePayPalAppSwitch: payPalRequest?.enablePayPalAppSwitch,
315327
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
316328
fundingSource: fundingSource?.rawValue,
317-
isVaultRequest: isVaultRequest
329+
isVaultRequest: isVaultRequest,
330+
recurringBillingPlanType: recurringBillingPlanType,
331+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
318332
)
319333
BTPayPalClient.payPalClient = self
320334
appSwitchCompletion = completion
@@ -328,7 +342,9 @@ import BraintreeDataCollector
328342
didEnablePayPalAppSwitch: payPalRequest?.enablePayPalAppSwitch,
329343
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
330344
fundingSource: fundingSource?.rawValue,
331-
isVaultRequest: isVaultRequest
345+
isVaultRequest: isVaultRequest,
346+
recurringBillingPlanType: recurringBillingPlanType,
347+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
332348
)
333349

334350
openURLInDefaultBrowser(url, completion: completion)
@@ -346,7 +362,9 @@ import BraintreeDataCollector
346362
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
347363
fundingSource: fundingSource?.rawValue,
348364
isVaultRequest: isVaultRequest,
349-
shopperSessionID: payPalRequest?.shopperSessionID
365+
recurringBillingPlanType: recurringBillingPlanType,
366+
shopperSessionID: payPalRequest?.shopperSessionID,
367+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
350368
)
351369

352370
application.open(url, options: [:]) { success in
@@ -370,7 +388,9 @@ import BraintreeDataCollector
370388
didEnablePayPalAppSwitch: payPalRequest?.enablePayPalAppSwitch,
371389
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
372390
fundingSource: fundingSource?.rawValue,
373-
isVaultRequest: isVaultRequest
391+
isVaultRequest: isVaultRequest,
392+
recurringBillingPlanType: recurringBillingPlanType,
393+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
374394
)
375395

376396
if isSuccess {
@@ -422,7 +442,9 @@ import BraintreeDataCollector
422442
didEnablePayPalAppSwitch: payPalRequest?.enablePayPalAppSwitch,
423443
fundingSource: fundingSource?.rawValue,
424444
isVaultRequest: isVaultRequest,
425-
shopperSessionID: payPalRequest?.shopperSessionID
445+
recurringBillingPlanType: recurringBillingPlanType,
446+
shopperSessionID: payPalRequest?.shopperSessionID,
447+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
426448
)
427449
apiClient.fetchOrReturnRemoteConfiguration { configuration, error in
428450
if let error {
@@ -474,7 +496,6 @@ import BraintreeDataCollector
474496
}
475497

476498
self.contextID = approvalURL.baToken ?? approvalURL.ecToken
477-
478499
self.experiment = approvalURL.experiment
479500

480501
let dataCollector = BTDataCollector(authorization: self.apiClient.authorization.originalValue)
@@ -530,7 +551,9 @@ import BraintreeDataCollector
530551
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
531552
fundingSource: fundingSource?.rawValue,
532553
isVaultRequest: isVaultRequest,
533-
shopperSessionID: payPalRequest?.shopperSessionID
554+
recurringBillingPlanType: recurringBillingPlanType,
555+
shopperSessionID: payPalRequest?.shopperSessionID,
556+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
534557
)
535558

536559
return
@@ -549,7 +572,9 @@ import BraintreeDataCollector
549572
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
550573
fundingSource: fundingSource?.rawValue,
551574
isVaultRequest: isVaultRequest,
552-
shopperSessionID: payPalRequest?.shopperSessionID
575+
recurringBillingPlanType: recurringBillingPlanType,
576+
shopperSessionID: payPalRequest?.shopperSessionID,
577+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
553578
)
554579

555580
var urlComponents = URLComponents(url: payPalAppRedirectURL, resolvingAgainstBaseURL: true)
@@ -589,7 +614,9 @@ import BraintreeDataCollector
589614
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
590615
fundingSource: fundingSource?.rawValue,
591616
isVaultRequest: isVaultRequest,
592-
shopperSessionID: payPalRequest?.shopperSessionID
617+
recurringBillingPlanType: recurringBillingPlanType,
618+
shopperSessionID: payPalRequest?.shopperSessionID,
619+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
593620
)
594621

595622
approvalURL = appSwitchURL
@@ -641,7 +668,9 @@ import BraintreeDataCollector
641668
fundingSource: fundingSource?.rawValue,
642669
isConfigFromCache: isConfigFromCache,
643670
isVaultRequest: isVaultRequest,
644-
shopperSessionID: payPalRequest?.shopperSessionID
671+
recurringBillingPlanType: recurringBillingPlanType,
672+
shopperSessionID: payPalRequest?.shopperSessionID,
673+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
645674
)
646675
} else {
647676
apiClient.sendAnalyticsEvent(
@@ -654,7 +683,9 @@ import BraintreeDataCollector
654683
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
655684
fundingSource: fundingSource?.rawValue,
656685
isVaultRequest: isVaultRequest,
657-
shopperSessionID: payPalRequest?.shopperSessionID
686+
recurringBillingPlanType: recurringBillingPlanType,
687+
shopperSessionID: payPalRequest?.shopperSessionID,
688+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
658689
)
659690
}
660691
} sessionDidCancel: { [self] in
@@ -671,7 +702,9 @@ import BraintreeDataCollector
671702
didEnablePayPalAppSwitch: payPalRequest?.enablePayPalAppSwitch,
672703
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
673704
fundingSource: fundingSource?.rawValue,
674-
isVaultRequest: isVaultRequest
705+
isVaultRequest: isVaultRequest,
706+
recurringBillingPlanType: recurringBillingPlanType,
707+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
675708
)
676709
}
677710

@@ -692,7 +725,9 @@ import BraintreeDataCollector
692725
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
693726
fundingSource: fundingSource?.rawValue,
694727
isVaultRequest: isVaultRequest,
695-
shopperSessionID: payPalRequest?.shopperSessionID
728+
recurringBillingPlanType: recurringBillingPlanType,
729+
shopperSessionID: payPalRequest?.shopperSessionID,
730+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
696731
)
697732
}
698733
}
@@ -729,7 +764,9 @@ import BraintreeDataCollector
729764
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
730765
fundingSource: fundingSource?.rawValue,
731766
isVaultRequest: isVaultRequest,
732-
shopperSessionID: payPalRequest?.shopperSessionID
767+
recurringBillingPlanType: recurringBillingPlanType,
768+
shopperSessionID: payPalRequest?.shopperSessionID,
769+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
733770
)
734771
completion(result, nil)
735772
}
@@ -746,7 +783,9 @@ import BraintreeDataCollector
746783
errorDescription: error.localizedDescription,
747784
fundingSource: fundingSource?.rawValue,
748785
isVaultRequest: isVaultRequest,
749-
shopperSessionID: payPalRequest?.shopperSessionID
786+
recurringBillingPlanType: recurringBillingPlanType,
787+
shopperSessionID: payPalRequest?.shopperSessionID,
788+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
750789
)
751790
completion(nil, error)
752791
}
@@ -761,7 +800,9 @@ import BraintreeDataCollector
761800
didPayPalServerAttemptAppSwitch: didPayPalServerAttemptAppSwitch,
762801
fundingSource: fundingSource?.rawValue,
763802
isVaultRequest: isVaultRequest,
764-
shopperSessionID: payPalRequest?.shopperSessionID
803+
recurringBillingPlanType: recurringBillingPlanType,
804+
shopperSessionID: payPalRequest?.shopperSessionID,
805+
shouldRequestBillingAgreement: shouldRequestBillingAgreement
765806
)
766807
completion(nil, BTPayPalError.canceled)
767808
}

UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,8 @@ class BTPayPalClient_Tests: XCTestCase {
14751475
XCTAssertTrue(mockAPIClient.postedIsVaultRequest)
14761476
XCTAssertEqual(mockAPIClient.postedContextType, "BA-TOKEN")
14771477
XCTAssertEqual(mockAPIClient.postedFundingSource, "paypal")
1478+
XCTAssertFalse(mockAPIClient.postedShouldRequestBillingAgreement)
1479+
XCTAssertNil(mockAPIClient.postedRecurringBillingPlanType)
14781480
}
14791481

14801482
func testTokenize_whenVaultCreditRequest_setsVaultAnalyticsTags() async {
@@ -1485,6 +1487,8 @@ class BTPayPalClient_Tests: XCTestCase {
14851487
XCTAssertTrue(mockAPIClient.postedIsVaultRequest)
14861488
XCTAssertEqual(mockAPIClient.postedContextType, "BA-TOKEN")
14871489
XCTAssertEqual(mockAPIClient.postedFundingSource, "credit")
1490+
XCTAssertFalse(mockAPIClient.postedShouldRequestBillingAgreement)
1491+
XCTAssertNil(mockAPIClient.postedRecurringBillingPlanType)
14881492
}
14891493

14901494
func testTokenize_whenCheckoutRequest_setsVaultAnalyticsTags() async {
@@ -1495,6 +1499,8 @@ class BTPayPalClient_Tests: XCTestCase {
14951499
XCTAssertFalse(mockAPIClient.postedIsVaultRequest)
14961500
XCTAssertEqual(mockAPIClient.postedContextType, "EC-TOKEN")
14971501
XCTAssertEqual(mockAPIClient.postedFundingSource, "paypal")
1502+
XCTAssertFalse(mockAPIClient.postedShouldRequestBillingAgreement)
1503+
XCTAssertNil(mockAPIClient.postedRecurringBillingPlanType)
14981504
}
14991505

15001506
func testTokenize_whenCheckoutCreditRequest_setsCorrectAnalyticsTags() async {
@@ -1505,16 +1511,32 @@ class BTPayPalClient_Tests: XCTestCase {
15051511
XCTAssertFalse(mockAPIClient.postedIsVaultRequest)
15061512
XCTAssertEqual(mockAPIClient.postedContextType, "EC-TOKEN")
15071513
XCTAssertEqual(mockAPIClient.postedFundingSource, "credit")
1514+
XCTAssertFalse(mockAPIClient.postedShouldRequestBillingAgreement)
1515+
XCTAssertNil(mockAPIClient.postedRecurringBillingPlanType)
15081516
}
15091517

15101518
func testTokenize_whenCheckoutPayLaterRequest_setsCorrectAnalyticsTags() async {
1511-
let checkoutRequest = BTPayPalCheckoutRequest(amount: "2.00", offerPayLater: true)
1519+
let checkoutRequest = BTPayPalCheckoutRequest(amount: "100.00", offerPayLater: true)
15121520

15131521
let _ = try? await payPalClient.tokenize(checkoutRequest)
15141522

15151523
XCTAssertFalse(mockAPIClient.postedIsVaultRequest)
15161524
XCTAssertEqual(mockAPIClient.postedContextType, "EC-TOKEN")
15171525
XCTAssertEqual(mockAPIClient.postedFundingSource, "paylater")
1526+
XCTAssertFalse(mockAPIClient.postedShouldRequestBillingAgreement)
1527+
XCTAssertNil(mockAPIClient.postedRecurringBillingPlanType)
1528+
}
1529+
1530+
func testTokenize_whenCheckoutRequestBillingAgreementTrue_setsBillingWithPurchaseAnalytics() async {
1531+
let checkoutRequest = BTPayPalCheckoutRequest(amount: "2.00", recurringBillingPlanType: .subscription, requestBillingAgreement: true)
1532+
1533+
let _ = try? await payPalClient.tokenize(checkoutRequest)
1534+
1535+
XCTAssertFalse(mockAPIClient.postedIsVaultRequest)
1536+
XCTAssertEqual(mockAPIClient.postedContextType, "EC-TOKEN")
1537+
XCTAssertEqual(mockAPIClient.postedFundingSource, "paypal")
1538+
XCTAssertTrue(mockAPIClient.postedShouldRequestBillingAgreement)
1539+
XCTAssertEqual(mockAPIClient.postedRecurringBillingPlanType, "SUBSCRIPTION")
15181540
}
15191541

15201542
func testTokenize_whenShopperSessionIDSetOnRequest_includesInAnalytics() async {

0 commit comments

Comments
 (0)