From 830a48fc79db979448b2ba84d7654cf2a82f183b Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Fri, 20 Dec 2024 09:16:41 -0600 Subject: [PATCH] [v7] Update Venmo to Support Only Universal Links (#1489) * Remove BTAppContextSwitcher.sharedInstance.returnURLScheme * Remove BTVenmoClient(apiClient:) init in favor of universalLink init * Cleanup demo app * Remove logic around returnURLScheme in the Venmo flow since this is no longer needed * Remove no longer needed error case * Cleanup unit tests based on updated logic --- CHANGELOG.md | 2 + Demo/Application/Base/AppDelegate.swift | 2 - .../PaymentButtonBaseViewController.swift | 16 +++ .../PayPalWebCheckoutViewController.swift | 16 --- .../ShopperInsightsViewController.swift | 8 +- .../Features/VenmoViewController.swift | 31 +++-- .../CarthageTest/ViewController.swift | 5 +- .../SPMTest/SPMTest/ViewController.swift | 5 +- .../BraintreeCore/BTAppContextSwitcher.swift | 21 ---- .../BTVenmoAppSwitchRedirectURL.swift | 43 +------ .../BTVenmoAppSwitchReturnURL.swift | 1 - Sources/BraintreeVenmo/BTVenmoClient.swift | 53 +++------ Sources/BraintreeVenmo/BTVenmoError.swift | 43 +++---- .../BTAppContextSwitcher_Tests.swift | 5 - .../BTLocalPaymentClient_UnitTests.swift | 1 - .../BTVenmoAppSwitchRedirectURL_Tests.swift | 8 +- .../BTVenmoClient_Tests.swift | 109 +++--------------- V7_MIGRATION.md | 19 ++- 18 files changed, 115 insertions(+), 273 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0c82f103..500281cd43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ * BraintreeVenmo * Update `BTVenmoRequest` to make all properties accessible on the initializer only vs via the dot syntax. * Remove `fallbacktoWeb` property from `BTVenmoRequest`. All Venmo flows will now use universal links to switch to the Venmo app or fallback to the web flow if the Venmo app is not installed + * Remove `BTAppContextSwitcher.sharedInstance.returnURLScheme` + * `BTVenmoClient` initializer now requires a `universalLink` for switching to and from the Venmo app or web fallback flow * BraintreeSEPADirectDebit * Update `BTSEPADirectDebitRequest` to make all properties accessible on the initializer only vs via the dot syntax. * BraintreeLocalPayment diff --git a/Demo/Application/Base/AppDelegate.swift b/Demo/Application/Base/AppDelegate.swift index c07540ce8d..d5c35ef5dc 100644 --- a/Demo/Application/Base/AppDelegate.swift +++ b/Demo/Application/Base/AppDelegate.swift @@ -3,7 +3,6 @@ import BraintreeCore @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - private let returnURLScheme = "com.braintreepayments.Demo.payments" private let processInfoArgs = ProcessInfo.processInfo.arguments private let userDefaults = UserDefaults.standard @@ -13,7 +12,6 @@ import BraintreeCore ) -> Bool { registerDefaultsFromSettings() persistDemoSettings() - BTAppContextSwitcher.sharedInstance.returnURLScheme = returnURLScheme userDefaults.setValue(true, forKey: "magnes.debug.mode") diff --git a/Demo/Application/Base/PaymentButtonBaseViewController.swift b/Demo/Application/Base/PaymentButtonBaseViewController.swift index 32a7f1e36b..3c1456fc2b 100644 --- a/Demo/Application/Base/PaymentButtonBaseViewController.swift +++ b/Demo/Application/Base/PaymentButtonBaseViewController.swift @@ -51,4 +51,20 @@ class PaymentButtonBaseViewController: BaseViewController { button.addTarget(self, action: action, for: .touchUpInside) return button } + + // MARK: - Helpers + + func buttonsStackView(label: String, views: [UIView]) -> UIStackView { + let titleLabel = UILabel() + titleLabel.text = label + + let buttonsStackView = UIStackView(arrangedSubviews: [titleLabel] + views) + buttonsStackView.axis = .vertical + buttonsStackView.distribution = .fillProportionally + buttonsStackView.backgroundColor = .systemGray6 + buttonsStackView.layoutMargins = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) + buttonsStackView.isLayoutMarginsRelativeArrangement = true + + return buttonsStackView + } } diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index 5943bec649..d851698fca 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -250,20 +250,4 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { self.completionBlock(nonce) } } - - // MARK: - Helpers - - private func buttonsStackView(label: String, views: [UIView]) -> UIStackView { - let titleLabel = UILabel() - titleLabel.text = label - - let buttonsStackView = UIStackView(arrangedSubviews: [titleLabel] + views) - buttonsStackView.axis = .vertical - buttonsStackView.distribution = .fillProportionally - buttonsStackView.backgroundColor = .systemGray6 - buttonsStackView.layoutMargins = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5) - buttonsStackView.isLayoutMarginsRelativeArrangement = true - - return buttonsStackView - } } diff --git a/Demo/Application/Features/ShopperInsightsViewController.swift b/Demo/Application/Features/ShopperInsightsViewController.swift index 898d1a21a2..b817ce9f79 100644 --- a/Demo/Application/Features/ShopperInsightsViewController.swift +++ b/Demo/Application/Features/ShopperInsightsViewController.swift @@ -8,8 +8,12 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController { lazy var shopperInsightsClient = BTShopperInsightsClient(apiClient: apiClient) lazy var payPalClient = BTPayPalClient(apiClient: apiClient) - lazy var venmoClient = BTVenmoClient(apiClient: apiClient) - + lazy var venmoClient = BTVenmoClient( + apiClient: apiClient, + // swiftlint:disable:next force_unwrapping + universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")! + ) + lazy var payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(payPalVaultButtonTapped)) lazy var venmoButton = createButton(title: "Venmo", action: #selector(venmoButtonTapped)) diff --git a/Demo/Application/Features/VenmoViewController.swift b/Demo/Application/Features/VenmoViewController.swift index d5ef626ef6..d6a760ad86 100644 --- a/Demo/Application/Features/VenmoViewController.swift +++ b/Demo/Application/Features/VenmoViewController.swift @@ -7,26 +7,31 @@ class VenmoViewController: PaymentButtonBaseViewController { var venmoClient: BTVenmoClient! 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) + venmoClient = BTVenmoClient( + apiClient: apiClient, + // swiftlint:disable:next force_unwrapping + universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")! + ) + title = "Custom Venmo Button" } override func createPaymentButton() -> UIView { let venmoButton = createButton(title: "Venmo", action: #selector(tappedVenmo)) - let stackView = UIStackView(arrangedSubviews: [vaultToggle, universalLinkReturnToggle, venmoButton]) - stackView.axis = .vertical - stackView.spacing = 15 - stackView.alignment = .fill - stackView.distribution = .fillEqually - stackView.translatesAutoresizingMaskIntoConstraints = false + let venmoStackView = buttonsStackView(label: "Venmo Payment Flow", views: [ + vaultToggle, + venmoButton + ]) + + venmoStackView.spacing = 12 + venmoStackView.translatesAutoresizingMaskIntoConstraints = false - return stackView + return venmoStackView } @objc func tappedVenmo() { @@ -38,14 +43,6 @@ class VenmoViewController: PaymentButtonBaseViewController { vault: isVaultingEnabled ) - 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/SampleApps/CarthageTest/CarthageTest/ViewController.swift b/SampleApps/CarthageTest/CarthageTest/ViewController.swift index 748b3f2a46..9e369e5dc4 100644 --- a/SampleApps/CarthageTest/CarthageTest/ViewController.swift +++ b/SampleApps/CarthageTest/CarthageTest/ViewController.swift @@ -24,7 +24,10 @@ class ViewController: UIViewController { let payPalClient = BTPayPalClient(apiClient: apiClient) let payPalMessagingView = BTPayPalMessagingView(apiClient: apiClient) let threeDSecureClient = BTThreeDSecureClient(apiClient: apiClient) - let venmoClient = BTVenmoClient(apiClient: apiClient) + let venmoClient = BTVenmoClient( + apiClient: apiClient, + universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")! + ) let sepaDirectDebitClient = BTSEPADirectDebitClient(apiClient: apiClient) } } diff --git a/SampleApps/SPMTest/SPMTest/ViewController.swift b/SampleApps/SPMTest/SPMTest/ViewController.swift index 3232545c28..b1525c84a1 100644 --- a/SampleApps/SPMTest/SPMTest/ViewController.swift +++ b/SampleApps/SPMTest/SPMTest/ViewController.swift @@ -24,7 +24,10 @@ class ViewController: UIViewController { let payPalClient = BTPayPalClient(apiClient: apiClient) let payPalMessagingView = BTPayPalMessagingView(apiClient: apiClient) let threeDSecureClient = BTThreeDSecureClient(apiClient: apiClient) - let venmoClient = BTVenmoClient(apiClient: apiClient) + let venmoClient = BTVenmoClient( + apiClient: apiClient, + universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")! + ) let sepaDirectDebitClient = BTSEPADirectDebitClient(apiClient: apiClient) } } diff --git a/Sources/BraintreeCore/BTAppContextSwitcher.swift b/Sources/BraintreeCore/BTAppContextSwitcher.swift index 1ea5738eff..43713ab142 100644 --- a/Sources/BraintreeCore/BTAppContextSwitcher.swift +++ b/Sources/BraintreeCore/BTAppContextSwitcher.swift @@ -9,27 +9,6 @@ import UIKit /// Singleton for shared instance of `BTAppContextSwitcher` public static let sharedInstance = BTAppContextSwitcher() - - // NEXT_MAJOR_VERSION: move this property into the feature client request where it is used - /// 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. - @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/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift b/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift index 031a5497b5..558b8fbf04 100644 --- a/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift +++ b/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift @@ -6,17 +6,8 @@ import BraintreeCore struct BTVenmoAppSwitchRedirectURL { - // MARK: - Internal Properties - - /// The base app switch URL for Venmo. Does not include specific parameters. - static var baseAppSwitchURL: URL? { - appSwitchBaseURLComponents().url - } - // MARK: - Private Properties - static private let xCallbackTemplate: String = "scheme://x-callback-url/path" - private var queryParameters: [String: Any?] = [:] // MARK: - Initializer @@ -24,8 +15,7 @@ struct BTVenmoAppSwitchRedirectURL { init( paymentContextID: String, metadata: BTClientMetadata, - returnURLScheme: String?, - universalLink: URL?, + universalLink: URL, forMerchantID merchantID: String?, accessToken: String?, bundleDisplayName: String?, @@ -53,18 +43,11 @@ struct BTVenmoAppSwitchRedirectURL { "braintree_environment": environment, "resource_id": paymentContextID, "braintree_sdk_data": base64EncodedBraintreeData ?? "", - "customerClient": "MOBILE_APP" + "customerClient": "MOBILE_APP", + "x-success": universalLink.appendingPathComponent("success").absoluteString, + "x-error": universalLink.appendingPathComponent("error").absoluteString, + "x-cancel": universalLink.appendingPathComponent("cancel").absoluteString ] - - 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 @@ -82,20 +65,4 @@ struct BTVenmoAppSwitchRedirectURL { return urlComponent.url } - - // MARK: - Private Helper Methods - - private func constructRedirectURL(with scheme: String, result: String) -> URL? { - var components = URLComponents(string: BTVenmoAppSwitchRedirectURL.xCallbackTemplate) - components?.scheme = scheme - components?.percentEncodedPath = "/vzero/auth/venmo/\(result)" - return components?.url - } - - private static func appSwitchBaseURLComponents() -> URLComponents { - var components: URLComponents = URLComponents(string: xCallbackTemplate) ?? URLComponents() - components.scheme = BTCoreConstants.venmoURLScheme - components.percentEncodedPath = "/vzero/auth" - return components - } } diff --git a/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift b/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift index 569054b539..dded4671b9 100644 --- a/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift +++ b/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift @@ -69,6 +69,5 @@ struct BTVenmoAppSwitchReturnURL { /// - Returns: `true` if the url represents a Venmo Touch app switch return static func isValid(url: URL) -> Bool { (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 0b3be83ea2..92eccb24e8 100644 --- a/Sources/BraintreeVenmo/BTVenmoClient.swift +++ b/Sources/BraintreeVenmo/BTVenmoClient.swift @@ -29,6 +29,15 @@ import BraintreeCore /// Stored property used to determine whether a Venmo account nonce should be vaulted after an app switch return var shouldVault: Bool = false + /// Used for linking events from the client to server side request + /// In the Venmo flow this will be the payment context ID + private var payPalContextID: String? + + /// Used for sending the type of flow, universal vs deeplink to FPTI + private var linkType: LinkType? + + private var universalLink: URL + /// Used internally as a holder for the completion in methods that do not pass a completion such as `handleOpen`. /// This allows us to set and return a completion in our methods that otherwise cannot require a completion. var appSwitchCompletion: (BTVenmoAccountNonce?, Error?) -> Void = { _, _ in } @@ -39,32 +48,17 @@ import BraintreeCore /// We require a static reference of the client to call `handleReturnURL` and return to the app. static var venmoClient: BTVenmoClient? - /// Used for linking events from the client to server side request - /// In the Venmo flow this will be the payment context ID - private var payPalContextID: String? - - /// Used for sending the type of flow, universal vs deeplink to FPTI - private var linkType: LinkType? - - private var universalLink: URL? - // MARK: - Initializer - /// 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) + public init(apiClient: BTAPIClient, universalLink: URL) { + BTAppContextSwitcher.sharedInstance.register(BTVenmoClient.self) + + self.apiClient = apiClient self.universalLink = universalLink } @@ -77,27 +71,9 @@ import BraintreeCore /// an instance of `BTVenmoAccountNonce`; on failure or user cancelation you will receive an error. /// If the user cancels out of the flow, the error code will be `.canceled`. @objc(tokenizeWithVenmoRequest:completion:) - // swiftlint:disable:next function_body_length cyclomatic_complexity + // swiftlint:disable:next function_body_length public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) { apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted, isVaultRequest: shouldVault) - let returnURLScheme = BTAppContextSwitcher.sharedInstance._returnURLScheme - - if returnURLScheme.isEmpty { - NSLog( - "%@ Venmo requires a return URL scheme to be configured via [BTAppContextSwitcher setReturnURLScheme:]", - BTLogLevelDescription.string(for: .critical) - ) - notifyFailure(with: BTVenmoError.appNotAvailable, completion: completion) - return - } else if let bundleIdentifier = bundle.bundleIdentifier, !returnURLScheme.hasPrefix(bundleIdentifier) { - NSLog( - // swiftlint:disable:next line_length - "%@ Venmo requires [BTAppContextSwitcher setReturnURLScheme:] to be configured to begin with your app's bundle ID (%@). Currently, it is set to (%@)", - BTLogLevelDescription.string(for: .critical), - bundleIdentifier, - returnURLScheme - ) - } apiClient.fetchOrReturnRemoteConfiguration { configuration, error in if let error { @@ -164,7 +140,6 @@ import BraintreeCore let appSwitchURL = try BTVenmoAppSwitchRedirectURL( paymentContextID: paymentContextID, metadata: metadata, - returnURLScheme: returnURLScheme, universalLink: self.universalLink, forMerchantID: merchantProfileID, accessToken: configuration.venmoAccessToken, diff --git a/Sources/BraintreeVenmo/BTVenmoError.swift b/Sources/BraintreeVenmo/BTVenmoError.swift index 3aa2552a01..ac13497ac8 100644 --- a/Sources/BraintreeVenmo/BTVenmoError.swift +++ b/Sources/BraintreeVenmo/BTVenmoError.swift @@ -34,34 +34,31 @@ public enum BTVenmoError: Error, CustomNSError, LocalizedError, Equatable { /// 1. Venmo is not enabled case disabled - /// 2. The Venmo app is not installed or configured for app Switch - case appNotAvailable - - /// 3. Bundle display name is nil + /// 2. Bundle display name is nil case bundleDisplayNameMissing - /// 4. App Switch could not complete + /// 3. App Switch could not complete case appSwitchFailed - /// 5. Return URL is invalid + /// 4. Return URL is invalid case invalidReturnURL(String) - /// 6. No body was returned from the request + /// 5. No body was returned from the request case invalidBodyReturned - /// 7. Invalid request URL + /// 6. Invalid request URL case invalidRedirectURL(String) - /// 8. Failed to fetch Braintree configuration + /// 7. Failed to fetch Braintree configuration case fetchConfigurationFailed - /// 9. Enriched Customer Data is disabled + /// 8. Enriched Customer Data is disabled case enrichedCustomerDataDisabled - /// 10. The Venmo flow was canceled by the user + /// 9. The Venmo flow was canceled by the user case canceled - /// 11. One or more values in redirect URL are invalid + /// 10. One or more values in redirect URL are invalid case invalidRedirectURLParameter public static var errorDomain: String { @@ -74,26 +71,24 @@ public enum BTVenmoError: Error, CustomNSError, LocalizedError, Equatable { return 0 case .disabled: return 1 - case .appNotAvailable: - return 2 case .bundleDisplayNameMissing: - return 3 + return 2 case .appSwitchFailed: - return 4 + return 3 case .invalidReturnURL: - return 5 + return 4 case .invalidBodyReturned: - return 6 + return 5 case .invalidRedirectURL: - return 7 + return 6 case .fetchConfigurationFailed: - return 8 + return 7 case .enrichedCustomerDataDisabled: - return 9 + return 8 case .canceled: - return 10 + return 9 case .invalidRedirectURLParameter: - return 11 + return 10 } } @@ -103,8 +98,6 @@ public enum BTVenmoError: Error, CustomNSError, LocalizedError, Equatable { return "An unknown error occurred. Please contact support." case .disabled: return "Venmo is not enabled for this merchant account." - case .appNotAvailable: - return "The Venmo app is not installed on this device, or it is not configured or available for app switch." case .bundleDisplayNameMissing: return "CFBundleDisplayName must be non-nil. Please set 'Bundle display name' in your Info.plist." case .appSwitchFailed: diff --git a/UnitTests/BraintreeCoreTests/BTAppContextSwitcher_Tests.swift b/UnitTests/BraintreeCoreTests/BTAppContextSwitcher_Tests.swift index 153be1fd82..de9c018e5d 100644 --- a/UnitTests/BraintreeCoreTests/BTAppContextSwitcher_Tests.swift +++ b/UnitTests/BraintreeCoreTests/BTAppContextSwitcher_Tests.swift @@ -16,11 +16,6 @@ class BTAppContextSwitcher_Tests: XCTestCase { super.tearDown() } - func testSetReturnURLScheme() { - BTAppContextSwitcher.sharedInstance.returnURLScheme = "com.some.scheme" - XCTAssertEqual(appSwitch.returnURLScheme, "com.some.scheme") - } - func testHandleOpenURL_whenClientIsRegistered_invokesCanHandleReturnURL() { appSwitch.register(MockAppContextSwitchClient.self) let expectedURL = URL(string: "fake://url")! diff --git a/UnitTests/BraintreeLocalPaymentTests/BTLocalPaymentClient_UnitTests.swift b/UnitTests/BraintreeLocalPaymentTests/BTLocalPaymentClient_UnitTests.swift index 0f8e55f11d..0b415b668f 100644 --- a/UnitTests/BraintreeLocalPaymentTests/BTLocalPaymentClient_UnitTests.swift +++ b/UnitTests/BraintreeLocalPaymentTests/BTLocalPaymentClient_UnitTests.swift @@ -20,7 +20,6 @@ class BTLocalPaymentClient_UnitTests: XCTestCase { ) localPaymentRequest.localPaymentFlowDelegate = mockLocalPaymentRequestDelegate mockAPIClient = MockAPIClient(authorization: tempClientToken)! - BTAppContextSwitcher.sharedInstance.returnURLScheme = "com.my-return-url-scheme" } func testStartPayment_returnsErrorWhenConfigurationNil() { diff --git a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift index 7091d1be30..48c697ca1c 100644 --- a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift +++ b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift @@ -9,8 +9,7 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { _ = try BTVenmoAppSwitchRedirectURL( paymentContextID: "12345", metadata: BTClientMetadata(), - returnURLScheme: "url-scheme", - universalLink: nil, + universalLink: URL(string: "https://mywebsite.com/braintree-payments")!, forMerchantID: nil, accessToken: "access-token", bundleDisplayName: "display-name", @@ -18,7 +17,7 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { ) } catch { XCTAssertEqual(error as? BTVenmoError, .invalidRedirectURLParameter) - XCTAssertEqual((error as NSError).code, 11) + XCTAssertEqual((error as NSError).code, 10) XCTAssertEqual((error as NSError).localizedDescription, "One or more values in redirect URL are invalid.") } } @@ -28,8 +27,7 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { let requestURL = try BTVenmoAppSwitchRedirectURL( paymentContextID: "12345", metadata: BTClientMetadata(), - returnURLScheme: nil, - universalLink: URL(string: "https://mywebsite.com/braintree-payments"), + universalLink: URL(string: "https://mywebsite.com/braintree-payments")!, forMerchantID: "merchant-id", accessToken: "access-token", bundleDisplayName: "display-name", diff --git a/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift b/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift index 2d8a8183a4..dbaac78930 100644 --- a/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift +++ b/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift @@ -7,6 +7,7 @@ import UIKit class BTVenmoClient_Tests: XCTestCase { var mockAPIClient : MockAPIClient = MockAPIClient(authorization: "development_tokenization_key")! var venmoRequest: BTVenmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse) + var venmoClient: BTVenmoClient! override func setUp() { super.setUp() @@ -28,12 +29,15 @@ class BTVenmoClient_Tests: XCTestCase { ] ] ]) + + venmoClient = BTVenmoClient( + apiClient: mockAPIClient, + universalLink: URL(string: "https://mywebsite.com/braintree-payments")! + ) } func testTokenize_whenRemoteConfigurationFetchFails_callsBackWithConfigurationError() { mockAPIClient.cannedConfigurationResponseError = NSError(domain: "", code: 0, userInfo: nil) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let expectation = expectation(description: "Tokenize fails with error") venmoClient.tokenize(venmoRequest) { venmoAccount, error in @@ -46,8 +50,6 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_whenVenmoConfigurationDisabled_callsBackWithError() { mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [:] as [String: Any?]) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let expectation = expectation(description: "tokenization callback") venmoClient.tokenize(venmoRequest) { venmoAccount, error in @@ -61,8 +63,6 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_whenVenmoConfigurationMissing_callsBackWithError() { mockAPIClient.cannedConfigurationResponseBody = BTJSON(value: [:] as [String: Any?]) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let expectation = expectation(description: "tokenization callback") venmoClient.tokenize(venmoRequest) { venmoAccount, error in @@ -74,25 +74,8 @@ class BTVenmoClient_Tests: XCTestCase { waitForExpectations(timeout: 2) } - func testTokenizeVenmoAccount_whenReturnURLSchemeIsNil_andCallsBackWithError() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "" - - let expectation = expectation(description: "authorization callback") - venmoClient.tokenize(venmoRequest) { venmoAccount, error in - guard let error = error as NSError? else {return} - XCTAssertEqual(error.domain, BTVenmoError.errorDomain) - XCTAssertEqual(error.code, BTVenmoError.appNotAvailable.errorCode) - expectation.fulfill() - } - - waitForExpectations(timeout: 2) - } - func testTokenizeVenmoAccount_whenPaymentMethodUsageSet_createsPaymentContext() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoRequest.displayName = "app-display-name" - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -112,8 +95,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenDisplayNameNotSet_createsPaymentContextWithoutDisplayName() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -141,10 +122,8 @@ class BTVenmoClient_Tests: XCTestCase { ] ]) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoRequest.collectCustomerBillingAddress = true venmoRequest.collectCustomerShippingAddress = true - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -168,10 +147,9 @@ class BTVenmoClient_Tests: XCTestCase { "enrichedCustomerDataEnabled": true ] ]) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoRequest.collectCustomerBillingAddress = true venmoRequest.collectCustomerShippingAddress = true - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" + let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -194,11 +172,9 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_withAmountsAndLineItemsSet_createsPaymentContext() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoRequest.subTotalAmount = "9" venmoRequest.totalAmount = "9" venmoRequest.lineItems = [BTVenmoLineItem(quantity: 1, unitAmount: "9", name: "name", kind: .debit)] - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -239,8 +215,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenize_withoutLineItems_createsPaymentContextWithoutTransactionDetails() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -259,8 +233,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_opensVenmoURLWithPaymentContextID() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -286,8 +258,6 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_whenCannotParsePaymentContextID_callsBackWithError() { mockAPIClient.cannedResponseBody = BTJSON(value: ["random":["lady_gaga":"poker_face"]]) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -308,8 +278,6 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_whenFetchPaymentContextIDFails_callsBackWithError() { mockAPIClient.cannedResponseError = NSError(domain: "Venmo Error", code: 100, userInfo: nil) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -328,8 +296,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenVenmoIsEnabledInControlPanelAndConfiguredCorrectly_opensVenmoURLWithParams() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -351,8 +317,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenReturnURLContainsPaymentContextID_getsResultFromPaymentContext() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -379,8 +343,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenReturnURLContainsPaymentContextID_andFetchPaymentContextFails_returnsError() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -401,8 +363,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenUsingTokenizationKeyAndAppSwitchSucceeds_tokenizesVenmoAccount() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -426,8 +386,6 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_whenUsingClientTokenAndAppSwitchSucceeds_tokenizesVenmoAccount() { // Test setup sets up mockAPIClient with a tokenization key, we want a client token mockAPIClient.authorization = try! BTClientToken(clientToken: TestClientTokenFactory.token(withVersion: 2)) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -449,8 +407,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenAppSwitchFails_callsBackWithError() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -467,8 +423,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_vaultTrue_setsShouldVaultProperty() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -477,7 +431,7 @@ class BTVenmoClient_Tests: XCTestCase { venmoRequest.vault = true venmoClient.tokenize(venmoRequest) { venmoAccount, error in - XCTAssertTrue(venmoClient.shouldVault) + XCTAssertTrue(self.venmoClient.shouldVault) expectation.fulfill() } @@ -486,15 +440,13 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_vaultFalse_setsVaultToFalse() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() let expectation = expectation(description: "Callback invoked") venmoClient.tokenize(venmoRequest) { venmoAccount, error in - XCTAssertFalse(venmoClient.shouldVault) + XCTAssertFalse(self.venmoClient.shouldVault) expectation.fulfill() } @@ -505,9 +457,6 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_vaultTrue_callsBackWithNonce() { mockAPIClient.authorization = try! BTClientToken(clientToken: TestClientTokenFactory.validClientToken) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -546,10 +495,8 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_vaultTrue_sendsSucessAnalyticsEvent() { mockAPIClient.authorization = try! BTClientToken(clientToken: TestClientTokenFactory.validClientToken) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let expectation = expectation(description: "Callback invoked") @@ -588,10 +535,8 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenizeVenmoAccount_vaultTrue_sendsFailureAnalyticsEvent() { mockAPIClient.authorization = try! BTClientToken(clientToken: TestClientTokenFactory.validClientToken) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let expectation = expectation(description: "Callback invoked") @@ -612,10 +557,8 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenAppSwitchCanceled_callsBackWithCancelError() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let expectation = expectation(description: "Callback invoked") venmoClient.tokenize(venmoRequest) { venmoAccount, error in @@ -624,7 +567,7 @@ class BTVenmoClient_Tests: XCTestCase { let error = error! as NSError XCTAssertEqual(error.localizedDescription, BTVenmoError.canceled.localizedDescription) - XCTAssertEqual(error.code, 10) + XCTAssertEqual(error.code, 9) expectation.fulfill() } @@ -634,8 +577,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testAuthorizeAccountWithProfileID_withNilProfileID_usesDefaultProfileIDAndAccessTokenFromConfiguration() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -649,8 +590,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testAuthorizeAccountWithProfileID_withProfileID_usesProfileIDToAppSwitch() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() @@ -666,9 +605,7 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenIsFinalAmountSetAsTrue_createsPaymentContext() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoRequest.displayName = "app-display-name" - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication @@ -686,9 +623,7 @@ class BTVenmoClient_Tests: XCTestCase { } func testTokenizeVenmoAccount_whenIsFinalAmountSetAsFalse_createsPaymentContext() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoRequest.displayName = "app-display-name" - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication @@ -710,18 +645,19 @@ class BTVenmoClient_Tests: XCTestCase { func testAPIClientMetadata_hasIntegrationSetToCustom() { // API client by default uses source = .Unknown and integration = .Custom let apiClient = BTAPIClient(authorization: "development_testing_integration_merchant_id")! - let venmoClient = BTVenmoClient(apiClient: apiClient) - + let venmoClient = BTVenmoClient( + apiClient: apiClient, + universalLink: URL(string: "https://mywebsite.com/braintree-payments")! + ) + XCTAssertEqual(venmoClient.apiClient.metadata.integration, BTClientMetadataIntegration.custom) } func testTokenize_whenConfigurationIsInvalid_returnsError() async { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() mockAPIClient.cannedConfigurationResponseBody = nil - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" do { let _ = try await venmoClient.tokenize(venmoRequest) @@ -734,14 +670,12 @@ class BTVenmoClient_Tests: XCTestCase { func testTokenize_whenVenmoRequest_setsVaultAnalyticsTag() async { let venmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse) - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) let _ = try? await venmoClient.tokenize(venmoRequest) XCTAssertFalse(mockAPIClient.postedIsVaultRequest) } func testHandleOpen_sendsHandleReturnStartedEvent() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) let appSwitchURL = URL(string: "some-url")! venmoClient.handleOpen(appSwitchURL) @@ -749,7 +683,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testStartVenmoFlow_sendsAppSwitchStartedEvent() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) let appSwitchURL = URL(string: "some-url")! venmoClient.startVenmoFlow(with: appSwitchURL, shouldVault: false) { _, _ in } @@ -757,7 +690,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testInvokedOpenURLSuccessfully_whenSuccess_sendsAppSwitchSucceeded_withAppSwitchURL() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) let eventName = BTVenmoAnalytics.appSwitchSucceeded let appSwitchURL = URL(string: "some-url")! venmoClient.invokedOpenURLSuccessfully(true, shouldVault: true, appSwitchURL: appSwitchURL) { _, _ in } @@ -767,7 +699,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testInvokedOpenURLSuccessfully_whenFailure_sendsAppSwitchFailed_withAppSwitchURL() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) let eventName = BTVenmoAnalytics.appSwitchFailed let appSwitchURL = URL(string: "some-url")! venmoClient.invokedOpenURLSuccessfully(false, shouldVault: true, appSwitchURL: appSwitchURL) { _, _ in } @@ -779,9 +710,7 @@ class BTVenmoClient_Tests: XCTestCase { // MARK: - BTAppContextSwitchClient func testCanHandleReturnURL_withValidHost_andValidPath_returnsTrue() { - let host = "x-callback-url" - let path = "/vzero/auth/venmo/" - XCTAssertTrue(BTVenmoClient.canHandleReturnURL(URL(string: "fake-scheme://\(host)\(path)fake-result")!)) + XCTAssertTrue(BTVenmoClient.canHandleReturnURL(URL(string: "https://www.braintreesample.com/success")!)) } func testCanHandleReturnURL_withInvalidHost_andValidPath_returnsFalse() { @@ -801,10 +730,6 @@ class BTVenmoClient_Tests: XCTestCase { } func testAuthorizeAccountWithTokenizationKey_vaultTrue_willNotAttemptToVault() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" - venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -834,8 +759,6 @@ class BTVenmoClient_Tests: XCTestCase { // MARK: - openVenmoAppPageInAppStore func testGotoVenmoInAppStore_opensVenmoAppStoreURL() { - let venmoClient = BTVenmoClient(apiClient: mockAPIClient) - BTAppContextSwitcher.sharedInstance.returnURLScheme = "scheme" let fakeApplication = FakeApplication() venmoClient.application = fakeApplication venmoClient.bundle = FakeBundle() diff --git a/V7_MIGRATION.md b/V7_MIGRATION.md index b17c233a8d..c08403eb1a 100644 --- a/V7_MIGRATION.md +++ b/V7_MIGRATION.md @@ -14,8 +14,6 @@ _Documentation for v7 will be published to https://developer.paypal.com/braintre 1. [3D Secure](#3d-secure)] 1. [PayPal](#paypal) 1. [PayPal Native Checkout](#paypal-native-checkout) -1. [PayPal](#paypal) - ## Supported Versions @@ -27,12 +25,22 @@ v7 updates `BTCard` to require setting all properties through the initializer, r ## Venmo All properties within `BTVenmoRequest` can only be accessed on the initializer vs via the dot syntax. -Remove the `fallbackToWeb` boolean parameter from `BTVenmoRequest`. If a Buyer has the Venmo app installed and taps on "Pay with Venmo", they will automatically be switched to the Venmo app. If the Venmo app isn't installed, the Buyer will fallback to their default web brower to checkout. +Remove the `fallbackToWeb` boolean parameter from `BTVenmoRequest`. If a Buyer has the Venmo app installed and taps on "Pay with Venmo", they will automatically be switched to the Venmo app. If the Venmo app isn't installed, the Buyer will fallback to their default web browser to checkout. ``` let venmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse, vault: true) ``` +The `BTVenmoClient` initializer now requires a `universalLink` for switching to and from the Venmo app or web fallback flow + +```swift +let apiClient = BTAPIClient("") +let venmoClient = BTVenmoClient( + apiClient: apiClient, + universalLink: URL(string: "https://merchant-app.com/braintree-payments")! // merchant universal link +) +``` + ## SEPA Direct Debit All properties within `BTSEPADirectDebitRequest` can only be accessed on the initializer vs via the dot syntax. @@ -44,6 +52,8 @@ All properties within `BTThreeDSecureRequest` can only be accessed on the initia ## PayPal +v7 updates `BTPayPalRequest`, `BTPayPalVaultRequest` and `BTPayPalCheckoutRequest` to make all properties accessible on the initializer only vs via the dot syntax. + ### App Switch For the App Switch flow, you must update your `info.plist` with a simplified URL query scheme name, `paypal`. @@ -58,6 +68,3 @@ For the App Switch flow, you must update your `info.plist` with a simplified URL ## PayPal Native Checkout The PayPal Native Checkout integration is no longer supported. Please remove it from your app and use the [PayPal (web)](https://developer.paypal.com/braintree/docs/guides/paypal/overview/ios/v6) integration. - -## PayPal -v7 updates `BTPayPalRequest`, `BTPayPalVaultRequest` and `BTPayPalCheckoutRequest` to make all properties accessible on the initializer only vs via the dot syntax.