From 258ae49bede22af809e229f9ba6486656955408c Mon Sep 17 00:00:00 2001 From: agedd <105314544+agedd@users.noreply.github.com> Date: Thu, 20 Feb 2025 13:17:40 -0600 Subject: [PATCH] Merge App Switch Checkout Feature Branch (#1519) * Update `BTPayPalRequest` with App Switch Properties (#1465) * add initial commits for updating `BTPayPalRequest` with app switch properties * add changelog.md entry * address pr feedback * cleanup * cleanup doc string * address pr feedback * add app-switch support for one-time-checkout * Revert "add app-switch support for one-time-checkout" This reverts commit 26cfe786220f8e5ce5215c17ad19b05fc3289630. * add app-switch demo app support for one-time checkout flow (#1471) * update parsing app switch url for 1-time checkout flow (#1500) * Launch PayPal App For Checkout Flow (#1504) * make changes to launch pp app for checkout flow * address failing unit test * address pr comments * address pr feedback * disable unit test * clean up and address pr feedback * clean up and re-add test/relevant errors * fix warning * address swift lint error * ECS5 App Switch Feature Bug Fixes (#1512) * add token param * update parsing logic * fix linkType bug * remove token parameter as we expect gw to pass this in redirectURL * pull in main changes * address pr feedback * fix spacing * code cleanup * more cleanup --- CHANGELOG.md | 4 ++ .../PayPalWebCheckoutViewController.swift | 48 +++++++++++-- .../BTPayPalApprovalURLParser.swift | 3 +- .../BTPayPalCheckoutRequest.swift | 61 ++++++++++++---- Sources/BraintreePayPal/BTPayPalClient.swift | 18 ++--- Sources/BraintreePayPal/BTPayPalError.swift | 7 ++ Sources/BraintreePayPal/BTPayPalRequest.swift | 44 +++++++++--- .../BTPayPalVaultBaseRequest.swift | 19 +++-- .../BTPayPalVaultRequest.swift | 42 +++-------- .../BTPayPalClient_Tests.swift | 69 ++++++++++++++++++- 10 files changed, 243 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16de52ede5..650b850a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Braintree iOS SDK Release Notes ## unreleased +* BraintreePayPal + * Add PayPal App Switch checkout flow (BETA) + * Add `BTPayPalCheckoutRequest(userAuthenticationEmail:enablePayPalAppSwitch:amount:intent:userAction:offerPayLater:currencyCode:requestBillingAgreement:)` + * **Note:** This feature is currently in beta and may change or be removed in future releases. * BraintreeApplePay * Add `BTApplePayCardNonce.isDeviceToken` for MPAN identification diff --git a/Demo/Application/Features/PayPalWebCheckoutViewController.swift b/Demo/Application/Features/PayPalWebCheckoutViewController.swift index 13b3de2bf5..abb0471c1d 100644 --- a/Demo/Application/Features/PayPalWebCheckoutViewController.swift +++ b/Demo/Application/Features/PayPalWebCheckoutViewController.swift @@ -89,20 +89,32 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { override func createPaymentButton() -> UIView { let payPalCheckoutButton = createButton(title: "PayPal Checkout", action: #selector(tappedPayPalCheckout)) + let payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(tappedPayPalVault)) - let payPalAppSwitchButton = createButton(title: "PayPal App Switch", action: #selector(tappedPayPalAppSwitch)) + + let payPalAppSwitchForCheckoutButton = createButton( + title: "PayPal App Switch - Checkout", + action: #selector(tappedPayPalAppSwitchForCheckout) + ) + + let payPalAppSwitchForVaultButton = createButton( + title: "PayPal App Switch - Vault", + action: #selector(tappedPayPalAppSwitchForVault) + ) let oneTimeCheckoutStackView = buttonsStackView(label: "1-Time Checkout", views: [ payLaterToggle, newPayPalCheckoutToggle, contactInformationToggle, - payPalCheckoutButton + payPalCheckoutButton, + payPalAppSwitchForCheckoutButton ]) oneTimeCheckoutStackView.spacing = 12 + let vaultStackView = buttonsStackView(label: "Vault", views: [ rbaDataToggle, payPalVaultButton, - payPalAppSwitchButton + payPalAppSwitchForVaultButton ]) vaultStackView.spacing = 12 @@ -231,8 +243,36 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController { self.completionBlock(nonce) } } + + @objc func tappedPayPalAppSwitchForCheckout(_ sender: UIButton) { + sender.setTitle("Processing...", for: .disabled) + sender.isEnabled = false + + guard let userEmail = emailTextField.text, !userEmail.isEmpty else { + self.progressBlock("Email cannot be nil for App Switch flow") + sender.isEnabled = true + return + } + + let request = BTPayPalCheckoutRequest( + userAuthenticationEmail: userEmail, + enablePayPalAppSwitch: true, + amount: "10.00" + ) + + payPalClient.tokenize(request) { nonce, error in + sender.isEnabled = true + + guard let nonce else { + self.progressBlock(error?.localizedDescription) + return + } + + self.completionBlock(nonce) + } + } - @objc func tappedPayPalAppSwitch(_ sender: UIButton) { + @objc func tappedPayPalAppSwitchForVault(_ sender: UIButton) { sender.setTitle("Processing...", for: .disabled) sender.isEnabled = false diff --git a/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift b/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift index de2766b5c0..41dd5c9260 100644 --- a/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift +++ b/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift @@ -51,7 +51,8 @@ struct BTPayPalApprovalURLParser { url = payPalAppRedirectURL } else if let approvalURL = body["paymentResource"]["redirectUrl"].asURL() ?? body["agreementSetup"]["approvalUrl"].asURL() { - redirectType = .webBrowser(url: approvalURL) + let launchPayPalApp = body["paymentResource"]["launchPayPalApp"].asBool() ?? false + redirectType = launchPayPalApp ? .payPalApp(url: approvalURL) : .webBrowser(url: approvalURL) url = approvalURL } else { return nil diff --git a/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift b/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift index 6ad55d200e..4a9d1c6011 100644 --- a/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalCheckoutRequest.swift @@ -61,7 +61,6 @@ import BraintreeCore // MARK: - Public Properties /// Used for a one-time payment. - /// /// Amount must be greater than or equal to zero, may optionally contain exactly 2 decimal places separated by '.' and is limited to 7 digits before the decimal point. public var amount: String @@ -75,23 +74,55 @@ import BraintreeCore public var offerPayLater: Bool /// Optional: A three-character ISO-4217 ISO currency code to use for the transaction. Defaults to merchant currency code if not set. - /// /// - Note: See https://developer.paypal.com/docs/api/reference/currency-codes/ for a list of supported currency codes. public var currencyCode: String? /// Optional: If set to `true`, this enables the Checkout with Vault flow, where the customer will be prompted to consent to a billing agreement during checkout. Defaults to `false`. public var requestBillingAgreement: Bool - /// Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. - public var userAuthenticationEmail: String? - /// Optional: Contact information of the recipient for the order public var contactInformation: BTContactInformation? /// Optional: Server side shipping callback URL to be notified when a customer updates their shipping address or options. A callback request will be sent to the merchant server at this URL. public var shippingCallbackURL: URL? + + // MARK: - Initializers - // MARK: - Initializer + /// Initializes a PayPal Checkout request for the PayPal App Switch flow + /// - Parameters: + /// - userAuthenticationEmail: Required: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. + /// - enablePayPalAppSwitch: Required: Used to determine if the customer will use the PayPal app switch flow. + /// - amount: Required: Used for a one-time payment. Amount must be greater than or equal to zero, may optionally contain exactly 2 decimal places separated by '.' and is limited to 7 digits before the decimal point. + /// - intent: Optional: Payment intent. Defaults to `.authorize`. Only applies to PayPal Checkout. + /// - userAction: Optional: Changes the call-to-action in the PayPal Checkout flow. Defaults to `.none`. + /// - offerPayLater: Optional: Offers PayPal Pay Later if the customer qualifies. Defaults to `false`. Only available with PayPal Checkout. + /// - currencyCode: Optional: A three-character ISO-4217 ISO currency code to use for the transaction. Defaults to merchant currency code if not set. + /// See https://developer.paypal.com/docs/api/reference/currency-codes/ for a list of supported currency codes. + /// - requestBillingAgreement: Optional: If set to `true`, this enables the Checkout with Vault flow, where the customer will be prompted to consent to a billing agreement + /// during checkout. Defaults to `false`. + /// - Warning: This initializer should be used for merchants using the PayPal App Switch flow. This feature is currently in beta and may change or be removed in future releases. + /// - Note: The PayPal App Switch flow currently only supports the production environment. + public convenience init( + userAuthenticationEmail: String, + enablePayPalAppSwitch: Bool, + amount: String, + intent: BTPayPalRequestIntent = .authorize, + userAction: BTPayPalRequestUserAction = .none, + offerPayLater: Bool = false, + currencyCode: String? = nil, + requestBillingAgreement: Bool = false + ) { + self.init( + amount: amount, + intent: intent, + userAction: userAction, + offerPayLater: offerPayLater, + currencyCode: currencyCode, + requestBillingAgreement: requestBillingAgreement, + userAuthenticationEmail: userAuthenticationEmail + ) + super.enablePayPalAppSwitch = enablePayPalAppSwitch + } /// Initializes a PayPal Native Checkout request /// - Parameters: @@ -106,6 +137,7 @@ import BraintreeCore /// during checkout. Defaults to `false`. /// - shippingCallbackURL: Optional: Server side shipping callback URL to be notified when a customer updates their shipping address or options. /// A callback request will be sent to the merchant server at this URL. + /// - userAuthenticationEmail: Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. public init( amount: String, intent: BTPayPalRequestIntent = .authorize, @@ -113,7 +145,8 @@ import BraintreeCore offerPayLater: Bool = false, currencyCode: String? = nil, requestBillingAgreement: Bool = false, - shippingCallbackURL: URL? = nil + shippingCallbackURL: URL? = nil, + userAuthenticationEmail: String? = nil ) { self.amount = amount self.intent = intent @@ -122,8 +155,12 @@ import BraintreeCore self.currencyCode = currencyCode self.requestBillingAgreement = requestBillingAgreement self.shippingCallbackURL = shippingCallbackURL - - super.init(hermesPath: "v1/paypal_hermes/create_payment_resource", paymentType: .checkout) + + super.init( + hermesPath: "v1/paypal_hermes/create_payment_resource", + paymentType: .checkout, + userAuthenticationEmail: userAuthenticationEmail + ) } // MARK: Public Methods @@ -135,7 +172,7 @@ import BraintreeCore universalLink: URL? = nil, isPayPalAppInstalled: Bool = false ) -> [String: Any] { - var baseParameters = super.parameters(with: configuration) + var baseParameters = super.parameters(with: configuration, universalLink: universalLink, isPayPalAppInstalled: isPayPalAppInstalled) var checkoutParameters: [String: Any] = [ "intent": intent.stringValue, "amount": amount, @@ -147,10 +184,6 @@ import BraintreeCore if currencyCode != nil { checkoutParameters["currency_iso_code"] = currencyCode } - - if let userAuthenticationEmail, !userAuthenticationEmail.isEmpty { - checkoutParameters["payer_email"] = userAuthenticationEmail - } if userAction != .none, var experienceProfile = baseParameters["experience_profile"] as? [String: Any] { experienceProfile["user_action"] = userAction.stringValue diff --git a/Sources/BraintreePayPal/BTPayPalClient.swift b/Sources/BraintreePayPal/BTPayPalClient.swift index 3487e1166a..4c25776bae 100644 --- a/Sources/BraintreePayPal/BTPayPalClient.swift +++ b/Sources/BraintreePayPal/BTPayPalClient.swift @@ -336,7 +336,7 @@ import BraintreeDataCollector request: BTPayPalRequest, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void ) { - linkType = (request as? BTPayPalVaultRequest)?.enablePayPalAppSwitch == true ? .universal : .deeplink + linkType = request.enablePayPalAppSwitch == true ? .universal : .deeplink self.payPalRequest = request apiClient.sendAnalyticsEvent( @@ -396,12 +396,14 @@ import BraintreeDataCollector switch approvalURL.redirectType { case .payPalApp(let url): - guard let baToken = approvalURL.baToken else { - self.notifyFailure(with: BTPayPalError.missingBAToken, completion: completion) + guard (self.isVaultRequest ? approvalURL.baToken : approvalURL.ecToken) != nil else { + self.notifyFailure( + with: self.isVaultRequest ? BTPayPalError.missingBAToken : BTPayPalError.missingECToken, + completion: completion + ) return } - - self.launchPayPalApp(with: url, baToken: baToken, completion: completion) + self.launchPayPalApp(with: url, completion: completion) case .webBrowser(let url): self.handlePayPalRequest(with: url, paymentType: request.paymentType, completion: completion) } @@ -411,7 +413,6 @@ import BraintreeDataCollector private func launchPayPalApp( with payPalAppRedirectURL: URL, - baToken: String, completion: @escaping (BTPayPalAccountNonce?, Error?) -> Void ) { apiClient.sendAnalyticsEvent( @@ -423,12 +424,13 @@ import BraintreeDataCollector ) var urlComponents = URLComponents(url: payPalAppRedirectURL, resolvingAgainstBaseURL: true) - urlComponents?.queryItems = [ - URLQueryItem(name: "ba_token", value: baToken), + let additionalQueryItems: [URLQueryItem] = [ URLQueryItem(name: "source", value: "braintree_sdk"), URLQueryItem(name: "switch_initiated_time", value: String(Int(round(Date().timeIntervalSince1970 * 1000)))) ] + urlComponents?.queryItems?.append(contentsOf: additionalQueryItems) + guard let redirectURL = urlComponents?.url else { self.notifyFailure(with: BTPayPalError.invalidURL("Unable to construct PayPal app redirect URL."), completion: completion) return diff --git a/Sources/BraintreePayPal/BTPayPalError.swift b/Sources/BraintreePayPal/BTPayPalError.swift index 3a947c3dd2..28d4a4d982 100644 --- a/Sources/BraintreePayPal/BTPayPalError.swift +++ b/Sources/BraintreePayPal/BTPayPalError.swift @@ -44,6 +44,9 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable { /// 13. Missing PayPal Request case missingPayPalRequest + + /// 14. Missing EC Token for App Switch + case missingECToken public static var errorDomain: String { "com.braintreepayments.BTPayPalErrorDomain" @@ -79,6 +82,8 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable { return 12 case .missingPayPalRequest: return 13 + case .missingECToken: + return 14 } } @@ -114,6 +119,8 @@ public enum BTPayPalError: Error, CustomNSError, LocalizedError, Equatable { return "Missing BA Token for PayPal App Switch." case .missingPayPalRequest: return "The PayPal Request was missing or invalid." + case .missingECToken: + return "Missing EC Token for PayPal App Switch." } } diff --git a/Sources/BraintreePayPal/BTPayPalRequest.swift b/Sources/BraintreePayPal/BTPayPalRequest.swift index b4cd2cca12..c73a655656 100644 --- a/Sources/BraintreePayPal/BTPayPalRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalRequest.swift @@ -5,7 +5,6 @@ import BraintreeCore #endif @objc public enum BTPayPalPaymentType: Int { - /// Checkout case checkout @@ -24,7 +23,6 @@ import BraintreeCore /// Use this option to specify the PayPal page to display when a user lands on the PayPal site to complete the payment. @objc public enum BTPayPalRequestLandingPageType: Int { - /// Default case none // Obj-C enums cannot be nil; this default option is used to make `landingPageType` optional for merchants @@ -96,14 +94,23 @@ import BraintreeCore /// :nodoc: Exposed publicly for use by PayPal Native Checkout module. This property is not covered by semantic versioning. @_documentation(visibility: private) public var paymentType: BTPayPalPaymentType - + /// Optional: A user's phone number to initiate a quicker authentication flow in the scenario where the user has a PayPal account /// identified with the same phone number. public var userPhoneNumber: BTPayPalPhoneNumber? - + + /// Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. + public var userAuthenticationEmail: String? + /// Optional: The shopper session ID returned from your shopper insights server SDK integration. public var shopperSessionID: String? + // MARK: - Internal Properties + + /// Optional: Used to determine if the customer will use the PayPal app switch flow. Defaults to `false`. + /// - Warning: This property is currently in beta and may change or be removed in future releases. + var enablePayPalAppSwitch: Bool + // MARK: - Static Properties static let callbackURLHostAndPath: String = "onetouch/v1/" @@ -124,6 +131,8 @@ import BraintreeCore billingAgreementDescription: String? = nil, riskCorrelationId: String? = nil, userPhoneNumber: BTPayPalPhoneNumber? = nil, + userAuthenticationEmail: String? = nil, + enablePayPalAppSwitch: Bool = false, shopperSessionID: String? = nil ) { self.hermesPath = hermesPath @@ -139,6 +148,8 @@ import BraintreeCore self.billingAgreementDescription = billingAgreementDescription self.riskCorrelationID = riskCorrelationId self.userPhoneNumber = userPhoneNumber + self.userAuthenticationEmail = userAuthenticationEmail + self.enablePayPalAppSwitch = enablePayPalAppSwitch self.shopperSessionID = shopperSessionID } @@ -180,11 +191,15 @@ import BraintreeCore let lineItemsArray = lineItems.compactMap { $0.requestParameters() } parameters["line_items"] = lineItemsArray } - - if let userPhoneNumberDict = try? userPhoneNumber?.toDictionary() { - parameters["phone_number"] = userPhoneNumberDict + + if let userPhoneNumberDictionary = try? userPhoneNumber?.toDictionary() { + parameters["phone_number"] = userPhoneNumberDictionary } - + + if let userAuthenticationEmail, !userAuthenticationEmail.isEmpty { + parameters["payer_email"] = userAuthenticationEmail + } + if let shopperSessionID { parameters["shopper_session_id"] = shopperSessionID } @@ -192,7 +207,18 @@ import BraintreeCore parameters["return_url"] = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)success" parameters["cancel_url"] = BTCoreConstants.callbackURLScheme + "://\(BTPayPalRequest.callbackURLHostAndPath)cancel" parameters["experience_profile"] = experienceProfile - + + if let universalLink, enablePayPalAppSwitch, isPayPalAppInstalled { + let appSwitchParameters: [String: Any] = [ + "launch_paypal_app": enablePayPalAppSwitch, + "os_version": UIDevice.current.systemVersion, + "os_type": UIDevice.current.systemName, + "merchant_app_return_url": universalLink.absoluteString + ] + + return parameters.merging(appSwitchParameters) { $1 } + } + return parameters } } diff --git a/Sources/BraintreePayPal/BTPayPalVaultBaseRequest.swift b/Sources/BraintreePayPal/BTPayPalVaultBaseRequest.swift index 7db4ac2d38..9050f667f2 100644 --- a/Sources/BraintreePayPal/BTPayPalVaultBaseRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalVaultBaseRequest.swift @@ -17,10 +17,21 @@ import BraintreeCore /// Initializes a PayPal Native Vault request /// - Parameters: /// - offerCredit: Optional: Offers PayPal Credit if the customer qualifies. Defaults to `false`. - public init(offerCredit: Bool = false) { + /// - userAuthenticationEmail: Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. + /// - enablePayPalAppSwitch: Optional: Used to determine if the customer will use the PayPal app switch flow. Defaults to `false`. + public init( + offerCredit: Bool = false, + userAuthenticationEmail: String? = nil, + enablePayPalAppSwitch: Bool = false + ) { self.offerCredit = offerCredit - - super.init(hermesPath: "v1/paypal_hermes/setup_billing_agreement", paymentType: .vault) + + super.init( + hermesPath: "v1/paypal_hermes/setup_billing_agreement", + paymentType: .vault, + userAuthenticationEmail: userAuthenticationEmail, + enablePayPalAppSwitch: enablePayPalAppSwitch + ) } // MARK: Public Methods @@ -32,7 +43,7 @@ import BraintreeCore universalLink: URL? = nil, isPayPalAppInstalled: Bool = false ) -> [String: Any] { - let baseParameters = super.parameters(with: configuration) + let baseParameters = super.parameters(with: configuration, universalLink: universalLink, isPayPalAppInstalled: isPayPalAppInstalled) var vaultParameters: [String: Any] = ["offer_paypal_credit": offerCredit] if let billingAgreementDescription { diff --git a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift index 422c38fa56..a796a2b617 100644 --- a/Sources/BraintreePayPal/BTPayPalVaultRequest.swift +++ b/Sources/BraintreePayPal/BTPayPalVaultRequest.swift @@ -6,18 +6,8 @@ import BraintreeCore /// Options for the PayPal Vault flow. @objcMembers public class BTPayPalVaultRequest: BTPayPalVaultBaseRequest { - - // MARK: - Public Properties - - /// Optional: User email to initiate a quicker authentication flow in cases where the user has a PayPal Account with the same email. - public var userAuthenticationEmail: String? - + // MARK: - Internal Properties - - /// Optional: Used to determine if the customer will use the PayPal app switch flow. - /// Defaults to `false`. - /// - Warning: This property is currently in beta and may change or be removed in future releases. - var enablePayPalAppSwitch: Bool = false /// Optional: Recurring billing plan type, or charge pattern. var recurringBillingPlanType: BTPayPalRecurringBillingPlanType? @@ -39,8 +29,11 @@ import BraintreeCore enablePayPalAppSwitch: Bool, offerCredit: Bool = false ) { - self.init(offerCredit: offerCredit, userAuthenticationEmail: userAuthenticationEmail) - self.enablePayPalAppSwitch = enablePayPalAppSwitch + self.init( + offerCredit: offerCredit, + userAuthenticationEmail: userAuthenticationEmail + ) + super.enablePayPalAppSwitch = enablePayPalAppSwitch } /// Initializes a PayPal Vault request @@ -57,8 +50,10 @@ import BraintreeCore ) { self.recurringBillingDetails = recurringBillingDetails self.recurringBillingPlanType = recurringBillingPlanType - self.userAuthenticationEmail = userAuthenticationEmail - super.init(offerCredit: offerCredit) + super.init( + offerCredit: offerCredit, + userAuthenticationEmail: userAuthenticationEmail + ) } public override func parameters( @@ -66,22 +61,7 @@ import BraintreeCore universalLink: URL? = nil, isPayPalAppInstalled: Bool = false ) -> [String: Any] { - var baseParameters = super.parameters(with: configuration) - - if let userAuthenticationEmail, !userAuthenticationEmail.isEmpty { - baseParameters["payer_email"] = userAuthenticationEmail - } - - if let universalLink, enablePayPalAppSwitch, isPayPalAppInstalled { - let appSwitchParameters: [String: Any] = [ - "launch_paypal_app": enablePayPalAppSwitch, - "os_version": UIDevice.current.systemVersion, - "os_type": UIDevice.current.systemName, - "merchant_app_return_url": universalLink.absoluteString - ] - - return baseParameters.merging(appSwitchParameters) { $1 } - } + var baseParameters = super.parameters(with: configuration, universalLink: universalLink, isPayPalAppInstalled: isPayPalAppInstalled) if let recurringBillingPlanType { baseParameters["plan_type"] = recurringBillingPlanType.rawValue diff --git a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift index 8b6403295c..8696cdd39d 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift @@ -745,7 +745,7 @@ class BTPayPalClient_Tests: XCTestCase { XCTAssertNil(BTPayPalClient.payPalClient) } - // MARK: - App Switch - tokenize + // MARK: - App Switch - Tokenize func testTokenizeVaultAccount_whenPayPalAppApprovalURLPresent_attemptsAppSwitchWithParameters() async { let fakeApplication = FakeApplication() @@ -858,6 +858,73 @@ class BTPayPalClient_Tests: XCTestCase { waitForExpectations(timeout: 1) } + + func testTokenizeCheckoutAccount_whenPayPalAppApprovalURLPresent_attemptsAppSwitchWithParameters() async { + let fakeApplication = FakeApplication() + payPalClient.application = fakeApplication + + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "https://www.some-url.com/some-path?token=value1", + "launchPayPalApp": true + ] + ]) + + let checkoutRequest = BTPayPalCheckoutRequest( + userAuthenticationEmail: "fake-pp@gmail.com", + enablePayPalAppSwitch: true, + amount: "10.00" + ) + payPalClient.tokenize(checkoutRequest) { _, _ in } + + XCTAssertTrue(fakeApplication.openURLWasCalled) + + let urlComponents = URLComponents(url: fakeApplication.lastOpenURL!, resolvingAgainstBaseURL: true) + XCTAssertEqual(urlComponents?.host, "www.some-url.com") + XCTAssertEqual(urlComponents?.path, "/some-path") + + XCTAssertEqual(urlComponents?.queryItems?[0].name, "token") + XCTAssertEqual(urlComponents?.queryItems?[0].value, "value1") + XCTAssertEqual(urlComponents?.queryItems?[1].name, "source") + XCTAssertEqual(urlComponents?.queryItems?[1].value, "braintree_sdk") + XCTAssertEqual(urlComponents?.queryItems?[2].name, "switch_initiated_time") + if let urlTimestamp = urlComponents?.queryItems?[2].value { + XCTAssertNotNil(urlTimestamp) + } else { + XCTFail("Expected integer value for query param `switch_initiated_time`") + } + } + + func testTokenizeCheckoutAccount_whenPayPalAppApprovalURLMissingECToken_returnsError() { + let fakeApplication = FakeApplication() + payPalClient.application = fakeApplication + + mockAPIClient.cannedResponseBody = BTJSON(value: [ + "paymentResource": [ + "redirectUrl": "https://www.some-url.com/some-path", + "launchPayPalApp": true + ] + ]) + + let checkoutRequest = BTPayPalCheckoutRequest( + userAuthenticationEmail: "fake-pp@gmail.com", + enablePayPalAppSwitch: true, + amount: "10.00" + ) + + let expectation = expectation(description: "completion block called") + payPalClient.tokenize(checkoutRequest) { nonce, error in + XCTAssertNil(nonce) + + guard let error = error as NSError? else { XCTFail(); return } + XCTAssertEqual(error.code, 14) + XCTAssertEqual(error.localizedDescription, "Missing EC Token for PayPal App Switch.") + XCTAssertEqual(error.domain, "com.braintreepayments.BTPayPalErrorDomain") + expectation.fulfill() + } + + waitForExpectations(timeout: 1) + } func testHandleReturn_whenURLIsUnknown_returnsError() { let request = BTPayPalVaultRequest(