diff --git a/CHANGELOG.md b/CHANGELOG.md index ef0c82f10..500281cd4 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 c07540ce8..d5c35ef5d 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 32a7f1e36..3c1456fc2 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 5943bec64..d851698fc 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 898d1a21a..b817ce9f7 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 d5ef626ef..d6a760ad8 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 748b3f2a4..9e369e5dc 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 3232545c2..b1525c84a 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 1ea5738ef..43713ab14 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 031a5497b..558b8fbf0 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 569054b53..dded4671b 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 0b3be83ea..92eccb24e 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 3aa2552a0..ac13497ac 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 153be1fd8..de9c018e5 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 0f8e55f11..0b415b668 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 7091d1be3..48c697ca1 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 2d8a8183a..dbaac7893 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 b17c233a8..c08403eb1 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.