diff --git a/Braintree.podspec b/Braintree.podspec
index f5d273c832..01086b8d05 100644
--- a/Braintree.podspec
+++ b/Braintree.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "Braintree"
- s.version = "6.24.0"
+ s.version = "6.25.0"
s.summary = "Braintree iOS SDK: Helps you accept card and alternative payments in your iOS app."
s.description = <<-DESC
Braintree is a full-stack payments platform for developers
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ba1482596..95779eadbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,20 @@
## unreleased
* BraintreePayPal
- * Add `BTPayPalRequest.userPhoneNumber` optional property
* Add `BTContactInformation` request object
* Add `BTPayPalCheckoutRequest.contactInformation` optional property
+
+## 6.25.0 (2024-12-11)
+* BraintreePayPal
+ * Add `BTPayPalRequest.userPhoneNumber` optional property
+ * Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI)
* BraintreeVenmo
* Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI)
+ * Add `BTVenmoClient(apiClient:universalLink:)` to use Universal Links when redirecting back from the Venmo flow
+* BraintreeCore
+ * Deprecate `BTAppContextSwitcher.sharedInstance.returnURLScheme`
+* BraintreeThreeDSecure
+ * Add `BTThreeDSecureRequest.requestorAppURL`
## 6.24.0 (2024-10-15)
* BraintreePayPal
diff --git a/Demo/Application/Features/VenmoViewController.swift b/Demo/Application/Features/VenmoViewController.swift
index 0b6537f168..1668872139 100644
--- a/Demo/Application/Features/VenmoViewController.swift
+++ b/Demo/Application/Features/VenmoViewController.swift
@@ -2,14 +2,16 @@ import UIKit
import BraintreeVenmo
class VenmoViewController: PaymentButtonBaseViewController {
-
+
// swiftlint:disable:next implicitly_unwrapped_optional
var venmoClient: BTVenmoClient!
let webFallbackToggle = Toggle(title: "Enable Web Fallback")
let vaultToggle = Toggle(title: "Vault")
-
+ let universalLinkReturnToggle = Toggle(title: "Use Universal Link Return")
+
override func viewDidLoad() {
+ super.heightConstraint = 150
super.viewDidLoad()
venmoClient = BTVenmoClient(apiClient: apiClient)
title = "Custom Venmo Button"
@@ -18,7 +20,7 @@ class VenmoViewController: PaymentButtonBaseViewController {
override func createPaymentButton() -> UIView {
let venmoButton = createButton(title: "Venmo", action: #selector(tappedVenmo))
- let stackView = UIStackView(arrangedSubviews: [webFallbackToggle, vaultToggle, venmoButton])
+ let stackView = UIStackView(arrangedSubviews: [webFallbackToggle, vaultToggle, universalLinkReturnToggle, venmoButton])
stackView.axis = .vertical
stackView.spacing = 15
stackView.alignment = .fill
@@ -40,7 +42,15 @@ class VenmoViewController: PaymentButtonBaseViewController {
if vaultToggle.isOn {
venmoRequest.vault = true
}
-
+
+ if universalLinkReturnToggle.isOn {
+ venmoClient = BTVenmoClient(
+ apiClient: apiClient,
+ // swiftlint:disable:next force_unwrapping
+ universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")!
+ )
+ }
+
Task {
do {
let venmoAccount = try await venmoClient.tokenize(venmoRequest)
diff --git a/Demo/Application/Supporting Files/Braintree-Demo-Info.plist b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist
index 03c10f885f..0fff3896e4 100644
--- a/Demo/Application/Supporting Files/Braintree-Demo-Info.plist
+++ b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist
@@ -41,7 +41,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 6.24.0
+ 6.25.0
CFBundleURLTypes
@@ -56,7 +56,7 @@
CFBundleVersion
- 6.24.0
+ 6.25.0
LSApplicationQueriesSchemes
com.braintreepayments.Demo.payments
diff --git a/Sources/BraintreeCore/BTAppContextSwitcher.swift b/Sources/BraintreeCore/BTAppContextSwitcher.swift
index db875e268d..1ea5738eff 100644
--- a/Sources/BraintreeCore/BTAppContextSwitcher.swift
+++ b/Sources/BraintreeCore/BTAppContextSwitcher.swift
@@ -14,7 +14,22 @@ import UIKit
/// The URL scheme to return to this app after switching to another app or opening a SFSafariViewController.
/// This URL scheme must be registered as a URL Type in the app's info.plist, and it must start with the app's bundle ID.
/// - Note: This property should only be used for the Venmo flow.
- public var returnURLScheme: String = ""
+ @available(
+ *,
+ deprecated,
+ message: "returnURLScheme is deprecated and will be removed in a future version. Use BTVenmoClient(apiClient:universalLink:)."
+ )
+ public var returnURLScheme: String {
+ get { _returnURLScheme }
+ set { _returnURLScheme = newValue }
+ }
+
+ // swiftlint:disable identifier_name
+ /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
+ /// Property for `returnURLScheme`. Created to avoid deprecation warnings upon accessing
+ /// `returnURLScheme` directly within our SDK. Use this value instead.
+ public var _returnURLScheme: String = ""
+ // swiftlint:enable identifier_name
// MARK: - Private Properties
diff --git a/Sources/BraintreeCore/BTCoreConstants.swift b/Sources/BraintreeCore/BTCoreConstants.swift
index aaa8eb3a20..a058997d4d 100644
--- a/Sources/BraintreeCore/BTCoreConstants.swift
+++ b/Sources/BraintreeCore/BTCoreConstants.swift
@@ -5,7 +5,7 @@ import Foundation
@objcMembers public class BTCoreConstants: NSObject {
/// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
- public static var braintreeSDKVersion: String = "6.24.0"
+ public static var braintreeSDKVersion: String = "6.25.0"
/// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
public static let callbackURLScheme: String = "sdk.ios.braintree"
diff --git a/Sources/BraintreeCore/Info.plist b/Sources/BraintreeCore/Info.plist
index 0a03531a0b..8724499eed 100644
--- a/Sources/BraintreeCore/Info.plist
+++ b/Sources/BraintreeCore/Info.plist
@@ -15,11 +15,11 @@
CFBundlePackageType
FMWK
CFBundleShortVersionString
- 6.24.0
+ 6.25.0
CFBundleSignature
????
CFBundleVersion
- 6.24.0
+ 6.25.0
NSPrincipalClass
diff --git a/Sources/BraintreeDataCollector/BTDataCollector.swift b/Sources/BraintreeDataCollector/BTDataCollector.swift
index 097588f2fa..02d6f18cdb 100644
--- a/Sources/BraintreeDataCollector/BTDataCollector.swift
+++ b/Sources/BraintreeDataCollector/BTDataCollector.swift
@@ -147,15 +147,14 @@ import BraintreeCore
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
if status == errSecSuccess,
- let existingItem = item as? [String: Any],
- let data = existingItem[kSecValueData as String] as? Data,
- let identifier = String(data: data, encoding: String.Encoding.utf8) {
+ let data = item as? Data,
+ let identifier = String(data: data, encoding: .utf8) {
return identifier
}
// If not, generate a new one and save it
let newIdentifier = UUID().uuidString
- query[kSecValueData as String] = newIdentifier
+ query[kSecValueData as String] = newIdentifier.data(using: .utf8)
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
SecItemAdd(query as CFDictionary, nil)
return newIdentifier
diff --git a/Sources/BraintreePayPal/BTPayPalClient.swift b/Sources/BraintreePayPal/BTPayPalClient.swift
index 0362ed1597..9d5ee0e8e5 100644
--- a/Sources/BraintreePayPal/BTPayPalClient.swift
+++ b/Sources/BraintreePayPal/BTPayPalClient.swift
@@ -284,6 +284,29 @@ import BraintreeDataCollector
performSwitchRequest(appSwitchURL: url, paymentType: paymentType, completion: completion)
}
+ func invokedOpenURLSuccessfully(_ success: Bool, url: URL, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void) {
+ if success {
+ apiClient.sendAnalyticsEvent(
+ BTPayPalAnalytics.appSwitchSucceeded,
+ isVaultRequest: isVaultRequest,
+ linkType: linkType,
+ payPalContextID: payPalContextID,
+ appSwitchURL: url
+ )
+ BTPayPalClient.payPalClient = self
+ appSwitchCompletion = completion
+ } else {
+ apiClient.sendAnalyticsEvent(
+ BTPayPalAnalytics.appSwitchFailed,
+ isVaultRequest: isVaultRequest,
+ linkType: linkType,
+ payPalContextID: payPalContextID,
+ appSwitchURL: url
+ )
+ notifyFailure(with: BTPayPalError.appSwitchFailed, completion: completion)
+ }
+ }
+
// MARK: - App Switch Methods
func handleReturnURL(_ url: URL) {
@@ -404,28 +427,7 @@ import BraintreeDataCollector
}
application.open(redirectURL) { success in
- self.invokedOpenURLSuccessfully(success, completion: completion)
- }
- }
-
- private func invokedOpenURLSuccessfully(_ success: Bool, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void) {
- if success {
- apiClient.sendAnalyticsEvent(
- BTPayPalAnalytics.appSwitchSucceeded,
- isVaultRequest: isVaultRequest,
- linkType: linkType,
- payPalContextID: payPalContextID
- )
- BTPayPalClient.payPalClient = self
- appSwitchCompletion = completion
- } else {
- apiClient.sendAnalyticsEvent(
- BTPayPalAnalytics.appSwitchFailed,
- isVaultRequest: isVaultRequest,
- linkType: linkType,
- payPalContextID: payPalContextID
- )
- notifyFailure(with: BTPayPalError.appSwitchFailed, completion: completion)
+ self.invokedOpenURLSuccessfully(success, url: redirectURL, completion: completion)
}
}
diff --git a/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift b/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift
index 43b3274951..d9ff3cc446 100644
--- a/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift
+++ b/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift
@@ -93,6 +93,10 @@ import BraintreeCore
/// When using `BTThreeDSecureUIType.native`, all `BTThreeDSecureRenderType` options except `.html` must be set.
public var renderTypes: [BTThreeDSecureRenderType]?
+ /// Optional. Three DS Requester APP URL Merchant app declaring their URL within the CReq message
+ /// so that the Authentication app can call the Merchant app after out of band authentication has occurred.
+ public var requestorAppURL: String?
+
/// A delegate for receiving information about the ThreeDSecure payment flow.
public weak var threeDSecureRequestDelegate: BTThreeDSecureRequestDelegate?
diff --git a/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift b/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift
index 89b21f4113..99cd10f9c0 100644
--- a/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift
+++ b/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift
@@ -45,6 +45,10 @@ class BTThreeDSecureV2Provider {
cardinalConfiguration.renderType = renderTypes.compactMap { $0.cardinalValue }
}
+ if let requestorAppURL = request.requestorAppURL {
+ cardinalConfiguration.threeDSRequestorAppURL = requestorAppURL
+ }
+
guard let cardinalAuthenticationJWT = configuration.cardinalAuthenticationJWT else {
completion(nil)
return
diff --git a/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift b/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift
index b9d561e064..862a1c9ea9 100644
--- a/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift
+++ b/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift
@@ -22,9 +22,10 @@ struct BTVenmoAppSwitchRedirectURL {
// MARK: - Initializer
init(
- returnURLScheme: String,
paymentContextID: String,
metadata: BTClientMetadata,
+ returnURLScheme: String?,
+ universalLink: URL?,
forMerchantID merchantID: String?,
accessToken: String?,
bundleDisplayName: String?,
@@ -46,9 +47,6 @@ struct BTVenmoAppSwitchRedirectURL {
let base64EncodedBraintreeData = serializedBraintreeData?.base64EncodedString()
queryParameters = [
- "x-success": constructRedirectURL(with: returnURLScheme, result: "success"),
- "x-error": constructRedirectURL(with: returnURLScheme, result: "error"),
- "x-cancel": constructRedirectURL(with: returnURLScheme, result: "cancel"),
"x-source": bundleDisplayName,
"braintree_merchant_id": merchantID,
"braintree_access_token": accessToken,
@@ -57,6 +55,16 @@ struct BTVenmoAppSwitchRedirectURL {
"braintree_sdk_data": base64EncodedBraintreeData ?? "",
"customerClient": "MOBILE_APP"
]
+
+ if let universalLink {
+ queryParameters["x-success"] = universalLink.appendingPathComponent("success").absoluteString
+ queryParameters["x-error"] = universalLink.appendingPathComponent("error").absoluteString
+ queryParameters["x-cancel"] = universalLink.appendingPathComponent("cancel").absoluteString
+ } else if let returnURLScheme {
+ queryParameters["x-success"] = constructRedirectURL(with: returnURLScheme, result: "success")
+ queryParameters["x-error"] = constructRedirectURL(with: returnURLScheme, result: "error")
+ queryParameters["x-cancel"] = constructRedirectURL(with: returnURLScheme, result: "cancel")
+ }
}
// MARK: - Internal Methods
diff --git a/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift b/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
index b1fcd30be4..569054b539 100644
--- a/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
+++ b/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
@@ -41,7 +41,7 @@ struct BTVenmoAppSwitchReturnURL {
init?(url: URL) {
let parameters = BTURLUtils.queryParameters(for: url)
- if url.path == "/vzero/auth/venmo/success" {
+ if url.path.contains("success") {
if let resourceID = parameters["resource_id"] {
state = .succeededWithPaymentContext
paymentContextID = resourceID
@@ -50,12 +50,12 @@ struct BTVenmoAppSwitchReturnURL {
nonce = parameters["paymentMethodNonce"] ?? parameters["payment_method_nonce"]
username = parameters["username"]
}
- } else if url.path == "/vzero/auth/venmo/error" {
+ } else if url.path.contains("error") {
state = .failed
let errorMessage: String? = parameters["errorMessage"] ?? parameters["error_message"]
let errorCode = Int(parameters["errorCode"] ?? parameters["error_code"] ?? "0")
error = BTVenmoAppSwitchError.returnURLError(errorCode ?? 0, errorMessage)
- } else if url.path == "/vzero/auth/venmo/cancel" {
+ } else if url.path.contains("cancel") {
state = .canceled
} else {
state = .unknown
@@ -68,6 +68,7 @@ struct BTVenmoAppSwitchReturnURL {
/// - Parameter url: an app switch return URL
/// - Returns: `true` if the url represents a Venmo Touch app switch return
static func isValid(url: URL) -> Bool {
- url.host == "x-callback-url" && url.path.hasPrefix("/vzero/auth/venmo/")
+ (url.scheme == "https" && (url.path.contains("cancel") || url.path.contains("success") || url.path.contains("error")))
+ || (url.host == "x-callback-url" && url.path.hasPrefix("/vzero/auth/venmo/"))
}
}
diff --git a/Sources/BraintreeVenmo/BTVenmoClient.swift b/Sources/BraintreeVenmo/BTVenmoClient.swift
index f5fa291508..7193bb8758 100644
--- a/Sources/BraintreeVenmo/BTVenmoClient.swift
+++ b/Sources/BraintreeVenmo/BTVenmoClient.swift
@@ -46,9 +46,11 @@ import BraintreeCore
/// Used for sending the type of flow, universal vs deeplink to FPTI
private var linkType: LinkType?
+ private var universalLink: URL?
+
// MARK: - Initializer
- /// Creates an Apple Pay client
+ /// Creates a Venmo client
/// - Parameter apiClient: An API client
@objc(initWithAPIClient:)
public init(apiClient: BTAPIClient) {
@@ -56,6 +58,16 @@ import BraintreeCore
self.apiClient = apiClient
}
+ /// Initialize a new Venmo client instance.
+ /// - Parameters:
+ /// - apiClient: The API Client
+ /// - universalLink: The URL for the Venmo app to redirect to after user authentication completes. Must be a valid HTTPS URL dedicated to Braintree app switch returns.
+ @objc(initWithAPIClient:universalLink:)
+ public convenience init(apiClient: BTAPIClient, universalLink: URL) {
+ self.init(apiClient: apiClient)
+ self.universalLink = universalLink
+ }
+
// MARK: - Public Methods
/// Initiates Venmo login via app switch, which returns a BTVenmoAccountNonce when successful.
@@ -69,7 +81,7 @@ import BraintreeCore
public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) {
linkType = request.fallbackToWeb ? .universal : .deeplink
apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted, isVaultRequest: shouldVault, linkType: linkType)
- let returnURLScheme = BTAppContextSwitcher.sharedInstance.returnURLScheme
+ let returnURLScheme = BTAppContextSwitcher.sharedInstance._returnURLScheme
if returnURLScheme.isEmpty {
NSLog(
@@ -151,9 +163,10 @@ import BraintreeCore
do {
let appSwitchURL = try BTVenmoAppSwitchRedirectURL(
- returnURLScheme: returnURLScheme,
paymentContextID: paymentContextID,
metadata: metadata,
+ returnURLScheme: returnURLScheme,
+ universalLink: self.universalLink,
forMerchantID: merchantProfileID,
accessToken: configuration.venmoAccessToken,
bundleDisplayName: bundleDisplayName,
diff --git a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift
index 7fff4ad489..787e9ef356 100644
--- a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift
+++ b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift
@@ -982,6 +982,24 @@ class BTPayPalClient_Tests: XCTestCase {
XCTAssertNil(lastPostParameters["merchant_app_return_url"] as? String)
}
+ func testInvokedOpenURLSuccessfully_whenSuccess_sendsAppSwitchSucceededWithAppSwitchURL() {
+ let eventName = BTPayPalAnalytics.appSwitchSucceeded
+ let fakeURL = URL(string: "some-url")!
+ payPalClient.invokedOpenURLSuccessfully(true, url: fakeURL) { _, _ in }
+
+ XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, eventName)
+ XCTAssertEqual(mockAPIClient.postedAppSwitchURL[eventName], fakeURL.absoluteString)
+ }
+
+ func testInvokedOpenURLSuccessfully_whenFailure_sendsAppSwitchFailedWithAppSwitchURL() {
+ let eventName = BTPayPalAnalytics.appSwitchFailed
+ let fakeURL = URL(string: "some-url")!
+ payPalClient.invokedOpenURLSuccessfully(false, url: fakeURL) { _, _ in }
+
+ XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first!, eventName)
+ XCTAssertEqual(mockAPIClient.postedAppSwitchURL[eventName], fakeURL.absoluteString)
+ }
+
// MARK: - Analytics
func testAPIClientMetadata_hasIntegrationSetToCustom() {
diff --git a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift
index da92e9bc00..fef916e212 100644
--- a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift
+++ b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift
@@ -7,9 +7,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
func testUrlSchemeURL_whenAllValuesAreInitialized_returnsURLWithPaymentContextID() {
do {
let requestURL = try BTVenmoAppSwitchRedirectURL(
- returnURLScheme: "url-scheme",
paymentContextID: "12345",
metadata: BTClientMetadata(),
+ returnURLScheme: "url-scheme",
+ universalLink: nil,
forMerchantID: "merchant-id",
accessToken: "access-token",
bundleDisplayName: "display-name",
@@ -29,9 +30,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
func testAppSwitchURL_whenMerchantIDNil_throwsError() {
do {
_ = try BTVenmoAppSwitchRedirectURL(
- returnURLScheme: "url-scheme",
paymentContextID: "12345",
metadata: BTClientMetadata(),
+ returnURLScheme: "url-scheme",
+ universalLink: nil,
forMerchantID: nil,
accessToken: "access-token",
bundleDisplayName: "display-name",
@@ -47,9 +49,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
func testUniversalLinkURL_whenAllValuesInitialized_returnsURLWithAllValues() {
do {
let requestURL = try BTVenmoAppSwitchRedirectURL(
- returnURLScheme: "url-scheme",
paymentContextID: "12345",
metadata: BTClientMetadata(),
+ returnURLScheme: nil,
+ universalLink: URL(string: "https://mywebsite.com/braintree-payments"),
forMerchantID: "merchant-id",
accessToken: "access-token",
bundleDisplayName: "display-name",
@@ -60,9 +63,9 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
let components = URLComponents(string: requestURL.universalLinksURL()!.absoluteString)
guard let queryItems = components?.queryItems else { XCTFail(); return }
- XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "url-scheme://x-callback-url/vzero/auth/venmo/success")))
- XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "url-scheme://x-callback-url/vzero/auth/venmo/error")))
- XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "url-scheme://x-callback-url/vzero/auth/venmo/cancel")))
+ XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "https://mywebsite.com/braintree-payments/success")))
+ XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "https://mywebsite.com/braintree-payments/error")))
+ XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "https://mywebsite.com/braintree-payments/cancel")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-source", value: "display-name")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_merchant_id", value: "merchant-id")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_access_token", value: "access-token")))