diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index fccfa2bc49..40cd0b4d64 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,6 +9,7 @@ Thank you for your contribution to Braintree. ### Checklist - [ ] Added a changelog entry +- [ ] Tested and confirmed payment flows affected by this change are functioning as expected ### Authors > List GitHub usernames for everyone who contributed to this pull request. diff --git a/Braintree.podspec b/Braintree.podspec index dda5571a3e..839bf1b0f0 100644 --- a/Braintree.podspec +++ b/Braintree.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Braintree" - s.version = "6.24.0" + s.version = "6.25.0" s.summary = "Braintree iOS SDK: Helps you accept card and alternative payments in your iOS app." s.description = <<-DESC Braintree is a full-stack payments platform for developers diff --git a/CHANGELOG.md b/CHANGELOG.md index 24232627b2..ef0c82f103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,11 +27,17 @@ * BraintreePayPal * Update PayPal app URL query scheme from `paypal-app-switch-checkout` to `paypal` -## unreleased +## 6.25.0 (2024-12-11) * BraintreePayPal * Add `BTPayPalRequest.userPhoneNumber` optional property + * Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI) * BraintreeVenmo * Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI) + * Add `BTVenmoClient(apiClient:universalLink:)` to use Universal Links when redirecting back from the Venmo flow +* BraintreeCore + * Deprecate `BTAppContextSwitcher.sharedInstance.returnURLScheme` +* BraintreeThreeDSecure + * Add `BTThreeDSecureRequest.requestorAppURL` ## 6.24.0 (2024-10-15) * BraintreePayPal diff --git a/Demo/Application/Features/VenmoViewController.swift b/Demo/Application/Features/VenmoViewController.swift index 24b6494a0c..d5ef626ef6 100644 --- a/Demo/Application/Features/VenmoViewController.swift +++ b/Demo/Application/Features/VenmoViewController.swift @@ -2,13 +2,15 @@ import UIKit import BraintreeVenmo class VenmoViewController: PaymentButtonBaseViewController { - + // swiftlint:disable:next implicitly_unwrapped_optional 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) title = "Custom Venmo Button" @@ -17,7 +19,7 @@ class VenmoViewController: PaymentButtonBaseViewController { override func createPaymentButton() -> UIView { let venmoButton = createButton(title: "Venmo", action: #selector(tappedVenmo)) - let stackView = UIStackView(arrangedSubviews: [vaultToggle, venmoButton]) + let stackView = UIStackView(arrangedSubviews: [vaultToggle, universalLinkReturnToggle, venmoButton]) stackView.axis = .vertical stackView.spacing = 15 stackView.alignment = .fill @@ -29,13 +31,21 @@ class VenmoViewController: PaymentButtonBaseViewController { @objc func tappedVenmo() { self.progressBlock("Tapped Venmo - initiating Venmo auth") - + let isVaultingEnabled = vaultToggle.isOn let venmoRequest = BTVenmoRequest( paymentMethodUsage: .multiUse, 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/Demo/Application/Supporting Files/Braintree-Demo-Info.plist b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist index ad49c33372..495324c214 100644 --- a/Demo/Application/Supporting Files/Braintree-Demo-Info.plist +++ b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist @@ -41,7 +41,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 6.24.0 + 6.25.0 CFBundleURLTypes @@ -56,7 +56,7 @@ CFBundleVersion - 6.24.0 + 6.25.0 LSApplicationQueriesSchemes com.braintreepayments.Demo.payments diff --git a/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_UITests_Extensions.swift b/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_UITests_Extensions.swift index e641822bf9..27e454da71 100644 --- a/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_UITests_Extensions.swift +++ b/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_UITests_Extensions.swift @@ -22,14 +22,6 @@ internal extension XCUIApplication { return buttons["Tokenize and Verify New Card"] } - var webViewPasswordTextField: XCUIElement { - return webViews.element.otherElements.children(matching: .other).children(matching: .secureTextField).element - } - - var webViewSubmitButton: XCUIElement { - return webViews.element.otherElements.children(matching: .other).children(matching: .other).buttons["Submit"] - } - var cardinalSubmitButton: XCUIElement { return buttons["SUBMIT"] } @@ -37,23 +29,11 @@ internal extension XCUIApplication { var liabilityShiftedMessage: XCUIElement { return buttons["Liability shift possible and liability shifted"] } - - var authenticationFailedMessage: XCUIElement { - return buttons["Failed to authenticate, please try a different form of payment."] - } - + var liabilityCouldNotBeShiftedMessage: XCUIElement { return buttons["3D Secure authentication was attempted but liability shift is not possible"] } - var unexpectedErrorMessage: XCUIElement { - return buttons["An unexpected error occurred"] - } - - var internalErrorMessage: XCUIElement { - return buttons["Internal Error."] - } - func enterCardDetailsWith(cardNumber: String, expirationDate: String = UITestDateGenerator.sharedInstance.futureDate()) { cardNumberTextField.tap() cardNumberTextField.typeText(cardNumber) diff --git a/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_V2_UITests.swift b/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_V2_UITests.swift index 78a8eff2af..33fcc52054 100644 --- a/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_V2_UITests.swift +++ b/Demo/UI Tests/ThreeDSecure UI Tests/ThreeDSecure_V2_UITests.swift @@ -16,15 +16,6 @@ class ThreeDSecure_V2_UITests: XCTestCase { app.launch() } - func testThreeDSecurePaymentFlowV2_frictionlessFlow_andTransacts() { - waitForElementToAppear(app.cardNumberTextField) - app.enterCardDetailsWith(cardNumber: "4000000000001000", expirationDate: expirationDate) - app.tokenizeButton.tap() - sleep(2) - - waitForElementToAppear(app.liabilityShiftedMessage) - } - func testThreeDSecurePaymentFlowV2_challengeFlow_andTransacts() { waitForElementToAppear(app.cardNumberTextField) app.enterCardDetailsWith(cardNumber: "4000000000001091", expirationDate: expirationDate) @@ -44,16 +35,7 @@ class ThreeDSecure_V2_UITests: XCTestCase { waitForElementToAppear(app.liabilityShiftedMessage) } - - func testThreeDSecurePaymentFlowV2_noChallenge_andFails() { - waitForElementToAppear(app.cardNumberTextField) - app.enterCardDetailsWith(cardNumber: "5200000000001013", expirationDate: expirationDate) - app.tokenizeButton.tap() - sleep(2) - - waitForElementToAppear(app.liabilityCouldNotBeShiftedMessage) - } - + func testThreeDSecurePaymentFlowV2_challengeFlow_andFails() { waitForElementToAppear(app.cardNumberTextField) app.enterCardDetailsWith(cardNumber: "4000000000001109", expirationDate: expirationDate) @@ -74,26 +56,6 @@ class ThreeDSecure_V2_UITests: XCTestCase { waitForElementToAppear(app.liabilityCouldNotBeShiftedMessage, timeout: 30) } - func testThreeDSecurePaymentFlowV2_acceptsPassword_failsToAuthenticateNonce_dueToCardinalError() { - waitForElementToAppear(app.cardNumberTextField) - app.enterCardDetailsWith(cardNumber: "4000000000001125") - app.tokenizeButton.tap() - sleep(2) - - waitForElementToAppear(app.staticTexts["Purchase Authentication"], timeout: .threeDSecureTimeout) - - let textField = app.textFields.element(boundBy: 0) - waitForElementToBeHittable(textField) - textField.forceTapElement() - sleep(2) - textField.typeText("1234") - - app.cardinalSubmitButton.forceTapElement() - sleep(2) - - waitForElementToAppear(app.internalErrorMessage, timeout: 30) - } - func testThreeDSecurePaymentFlowV2_returnsToApp_whenCancelTapped() { waitForElementToAppear(app.cardNumberTextField) app.enterCardDetailsWith(cardNumber: "4000000000001091") @@ -106,31 +68,4 @@ class ThreeDSecure_V2_UITests: XCTestCase { waitForElementToAppear(app.buttons["Canceled 🎲"]) } - - func testThreeDSecurePaymentFlowV2_bypassedAuthentication() { - waitForElementToAppear(app.cardNumberTextField) - app.enterCardDetailsWith(cardNumber: "4000000000001083") - app.tokenizeButton.tap() - sleep(2) - - waitForElementToAppear(app.liabilityCouldNotBeShiftedMessage) - } - - func testThreeDSecurePaymentFlowV2_lookupError() { - waitForElementToAppear(app.cardNumberTextField) - app.enterCardDetailsWith(cardNumber: "4000000000001034") - app.tokenizeButton.tap() - sleep(2) - - waitForElementToAppear(app.liabilityCouldNotBeShiftedMessage) - } - - func testThreeDSecurePaymentFlowV2_timeout() { - waitForElementToAppear(app.cardNumberTextField) - app.enterCardDetailsWith(cardNumber: "4000000000001075") - app.tokenizeButton.tap() - sleep(2) - - waitForElementToAppear(app.liabilityCouldNotBeShiftedMessage, timeout: 45) - } } diff --git a/Sources/BraintreeCore/BTAppContextSwitcher.swift b/Sources/BraintreeCore/BTAppContextSwitcher.swift index db875e268d..1ea5738eff 100644 --- a/Sources/BraintreeCore/BTAppContextSwitcher.swift +++ b/Sources/BraintreeCore/BTAppContextSwitcher.swift @@ -14,7 +14,22 @@ import UIKit /// The URL scheme to return to this app after switching to another app or opening a SFSafariViewController. /// This URL scheme must be registered as a URL Type in the app's info.plist, and it must start with the app's bundle ID. /// - Note: This property should only be used for the Venmo flow. - public var returnURLScheme: String = "" + @available( + *, + deprecated, + message: "returnURLScheme is deprecated and will be removed in a future version. Use BTVenmoClient(apiClient:universalLink:)." + ) + public var returnURLScheme: String { + get { _returnURLScheme } + set { _returnURLScheme = newValue } + } + + // swiftlint:disable identifier_name + /// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. + /// Property for `returnURLScheme`. Created to avoid deprecation warnings upon accessing + /// `returnURLScheme` directly within our SDK. Use this value instead. + public var _returnURLScheme: String = "" + // swiftlint:enable identifier_name // MARK: - Private Properties diff --git a/Sources/BraintreeCore/BTCoreConstants.swift b/Sources/BraintreeCore/BTCoreConstants.swift index bce33c21f6..8bf5690bea 100644 --- a/Sources/BraintreeCore/BTCoreConstants.swift +++ b/Sources/BraintreeCore/BTCoreConstants.swift @@ -5,7 +5,7 @@ import Foundation @objcMembers public class BTCoreConstants: NSObject { /// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. - public static var braintreeSDKVersion: String = "6.24.0" + public static var braintreeSDKVersion: String = "6.25.0" /// :nodoc: This property is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time. public static let callbackURLScheme: String = "sdk.ios.braintree" diff --git a/Sources/BraintreeCore/Info.plist b/Sources/BraintreeCore/Info.plist index 0a03531a0b..8724499eed 100644 --- a/Sources/BraintreeCore/Info.plist +++ b/Sources/BraintreeCore/Info.plist @@ -15,11 +15,11 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 6.24.0 + 6.25.0 CFBundleSignature ???? CFBundleVersion - 6.24.0 + 6.25.0 NSPrincipalClass diff --git a/Sources/BraintreeDataCollector/BTDataCollector.swift b/Sources/BraintreeDataCollector/BTDataCollector.swift index 097588f2fa..02d6f18cdb 100644 --- a/Sources/BraintreeDataCollector/BTDataCollector.swift +++ b/Sources/BraintreeDataCollector/BTDataCollector.swift @@ -147,15 +147,14 @@ import BraintreeCore var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) if status == errSecSuccess, - let existingItem = item as? [String: Any], - let data = existingItem[kSecValueData as String] as? Data, - let identifier = String(data: data, encoding: String.Encoding.utf8) { + let data = item as? Data, + let identifier = String(data: data, encoding: .utf8) { return identifier } // If not, generate a new one and save it let newIdentifier = UUID().uuidString - query[kSecValueData as String] = newIdentifier + query[kSecValueData as String] = newIdentifier.data(using: .utf8) query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly SecItemAdd(query as CFDictionary, nil) return newIdentifier diff --git a/Sources/BraintreePayPal/BTPayPalClient.swift b/Sources/BraintreePayPal/BTPayPalClient.swift index 629c542408..bced82fac1 100644 --- a/Sources/BraintreePayPal/BTPayPalClient.swift +++ b/Sources/BraintreePayPal/BTPayPalClient.swift @@ -284,6 +284,29 @@ import BraintreeDataCollector performSwitchRequest(appSwitchURL: url, paymentType: paymentType, completion: completion) } + func invokedOpenURLSuccessfully(_ success: Bool, url: URL, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void) { + if success { + apiClient.sendAnalyticsEvent( + BTPayPalAnalytics.appSwitchSucceeded, + isVaultRequest: isVaultRequest, + linkType: linkType, + payPalContextID: payPalContextID, + appSwitchURL: url + ) + BTPayPalClient.payPalClient = self + appSwitchCompletion = completion + } else { + apiClient.sendAnalyticsEvent( + BTPayPalAnalytics.appSwitchFailed, + isVaultRequest: isVaultRequest, + linkType: linkType, + payPalContextID: payPalContextID, + appSwitchURL: url + ) + notifyFailure(with: BTPayPalError.appSwitchFailed, completion: completion) + } + } + // MARK: - App Switch Methods func handleReturnURL(_ url: URL) { @@ -404,28 +427,7 @@ import BraintreeDataCollector } application.open(redirectURL) { success in - self.invokedOpenURLSuccessfully(success, completion: completion) - } - } - - private func invokedOpenURLSuccessfully(_ success: Bool, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void) { - if success { - apiClient.sendAnalyticsEvent( - BTPayPalAnalytics.appSwitchSucceeded, - isVaultRequest: isVaultRequest, - linkType: linkType, - payPalContextID: payPalContextID - ) - BTPayPalClient.payPalClient = self - appSwitchCompletion = completion - } else { - apiClient.sendAnalyticsEvent( - BTPayPalAnalytics.appSwitchFailed, - isVaultRequest: isVaultRequest, - linkType: linkType, - payPalContextID: payPalContextID - ) - notifyFailure(with: BTPayPalError.appSwitchFailed, completion: completion) + self.invokedOpenURLSuccessfully(success, url: redirectURL, completion: completion) } } diff --git a/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift b/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift index 02f65b93c4..b02c833575 100644 --- a/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift +++ b/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift @@ -29,6 +29,7 @@ import BraintreeCore let shippingMethod: BTThreeDSecureShippingMethod let uiType: BTThreeDSecureUIType let v2UICustomization: BTThreeDSecureV2UICustomization? + let requestorAppURL: String? var dfReferenceID: String? @@ -54,6 +55,8 @@ import BraintreeCore /// - shippingMethod: Optional. The shipping method chosen for the transaction /// - uiType: Optional: Sets all UI types that the device supports for displaying specific challenge user interfaces in the 3D Secure challenge. Defaults to `.both` /// - v2UICustomization: Optional. UI Customization for 3DS2 challenge views. + /// - requestorAppURL: 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 init( amount: String, nonce: String, @@ -72,7 +75,8 @@ import BraintreeCore requestedExemptionType: BTThreeDSecureRequestedExemptionType = .unspecified, shippingMethod: BTThreeDSecureShippingMethod = .unspecified, uiType: BTThreeDSecureUIType = .both, - v2UICustomization: BTThreeDSecureV2UICustomization? = nil + v2UICustomization: BTThreeDSecureV2UICustomization? = nil, + requestorAppURL: String? = nil ) { self.amount = amount self.nonce = nonce @@ -92,5 +96,6 @@ import BraintreeCore self.shippingMethod = shippingMethod self.uiType = uiType self.v2UICustomization = v2UICustomization + self.requestorAppURL = requestorAppURL } } diff --git a/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift b/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift index 89b21f4113..99cd10f9c0 100644 --- a/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift +++ b/Sources/BraintreeThreeDSecure/BTThreeDSecureV2Provider.swift @@ -45,6 +45,10 @@ class BTThreeDSecureV2Provider { cardinalConfiguration.renderType = renderTypes.compactMap { $0.cardinalValue } } + if let requestorAppURL = request.requestorAppURL { + cardinalConfiguration.threeDSRequestorAppURL = requestorAppURL + } + guard let cardinalAuthenticationJWT = configuration.cardinalAuthenticationJWT else { completion(nil) return diff --git a/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift b/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift index c02ad1e2a0..031a5497b5 100644 --- a/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift +++ b/Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift @@ -22,9 +22,10 @@ struct BTVenmoAppSwitchRedirectURL { // MARK: - Initializer init( - returnURLScheme: String, paymentContextID: String, metadata: BTClientMetadata, + returnURLScheme: String?, + universalLink: URL?, forMerchantID merchantID: String?, accessToken: String?, bundleDisplayName: String?, @@ -46,9 +47,6 @@ struct BTVenmoAppSwitchRedirectURL { let base64EncodedBraintreeData = serializedBraintreeData?.base64EncodedString() queryParameters = [ - "x-success": constructRedirectURL(with: returnURLScheme, result: "success"), - "x-error": constructRedirectURL(with: returnURLScheme, result: "error"), - "x-cancel": constructRedirectURL(with: returnURLScheme, result: "cancel"), "x-source": bundleDisplayName, "braintree_merchant_id": merchantID, "braintree_access_token": accessToken, @@ -57,6 +55,16 @@ struct BTVenmoAppSwitchRedirectURL { "braintree_sdk_data": base64EncodedBraintreeData ?? "", "customerClient": "MOBILE_APP" ] + + if let universalLink { + queryParameters["x-success"] = universalLink.appendingPathComponent("success").absoluteString + queryParameters["x-error"] = universalLink.appendingPathComponent("error").absoluteString + queryParameters["x-cancel"] = universalLink.appendingPathComponent("cancel").absoluteString + } else if let returnURLScheme { + queryParameters["x-success"] = constructRedirectURL(with: returnURLScheme, result: "success") + queryParameters["x-error"] = constructRedirectURL(with: returnURLScheme, result: "error") + queryParameters["x-cancel"] = constructRedirectURL(with: returnURLScheme, result: "cancel") + } } // MARK: - Internal Methods diff --git a/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift b/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift index b1fcd30be4..569054b539 100644 --- a/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift +++ b/Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift @@ -41,7 +41,7 @@ struct BTVenmoAppSwitchReturnURL { init?(url: URL) { let parameters = BTURLUtils.queryParameters(for: url) - if url.path == "/vzero/auth/venmo/success" { + if url.path.contains("success") { if let resourceID = parameters["resource_id"] { state = .succeededWithPaymentContext paymentContextID = resourceID @@ -50,12 +50,12 @@ struct BTVenmoAppSwitchReturnURL { nonce = parameters["paymentMethodNonce"] ?? parameters["payment_method_nonce"] username = parameters["username"] } - } else if url.path == "/vzero/auth/venmo/error" { + } else if url.path.contains("error") { state = .failed let errorMessage: String? = parameters["errorMessage"] ?? parameters["error_message"] let errorCode = Int(parameters["errorCode"] ?? parameters["error_code"] ?? "0") error = BTVenmoAppSwitchError.returnURLError(errorCode ?? 0, errorMessage) - } else if url.path == "/vzero/auth/venmo/cancel" { + } else if url.path.contains("cancel") { state = .canceled } else { state = .unknown @@ -68,6 +68,7 @@ struct BTVenmoAppSwitchReturnURL { /// - Parameter url: an app switch return URL /// - Returns: `true` if the url represents a Venmo Touch app switch return static func isValid(url: URL) -> Bool { - url.host == "x-callback-url" && url.path.hasPrefix("/vzero/auth/venmo/") + (url.scheme == "https" && (url.path.contains("cancel") || url.path.contains("success") || url.path.contains("error"))) + || (url.host == "x-callback-url" && url.path.hasPrefix("/vzero/auth/venmo/")) } } diff --git a/Sources/BraintreeVenmo/BTVenmoClient.swift b/Sources/BraintreeVenmo/BTVenmoClient.swift index 873db989fe..0b3be83ea2 100644 --- a/Sources/BraintreeVenmo/BTVenmoClient.swift +++ b/Sources/BraintreeVenmo/BTVenmoClient.swift @@ -46,9 +46,11 @@ import BraintreeCore /// Used for sending the type of flow, universal vs deeplink to FPTI private var linkType: LinkType? + private var universalLink: URL? + // MARK: - Initializer - /// Creates an Apple Pay client + /// Creates a Venmo client /// - Parameter apiClient: An API client @objc(initWithAPIClient:) public init(apiClient: BTAPIClient) { @@ -56,6 +58,16 @@ import BraintreeCore self.apiClient = apiClient } + /// Initialize a new Venmo client instance. + /// - Parameters: + /// - apiClient: The API Client + /// - universalLink: The URL for the Venmo app to redirect to after user authentication completes. Must be a valid HTTPS URL dedicated to Braintree app switch returns. + @objc(initWithAPIClient:universalLink:) + public convenience init(apiClient: BTAPIClient, universalLink: URL) { + self.init(apiClient: apiClient) + self.universalLink = universalLink + } + // MARK: - Public Methods /// Initiates Venmo login via app switch, which returns a BTVenmoAccountNonce when successful. @@ -68,7 +80,7 @@ import BraintreeCore // swiftlint:disable:next function_body_length cyclomatic_complexity public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) { apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted, isVaultRequest: shouldVault) - let returnURLScheme = BTAppContextSwitcher.sharedInstance.returnURLScheme + let returnURLScheme = BTAppContextSwitcher.sharedInstance._returnURLScheme if returnURLScheme.isEmpty { NSLog( @@ -150,9 +162,10 @@ import BraintreeCore do { let appSwitchURL = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: returnURLScheme, paymentContextID: paymentContextID, metadata: metadata, + returnURLScheme: returnURLScheme, + universalLink: self.universalLink, forMerchantID: merchantProfileID, accessToken: configuration.venmoAccessToken, bundleDisplayName: bundleDisplayName, diff --git a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift index 7fff4ad489..787e9ef356 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift @@ -982,6 +982,24 @@ class BTPayPalClient_Tests: XCTestCase { XCTAssertNil(lastPostParameters["merchant_app_return_url"] as? String) } + func testInvokedOpenURLSuccessfully_whenSuccess_sendsAppSwitchSucceededWithAppSwitchURL() { + let eventName = BTPayPalAnalytics.appSwitchSucceeded + let fakeURL = URL(string: "some-url")! + payPalClient.invokedOpenURLSuccessfully(true, url: fakeURL) { _, _ in } + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, eventName) + XCTAssertEqual(mockAPIClient.postedAppSwitchURL[eventName], fakeURL.absoluteString) + } + + func testInvokedOpenURLSuccessfully_whenFailure_sendsAppSwitchFailedWithAppSwitchURL() { + let eventName = BTPayPalAnalytics.appSwitchFailed + let fakeURL = URL(string: "some-url")! + payPalClient.invokedOpenURLSuccessfully(false, url: fakeURL) { _, _ in } + + XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.first!, eventName) + XCTAssertEqual(mockAPIClient.postedAppSwitchURL[eventName], fakeURL.absoluteString) + } + // MARK: - Analytics func testAPIClientMetadata_hasIntegrationSetToCustom() { diff --git a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift index 98b2cdf2ee..7091d1be30 100644 --- a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift +++ b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift @@ -7,9 +7,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { func testAppSwitchURL_whenMerchantIDNil_throwsError() { do { _ = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: "url-scheme", paymentContextID: "12345", metadata: BTClientMetadata(), + returnURLScheme: "url-scheme", + universalLink: nil, forMerchantID: nil, accessToken: "access-token", bundleDisplayName: "display-name", @@ -25,9 +26,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { func testUniversalLinkURL_whenAllValuesInitialized_returnsURLWithAllValues() { do { let requestURL = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: "url-scheme", paymentContextID: "12345", metadata: BTClientMetadata(), + returnURLScheme: nil, + universalLink: URL(string: "https://mywebsite.com/braintree-payments"), forMerchantID: "merchant-id", accessToken: "access-token", bundleDisplayName: "display-name", @@ -38,9 +40,9 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { let components = URLComponents(string: requestURL.universalLinksURL()!.absoluteString) guard let queryItems = components?.queryItems else { XCTFail(); return } - XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "url-scheme://x-callback-url/vzero/auth/venmo/success"))) - XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "url-scheme://x-callback-url/vzero/auth/venmo/error"))) - XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "url-scheme://x-callback-url/vzero/auth/venmo/cancel"))) + XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "https://mywebsite.com/braintree-payments/success"))) + XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "https://mywebsite.com/braintree-payments/error"))) + XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "https://mywebsite.com/braintree-payments/cancel"))) XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-source", value: "display-name"))) XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_merchant_id", value: "merchant-id"))) XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_access_token", value: "access-token")))