Skip to content

Commit

Permalink
Merge branch 'main' into contact-information-feature
Browse files Browse the repository at this point in the history
# Conflicts:
#	CHANGELOG.md
  • Loading branch information
jaxdesmarais committed Dec 16, 2024
2 parents 06969f1 + 652e696 commit a650958
Show file tree
Hide file tree
Showing 16 changed files with 141 additions and 55 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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
46 changes: 24 additions & 22 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
}

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
Loading

0 comments on commit a650958

Please sign in to comment.