Skip to content

Commit

Permalink
Merge branch 'shopper-insights-rp1-feature' of https://github.com/bra…
Browse files Browse the repository at this point in the history
…intree/braintree_ios into shopper-insights-rp2-sendSelected

# Conflicts:
#	CHANGELOG.md
  • Loading branch information
stechiu committed Dec 17, 2024
2 parents 9e1a125 + 7b5d6b6 commit 2e56774
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Braintree.podspec
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Braintree iOS SDK Release Notes

## unreleased
## 6.25.0 (2024-12-11)
* BraintreePayPal
* Add `BTPayPalRequest.userPhoneNumber` optional property
* Add `shopperSessionID` to `BTPayPalCheckoutRequest` and `BTPayPalVaultRequest`
Expand All @@ -10,6 +10,14 @@
* Add `shopperSessionID` to `BTShopperInsightsClient` initializer
* Add `isPayPalAppInstalled()` and/or `isVenmoAppInstalled()`
* Replace `sendPayPalSelectedEvent()` and `sendPayPalSelectedEvent()` with `sendSelectedEvent(for:)`
* 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
Expand Down
18 changes: 14 additions & 4 deletions Demo/Application/Features/VenmoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions Demo/Application/Supporting Files/Braintree-Demo-Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>6.24.0</string>
<string>6.25.0</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
Expand All @@ -56,7 +56,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>6.24.0</string>
<string>6.25.0</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>com.braintreepayments.Demo.payments</string>
Expand Down
17 changes: 16 additions & 1 deletion Sources/BraintreeCore/BTAppContextSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Sources/BraintreeCore/BTCoreConstants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions Sources/BraintreeCore/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>6.24.0</string>
<string>6.25.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>6.24.0</string>
<string>6.25.0</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
Expand Down
7 changes: 3 additions & 4 deletions Sources/BraintreeDataCollector/BTDataCollector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down
4 changes: 4 additions & 0 deletions Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?,
Expand All @@ -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,
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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/"))
}
}
19 changes: 16 additions & 3 deletions Sources/BraintreeVenmo/BTVenmoClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,28 @@ 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) {
BTAppContextSwitcher.sharedInstance.register(BTVenmoClient.self)
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.
Expand All @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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")))
Expand Down

0 comments on commit 2e56774

Please sign in to comment.