Skip to content

Commit ee5394e

Browse files
authored
Fix ClientMetadataID Being Nil For PayPal Checkout With BillingAgreement Flow (#1807)
* fix(paypal): Set CMID for PP checkout with BA flow
1 parent 59f445c commit ee5394e

3 files changed

Lines changed: 32 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
* Fix intermittent crash in `collectDeviceData` caused by an internal SDK component accessing `UIPasteboard` off the main thread
1010
* BraintreePayPal
1111
* Fix `BTPayPalAccountNonce.clientMetadataID` returning `nil` caused by `clientMetadataID` being generated off the main thread
12+
* Fix `BTPayPalAccountNonce.clientMetadataID` returning `nil` for checkout flows with `requestBillingAgreement` enabled
1213

1314
## 7.6.0 (2026-05-18)
1415
* Fix inconsistency in minimum deployment target, which is now consistently iOS 16 (fixes #1757)

Demo/Application/Features/PayPalWebCheckoutViewController.swift

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import UIKit
33
import BraintreePayPal
44
import BraintreeCore
55

6-
// swiftlint:disable type_body_length
6+
// swiftlint:disable type_body_length file_length
77
class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
88

99
lazy var payPalClient = BTPayPalClient(
@@ -78,6 +78,7 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
7878
let rbaDataToggle = Toggle(title: "Recurring Billing (RBA) Data")
7979
let contactInformationToggle = Toggle(title: "Add Contact Information")
8080
let amountBreakdownToggle = Toggle(title: "Amount Breakdown")
81+
let requestBillingAgreementToggle = Toggle(title: "Request Billing Agreement")
8182

8283
override func viewDidLoad() {
8384
super.heightConstraint = 500
@@ -112,6 +113,7 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
112113
let oneTimeCheckoutStackView = buttonsStackView(label: "1-Time Checkout", views: [
113114
contactInformationToggle,
114115
amountBreakdownToggle,
116+
requestBillingAgreementToggle,
115117
payPalCheckoutButton,
116118
payPalAppSwitchForCheckoutButton,
117119
payPalAppSwitchForCheckoutPayLaterButton,
@@ -180,6 +182,7 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
180182
contactInformation: contactInformationToggle.isOn ? contactInformation : nil,
181183
contactPreference: .updateContactInformation,
182184
lineItems: [lineItem],
185+
requestBillingAgreement: requestBillingAgreementToggle.isOn,
183186
userAuthenticationEmail: emailTextField.text,
184187
userPhoneNumber: BTPayPalPhoneNumber(
185188
countryCode: countryCodeTextField.text ?? "",
@@ -243,9 +246,9 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
243246
self.completionBlock(nonce)
244247
}
245248
}
246-
249+
247250
// MARK: - Vault Flows
248-
251+
249252
@objc func tappedPayPalVault(_ sender: UIButton) {
250253
progressBlock("Tapped PayPal - Vault using BTPayPalClient")
251254
sender.setTitle("Processing...", for: .disabled)
@@ -302,7 +305,7 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
302305
self.completionBlock(nonce)
303306
}
304307
}
305-
308+
306309
@objc func tappedPayPalAppSwitchForCheckout(_ sender: UIButton) {
307310
sender.setTitle("Processing...", for: .disabled)
308311
sender.isEnabled = false
@@ -317,12 +320,12 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
317320

318321
payPalClient.tokenize(request) { nonce, error in
319322
sender.isEnabled = true
320-
323+
321324
guard let nonce else {
322325
self.progressBlock(error?.localizedDescription)
323326
return
324327
}
325-
328+
326329
self.completionBlock(nonce)
327330
}
328331
}
@@ -341,12 +344,12 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
341344

342345
payPalClient.tokenize(request) { nonce, error in
343346
sender.isEnabled = true
344-
347+
345348
guard let nonce else {
346349
self.progressBlock(error?.localizedDescription)
347350
return
348351
}
349-
352+
350353
self.completionBlock(nonce)
351354
}
352355
}
@@ -362,16 +365,16 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
362365

363366
payPalClient.tokenize(request) { nonce, error in
364367
sender.isEnabled = true
365-
368+
366369
guard let nonce else {
367370
self.progressBlock(error?.localizedDescription)
368371
return
369372
}
370-
373+
371374
self.completionBlock(nonce)
372375
}
373376
}
374-
377+
375378
@objc func tappedPayLaterForCheckout(_ sender: UIButton) {
376379
sender.setTitle("Processing...", for: .disabled)
377380
sender.isEnabled = false
@@ -387,12 +390,12 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
387390

388391
payPalClient.tokenize(request) { nonce, error in
389392
sender.isEnabled = true
390-
393+
391394
guard let nonce else {
392395
self.progressBlock(error?.localizedDescription)
393396
return
394397
}
395-
398+
396399
self.completionBlock(nonce)
397400
}
398401
}

Sources/BraintreePayPal/BTPayPalClient.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import BraintreeDataCollector
2828
/// Used in POST body for FPTI analytics & `/paypal_account` fetch.
2929
/// The key is the PayPal context ID (e.g., BA or EC token), and the value is the corresponding client metadata ID.
3030
var clientMetadataIDs: [String: String] = [:]
31-
31+
3232
/// Exposed for testing the intent associated with this request
3333
var payPalRequest: BTPayPalRequest?
3434

@@ -748,9 +748,20 @@ import BraintreeDataCollector
748748
private func extractToken(from url: URL?) -> String? {
749749
guard let url else { return nil }
750750

751-
let baToken = BTURLUtils.queryParameters(for: url)["ba_token"]
752-
let ecToken = BTURLUtils.queryParameters(for: url)["token"]
753-
let token = baToken ?? ecToken
751+
let params = BTURLUtils.queryParameters(for: url)
752+
let baToken = params["ba_token"]
753+
let ecToken = params["token"]
754+
755+
// For checkout+requestBillingAgreement, the return URL contains both an EC token and a newly-minted
756+
// BA token. The CMID was stored under the EC token (from the approval URL), so we must look up by
757+
// ecToken here rather than defaulting to baToken.
758+
let token: String?
759+
if payPalRequest?.paymentType == .checkout && shouldRequestBillingAgreement == true {
760+
token = ecToken
761+
} else {
762+
token = baToken ?? ecToken
763+
}
764+
754765
return token?.isEmpty == true ? nil : token
755766
}
756767

0 commit comments

Comments
 (0)