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 f5d273c832..01086b8d05 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/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 45a9a1046f..be611fb37c 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -98,6 +98,7 @@ 800ED7832B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */; }; 800FC544257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */; }; 8014221C2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8014221B2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift */; }; + 802055222CDC29FE000BE30F /* BraintreeAmexExpress_IntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802055212CDC29FE000BE30F /* BraintreeAmexExpress_IntegrationTests.swift */; }; 8037BFB02B2CCC130017072C /* BTShopperInsightsAnalytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */; }; 804326BF2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */; }; 804698372B27C5390090878E /* BTShopperInsightsClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8064F38E2B1E492F0059C4CB /* BTShopperInsightsClient.swift */; }; @@ -841,6 +842,7 @@ 800ED7822B4F5B66007D8A30 /* BTEligiblePaymentsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTEligiblePaymentsRequest.swift; sourceTree = ""; }; 800FC543257FDC5100DEE132 /* BTApplePayCardNonce_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePayCardNonce_Tests.swift; sourceTree = ""; }; 8014221B2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalApprovalURLParser.swift; sourceTree = ""; }; + 802055212CDC29FE000BE30F /* BraintreeAmexExpress_IntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraintreeAmexExpress_IntegrationTests.swift; sourceTree = ""; }; 8037BFAF2B2CCC130017072C /* BTShopperInsightsAnalytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTShopperInsightsAnalytics.swift; sourceTree = ""; }; 804326BE2B1A5C5B0044E90B /* BTApplePaymentTokensRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTApplePaymentTokensRequest.swift; sourceTree = ""; }; 804698302B27C5340090878E /* BraintreeShopperInsights.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BraintreeShopperInsights.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1778,6 +1780,7 @@ isa = PBXGroup; children = ( A7ABD65D1B702FF000A1223C /* Braintree-API-Integration-Specs */, + 802055212CDC29FE000BE30F /* BraintreeAmexExpress_IntegrationTests.swift */, BE7BBDAE2AE9B628004E7AFC /* BraintreeApplePay_IntegrationTests.swift */, BE7BBDB22AE9B913004E7AFC /* BraintreeDataCollector_IntegrationTests.swift */, 57D9436F296CC79B0079EAB1 /* BraintreePayPal_IntegrationTests.swift */, @@ -3462,6 +3465,7 @@ 57D94372296CCA2F0079EAB1 /* String+NonceValidation.swift in Sources */, BE1ACEF72938F0B800707330 /* BTHTTP_SSLPinning_IntegrationTests.swift in Sources */, BEEB565B2AE9B3030029F264 /* BTIntegrationTestsConstants.swift in Sources */, + 802055222CDC29FE000BE30F /* BraintreeAmexExpress_IntegrationTests.swift in Sources */, BE7BBDB32AE9B913004E7AFC /* BraintreeDataCollector_IntegrationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CHANGELOG.md b/CHANGELOG.md index 156f060520..748b2edb88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,16 @@ # Braintree iOS SDK Release Notes -## 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 0b6537f168..1668872139 100644 --- a/Demo/Application/Features/VenmoViewController.swift +++ b/Demo/Application/Features/VenmoViewController.swift @@ -2,14 +2,16 @@ import UIKit import BraintreeVenmo class VenmoViewController: PaymentButtonBaseViewController { - + // swiftlint:disable:next implicitly_unwrapped_optional var venmoClient: BTVenmoClient! let webFallbackToggle = Toggle(title: "Enable Web Fallback") let vaultToggle = Toggle(title: "Vault") - + let universalLinkReturnToggle = Toggle(title: "Use Universal Link Return") + override func viewDidLoad() { + super.heightConstraint = 150 super.viewDidLoad() venmoClient = BTVenmoClient(apiClient: apiClient) title = "Custom Venmo Button" @@ -18,7 +20,7 @@ class VenmoViewController: PaymentButtonBaseViewController { override func createPaymentButton() -> UIView { let venmoButton = createButton(title: "Venmo", action: #selector(tappedVenmo)) - let stackView = UIStackView(arrangedSubviews: [webFallbackToggle, vaultToggle, venmoButton]) + let stackView = UIStackView(arrangedSubviews: [webFallbackToggle, vaultToggle, universalLinkReturnToggle, venmoButton]) stackView.axis = .vertical stackView.spacing = 15 stackView.alignment = .fill @@ -40,7 +42,15 @@ class VenmoViewController: PaymentButtonBaseViewController { if vaultToggle.isOn { venmoRequest.vault = true } - + + if universalLinkReturnToggle.isOn { + venmoClient = BTVenmoClient( + apiClient: apiClient, + // swiftlint:disable:next force_unwrapping + universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")! + ) + } + Task { do { let venmoAccount = try await venmoClient.tokenize(venmoRequest) diff --git a/Demo/Application/Supporting Files/Braintree-Demo-Info.plist b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist index 03c10f885f..0fff3896e4 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/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 82895479b1..fe5df0082b 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -17,7 +17,6 @@ 42C574B725FA66FB008B3681 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 42C574B625FA66FB008B3681 /* Assets.xcassets */; }; 42C574BA25FA66FB008B3681 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 42C574B825FA66FB008B3681 /* LaunchScreen.storyboard */; }; 42C574D525FA6CAC008B3681 /* AppSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C574D425FA6CAC008B3681 /* AppSwitcher.swift */; }; - 42C5BDD625A4CE4800E8FF40 /* AmericanExpress_UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C5BDD525A4CE4800E8FF40 /* AmericanExpress_UITests.swift */; }; 45DDDDA92C08FB0B00C262E5 /* PayPalMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 45DDDDA82C08FB0B00C262E5 /* PayPalMessages */; }; 57108A152832E789004EB870 /* PayPalNativeCheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57108A142832E789004EB870 /* PayPalNativeCheckoutViewController.swift */; }; 57108A172832EA04004EB870 /* BraintreePayPalNativeCheckout.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57108A162832EA04004EB870 /* BraintreePayPalNativeCheckout.framework */; }; @@ -141,7 +140,6 @@ 42C574B925FA66FB008B3681 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 42C574BB25FA66FB008B3681 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42C574D425FA6CAC008B3681 /* AppSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSwitcher.swift; sourceTree = ""; }; - 42C5BDD525A4CE4800E8FF40 /* AmericanExpress_UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmericanExpress_UITests.swift; sourceTree = ""; }; 42F3F6DB2603B83100401B0D /* CardinalMobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CardinalMobile.xcframework; path = ../Frameworks/CardinalMobile.xcframework; sourceTree = ""; }; 570B93D32853A6D30041BAFE /* BraintreeCoreSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeCoreSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 57108A142832E789004EB870 /* PayPalNativeCheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayPalNativeCheckoutViewController.swift; sourceTree = ""; }; @@ -272,14 +270,6 @@ path = MockVenmo; sourceTree = ""; }; - 42C5BDD425A4CE2E00E8FF40 /* American Express UI Tests */ = { - isa = PBXGroup; - children = ( - 42C5BDD525A4CE4800E8FF40 /* AmericanExpress_UITests.swift */, - ); - path = "American Express UI Tests"; - sourceTree = ""; - }; 80581A752553187800006F53 /* Venmo UI Tests */ = { isa = PBXGroup; children = ( @@ -414,7 +404,6 @@ A9B5ABDF24EB2A2200A4E1C8 /* UI Tests */ = { isa = PBXGroup; children = ( - 42C5BDD425A4CE2E00E8FF40 /* American Express UI Tests */, 42456E3B25474B620018374E /* Helpers */, A9B5ABE224EB2A2200A4E1C8 /* Info.plist */, BEF137E32B33818C00B9B225 /* PayPal Messaging UI Tests */, @@ -729,7 +718,6 @@ A9C4E07B24EC290F002F6FF2 /* PayPal_Vault_UITests.swift in Sources */, A9C4E07D24EC297F002F6FF2 /* ThreeDSecure_V2_UITests.swift in Sources */, 42456E3E25474B620018374E /* BTUITest.swift in Sources */, - 42C5BDD625A4CE4800E8FF40 /* AmericanExpress_UITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Demo/UI Tests/American Express UI Tests/AmericanExpress_UITests.swift b/Demo/UI Tests/American Express UI Tests/AmericanExpress_UITests.swift deleted file mode 100644 index 8fdb94d3cc..0000000000 --- a/Demo/UI Tests/American Express UI Tests/AmericanExpress_UITests.swift +++ /dev/null @@ -1,41 +0,0 @@ -import XCTest - -class AmericanExpress_UITests: XCTestCase { - - // swiftlint:disable:next implicitly_unwrapped_optional - var app: XCUIApplication! - - override func setUp() { - super.setUp() - continueAfterFailure = false - app = XCUIApplication() - app.launchArguments.append("-EnvironmentSandbox") - app.launchArguments.append("-UITestHardcodedClientToken") - app.launchArguments.append("-Integration:AmexViewController") - app.launch() - } - - func testValidCard_receivesRewardsBalance() { - waitForElementToBeHittable(app.buttons["Valid card"]) - app.buttons["Valid card"].tap() - sleep(2) - - XCTAssertTrue(app.buttons["45256433 Points, 316795.03 USD"].waitForExistence(timeout: 20)) - } - - func testInsufficientPointsCard_receivesErrorMessage() { - waitForElementToBeHittable(app.buttons["Insufficient points card"]) - app.buttons["Insufficient points card"].tap() - sleep(2) - - XCTAssertTrue(app.buttons["INQ2003: Not sufficient points in rewards account"].waitForExistence(timeout: 10)) - } - - func testIneligibleCard_receivesErrorMessage() { - waitForElementToBeHittable(app.buttons["Ineligible card"]) - app.buttons["Ineligible card"].tap() - sleep(2) - - XCTAssertTrue(app.buttons["INQ2002: Card is not eligible for the Program"].waitForExistence(timeout: 10)) - } -} diff --git a/Demo/UI Tests/PayPal Messaging UI Tests/PayPalMessaging_UITests.swift b/Demo/UI Tests/PayPal Messaging UI Tests/PayPalMessaging_UITests.swift index c4c78c4bab..36ebfdcdd6 100644 --- a/Demo/UI Tests/PayPal Messaging UI Tests/PayPalMessaging_UITests.swift +++ b/Demo/UI Tests/PayPal Messaging UI Tests/PayPalMessaging_UITests.swift @@ -19,9 +19,10 @@ final class PayPalMessaging_Success_UITests: XCTestCase { func testStart_withValidRequest_firesDelegates() { XCTAssertTrue(app.buttons["DELEGATE: didAppear fired"].waitForExistence(timeout: 30)) - let expectedButtonText = "PayPal - Pay monthly for purchases of $199-$10,000. Learn more" - waitForElementToBeHittable(app.buttons[expectedButtonText]) - app.buttons[expectedButtonText].tap() + let expectedButtonTextPredicate = NSPredicate(format: "label CONTAINS[c] 'Pay monthly for purchases of'") + let button = app.buttons.containing(expectedButtonTextPredicate) + waitForElementToBeHittable(button.element) + button.element.tap() sleep(2) app.buttons["PayPal learn more modal close"].tap() 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/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift b/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift new file mode 100644 index 0000000000..e3ab53db58 --- /dev/null +++ b/IntegrationTests/BraintreeAmexExpress_IntegrationTests.swift @@ -0,0 +1,31 @@ +import XCTest +@testable import BraintreeAmericanExpress +@testable import BraintreeCard +@testable import BraintreeCore + +class BraintreeAmexExpress_IntegrationTests: XCTestCase { + + func testGetRewardsBalance_returnsResult() async { + let apiClient = BTAPIClient(authorization: BTIntegrationTestsConstants.sandboxClientTokenVersion3)! + let cardClient = BTCardClient(apiClient: apiClient) + let amexClient = BTAmericanExpressClient(apiClient: apiClient) + + let card = BTCard() + card.number = "371260714673002" + card.expirationMonth = "12" + card.expirationYear = Helpers.shared.futureYear() + card.cvv = "1234" + + do { + let tokenizedCard = try await cardClient.tokenize(card) + let rewardsBalance = try await amexClient.getRewardsBalance(forNonce: tokenizedCard.nonce, currencyISOCode: "USD") + + XCTAssertEqual(rewardsBalance.rewardsAmount, "45256433") + XCTAssertEqual(rewardsBalance.rewardsUnit, "Points") + XCTAssertEqual(rewardsBalance.currencyAmount, "316795.03") + XCTAssertEqual(rewardsBalance.currencyIsoCode, "USD") + } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } + } +} 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 aaa8eb3a20..a058997d4d 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 0362ed1597..9d5ee0e8e5 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 43b3274951..d9ff3cc446 100644 --- a/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift +++ b/Sources/BraintreeThreeDSecure/BTThreeDSecureRequest.swift @@ -93,6 +93,10 @@ import BraintreeCore /// When using `BTThreeDSecureUIType.native`, all `BTThreeDSecureRenderType` options except `.html` must be set. public var renderTypes: [BTThreeDSecureRenderType]? + /// Optional. Three DS Requester APP URL Merchant app declaring their URL within the CReq message + /// so that the Authentication app can call the Merchant app after out of band authentication has occurred. + public var requestorAppURL: String? + /// A delegate for receiving information about the ThreeDSecure payment flow. public weak var threeDSecureRequestDelegate: BTThreeDSecureRequestDelegate? 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 b9d561e064..862a1c9ea9 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 f5fa291508..7193bb8758 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. @@ -69,7 +81,7 @@ import BraintreeCore public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) { linkType = request.fallbackToWeb ? .universal : .deeplink apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted, isVaultRequest: shouldVault, linkType: linkType) - let returnURLScheme = BTAppContextSwitcher.sharedInstance.returnURLScheme + let returnURLScheme = BTAppContextSwitcher.sharedInstance._returnURLScheme if returnURLScheme.isEmpty { NSLog( @@ -151,9 +163,10 @@ import BraintreeCore do { let appSwitchURL = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: returnURLScheme, paymentContextID: paymentContextID, metadata: metadata, + returnURLScheme: returnURLScheme, + universalLink: self.universalLink, forMerchantID: merchantProfileID, accessToken: configuration.venmoAccessToken, bundleDisplayName: bundleDisplayName, 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 da92e9bc00..fef916e212 100644 --- a/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift +++ b/UnitTests/BraintreeVenmoTests/BTVenmoAppSwitchRedirectURL_Tests.swift @@ -7,9 +7,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { func testUrlSchemeURL_whenAllValuesAreInitialized_returnsURLWithPaymentContextID() { do { let requestURL = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: "url-scheme", paymentContextID: "12345", metadata: BTClientMetadata(), + returnURLScheme: "url-scheme", + universalLink: nil, forMerchantID: "merchant-id", accessToken: "access-token", bundleDisplayName: "display-name", @@ -29,9 +30,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { func testAppSwitchURL_whenMerchantIDNil_throwsError() { do { _ = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: "url-scheme", paymentContextID: "12345", metadata: BTClientMetadata(), + returnURLScheme: "url-scheme", + universalLink: nil, forMerchantID: nil, accessToken: "access-token", bundleDisplayName: "display-name", @@ -47,9 +49,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { func testUniversalLinkURL_whenAllValuesInitialized_returnsURLWithAllValues() { do { let requestURL = try BTVenmoAppSwitchRedirectURL( - returnURLScheme: "url-scheme", paymentContextID: "12345", metadata: BTClientMetadata(), + returnURLScheme: nil, + universalLink: URL(string: "https://mywebsite.com/braintree-payments"), forMerchantID: "merchant-id", accessToken: "access-token", bundleDisplayName: "display-name", @@ -60,9 +63,9 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase { let components = URLComponents(string: requestURL.universalLinksURL()!.absoluteString) guard let queryItems = components?.queryItems else { XCTFail(); return } - XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "url-scheme://x-callback-url/vzero/auth/venmo/success"))) - XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "url-scheme://x-callback-url/vzero/auth/venmo/error"))) - XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "url-scheme://x-callback-url/vzero/auth/venmo/cancel"))) + XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "https://mywebsite.com/braintree-payments/success"))) + XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "https://mywebsite.com/braintree-payments/error"))) + XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "https://mywebsite.com/braintree-payments/cancel"))) XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-source", value: "display-name"))) XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_merchant_id", value: "merchant-id"))) XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_access_token", value: "access-token")))