From ddf266e00b00a38f4233f254f8274c088f6a33ea Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 10 Apr 2024 15:17:11 -0500 Subject: [PATCH 1/8] updates to move ReturnURL logic to one place --- Braintree.xcodeproj/project.pbxproj | 16 +-- .../BTPayPalAppSwitchReturnURL.swift | 41 ------- Sources/BraintreePayPal/BTPayPalClient.swift | 108 ++++++------------ .../BraintreePayPal/BTPayPalReturnURL.swift | 86 ++++++++++++++ .../BTPayPalAppSwitchReturnURL_Tests.swift | 25 ---- .../BTPayPalClient_Tests.swift | 9 +- .../BTPayPalReturnURL_Tests.swift | 27 +++++ 7 files changed, 166 insertions(+), 146 deletions(-) delete mode 100644 Sources/BraintreePayPal/BTPayPalAppSwitchReturnURL.swift create mode 100644 Sources/BraintreePayPal/BTPayPalReturnURL.swift delete mode 100644 UnitTests/BraintreePayPalTests/BTPayPalAppSwitchReturnURL_Tests.swift create mode 100644 UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift diff --git a/Braintree.xcodeproj/project.pbxproj b/Braintree.xcodeproj/project.pbxproj index 1e8a2c24da..0d80a80971 100644 --- a/Braintree.xcodeproj/project.pbxproj +++ b/Braintree.xcodeproj/project.pbxproj @@ -230,7 +230,7 @@ BE698EA428AD2C10001D9B10 /* BTCoreConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE698EA328AD2C10001D9B10 /* BTCoreConstants.swift */; }; BE698EA628B3CDAD001D9B10 /* BTCacheDateValidator_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE698EA528B3CDAD001D9B10 /* BTCacheDateValidator_Tests.swift */; }; BE6BC22C2BA9C67600C3E321 /* BTPayPalVaultBaseRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6BC22B2BA9C67600C3E321 /* BTPayPalVaultBaseRequest.swift */; }; - BE6BC22E2BA9CFFC00C3E321 /* BTPayPalAppSwitchReturnURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6BC22D2BA9CFFC00C3E321 /* BTPayPalAppSwitchReturnURL.swift */; }; + BE6BC22E2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */; }; BE70A963284FA3F000F6D3F7 /* BTDataCollectorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE70A962284FA3F000F6D3F7 /* BTDataCollectorError.swift */; }; BE70A965284FA9DE00F6D3F7 /* MockBTDataCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE70A964284FA9DE00F6D3F7 /* MockBTDataCollector.swift */; }; BE70A983284FC07C00F6D3F7 /* BraintreeDataCollector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A76D7C001BB1CAB00000FA6A /* BraintreeDataCollector.framework */; }; @@ -265,7 +265,7 @@ BE9FB82B2898324C00D6FE2F /* BTPaymentMethodNonce.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9FB82A2898324C00D6FE2F /* BTPaymentMethodNonce.swift */; }; BE9FB82D28984ADE00D6FE2F /* BTPaymentMethodNonceParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9FB82C28984ADE00D6FE2F /* BTPaymentMethodNonceParser.swift */; }; BEB9BF532A26872B00A3673E /* BTWebAuthenticationSessionClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEB9BF522A26872B00A3673E /* BTWebAuthenticationSessionClient.swift */; }; - BEBA590F2BB1B5B9005FA8A2 /* BTPayPalAppSwitchReturnURL_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA590E2BB1B5B9005FA8A2 /* BTPayPalAppSwitchReturnURL_Tests.swift */; }; + BEBA590F2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEBA590E2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift */; }; BEBC222728D25BB400D83186 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80DBE69423A931A600373230 /* Helpers.swift */; }; BEBC6E4B29258FD4004E25A0 /* BraintreeCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 570B93AC285397520041BAFE /* BraintreeCore.framework */; }; BEBC6E5E2927CF59004E25A0 /* Braintree.h in Headers */ = {isa = PBXBuildFile; fileRef = BEBC6E5D2927CF59004E25A0 /* Braintree.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -870,7 +870,7 @@ BE698EA528B3CDAD001D9B10 /* BTCacheDateValidator_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCacheDateValidator_Tests.swift; sourceTree = ""; }; BE698EAA28B50F41001D9B10 /* BTClientToken_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTClientToken_Tests.swift; sourceTree = ""; }; BE6BC22B2BA9C67600C3E321 /* BTPayPalVaultBaseRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalVaultBaseRequest.swift; sourceTree = ""; }; - BE6BC22D2BA9CFFC00C3E321 /* BTPayPalAppSwitchReturnURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalAppSwitchReturnURL.swift; sourceTree = ""; }; + BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalReturnURL.swift; sourceTree = ""; }; BE70A962284FA3F000F6D3F7 /* BTDataCollectorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTDataCollectorError.swift; sourceTree = ""; }; BE70A964284FA9DE00F6D3F7 /* MockBTDataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBTDataCollector.swift; sourceTree = ""; }; BE7A9643299FC5DE009AB920 /* BTConfiguration+ApplePay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BTConfiguration+ApplePay.swift"; sourceTree = ""; }; @@ -902,7 +902,7 @@ BE9FB82A2898324C00D6FE2F /* BTPaymentMethodNonce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPaymentMethodNonce.swift; sourceTree = ""; }; BE9FB82C28984ADE00D6FE2F /* BTPaymentMethodNonceParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPaymentMethodNonceParser.swift; sourceTree = ""; }; BEB9BF522A26872B00A3673E /* BTWebAuthenticationSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTWebAuthenticationSessionClient.swift; sourceTree = ""; }; - BEBA590E2BB1B5B9005FA8A2 /* BTPayPalAppSwitchReturnURL_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalAppSwitchReturnURL_Tests.swift; sourceTree = ""; }; + BEBA590E2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalReturnURL_Tests.swift; sourceTree = ""; }; BEBC6E5D2927CF59004E25A0 /* Braintree.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Braintree.h; sourceTree = ""; }; BEBC6F252937A510004E25A0 /* BTClientMetadata_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTClientMetadata_Tests.swift; sourceTree = ""; }; BEBC6F272937BD1F004E25A0 /* BTGraphQLHTTP_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTGraphQLHTTP_Tests.swift; sourceTree = ""; }; @@ -1197,7 +1197,7 @@ 57544F572952298900DEB7B0 /* BTPayPalAccountNonce.swift */, 3B7A261029C0CAA40087059D /* BTPayPalAnalytics.swift */, 8014221B2BAE935B009F9999 /* BTPayPalApprovalURLParser.swift */, - BE6BC22D2BA9CFFC00C3E321 /* BTPayPalAppSwitchReturnURL.swift */, + BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */, BE8E5CEE294B6937001BF017 /* BTPayPalCheckoutRequest.swift */, 57544F5929524E4D00DEB7B0 /* BTPayPalClient.swift */, 5754481F294A2EBE00DEB7B0 /* BTPayPalCreditFinancing.swift */, @@ -1713,13 +1713,13 @@ A95229C624FD949D006F7D25 /* BTConfiguration+PayPal_Tests.swift */, BEDEAF102AC1D049004EA970 /* BTPayPalAccountNonce_Tests.swift */, 3B7A261229C35B670087059D /* BTPayPalAnalytics_Tests.swift */, + BEBA590E2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift */, 42FC237025CE0E110047C49A /* BTPayPalCheckoutRequest_Tests.swift */, 427F32DF25D1D62D00435294 /* BTPayPalClient_Tests.swift */, BECB10C52B5999EE008D398E /* BTPayPalLineItem_Tests.swift */, 42FC218A25CDE0290047C49A /* BTPayPalRequest_Tests.swift */, 427F328F25D1A7B900435294 /* BTPayPalVaultRequest_Tests.swift */, A9E5C1E424FD665D00EE691F /* Info.plist */, - BEBA590E2BB1B5B9005FA8A2 /* BTPayPalAppSwitchReturnURL_Tests.swift */, ); path = BraintreePayPalTests; sourceTree = ""; @@ -2794,7 +2794,7 @@ BE349113294B798300D2CF68 /* BTPayPalRequest.swift in Sources */, 57544F5C295254A500DEB7B0 /* BTJSON+PayPal.swift in Sources */, 3B7A261129C0CAA40087059D /* BTPayPalAnalytics.swift in Sources */, - BE6BC22E2BA9CFFC00C3E321 /* BTPayPalAppSwitchReturnURL.swift in Sources */, + BE6BC22E2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift in Sources */, BE8E5CEF294B6937001BF017 /* BTPayPalCheckoutRequest.swift in Sources */, 5754481E294A2A1D00DEB7B0 /* BTPayPalCreditFinancingAmount.swift in Sources */, 57D9436E2968A8080079EAB1 /* BTPayPalLocaleCode.swift in Sources */, @@ -3118,7 +3118,7 @@ BECB10C62B5999EE008D398E /* BTPayPalLineItem_Tests.swift in Sources */, 3B7A261429C35BD00087059D /* BTPayPalAnalytics_Tests.swift in Sources */, A95229C724FD949D006F7D25 /* BTConfiguration+PayPal_Tests.swift in Sources */, - BEBA590F2BB1B5B9005FA8A2 /* BTPayPalAppSwitchReturnURL_Tests.swift in Sources */, + BEBA590F2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/BraintreePayPal/BTPayPalAppSwitchReturnURL.swift b/Sources/BraintreePayPal/BTPayPalAppSwitchReturnURL.swift deleted file mode 100644 index c6a3064bd0..0000000000 --- a/Sources/BraintreePayPal/BTPayPalAppSwitchReturnURL.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Foundation - -#if canImport(BraintreeCore) -import BraintreeCore -#endif - -enum BTPayPalAppSwitchReturnURLState { - case unknownPath - case succeeded - case canceled -} - -/// This class interprets URLs received from the PayPal app via app switch returns. -/// -/// PayPal app switch authorization requests should result in success or user-initiated cancelation. These states are communicated in the url. -struct BTPayPalAppSwitchReturnURL { - - /// The overall status of the app switch - success, cancelation, or an unknown path - var state: BTPayPalAppSwitchReturnURLState = .unknownPath - - /// Initializes a new `BTPayPalAppSwitchReturnURL` - /// - Parameter url: an incoming app switch url - init?(url: URL) { - if url.path.contains("success") { - state = .succeeded - } else if url.path.contains("cancel") { - state = .canceled - } else { - state = .unknownPath - } - } - - // MARK: - Static Methods - - /// Evaluates whether the url represents a valid PayPal return URL. - /// - Parameter url: an app switch return URL - /// - Returns: `true` if the url represents a valid PayPal app switch return - static func isValid(_ url: URL) -> Bool { - url.scheme == "https" && (url.path.contains("cancel") || url.path.contains("success")) - } -} diff --git a/Sources/BraintreePayPal/BTPayPalClient.swift b/Sources/BraintreePayPal/BTPayPalClient.swift index 6fb30ecc36..1246e83bae 100644 --- a/Sources/BraintreePayPal/BTPayPalClient.swift +++ b/Sources/BraintreePayPal/BTPayPalClient.swift @@ -48,6 +48,9 @@ import BraintreeDataCollector // MARK: - Private Properties + /// URL Scheme for PayPal In-App Checkout + private let payPalInAppScheme: String = "paypal-in-app-checkout://" + /// Indicates if the user returned back to the merchant app from the `BTWebAuthenticationSession` /// Will only be `true` if the user proceed through the `UIAlertController` private var webSessionReturned: Bool = false @@ -59,9 +62,6 @@ import BraintreeDataCollector /// Used for sending the type of flow, universal vs deeplink to FPTI private var linkType: String? = nil - /// URL Scheme for PayPal In-App Checkout - private let payPalInAppScheme: String = "paypal-in-app-checkout://" - // MARK: - Initializer /// Initialize a new PayPal client instance. @@ -182,17 +182,30 @@ import BraintreeDataCollector payPalContextID: payPalContextID, payPalInstalled: payPalAppInstalled ) - guard let url, isValidURLAction(url: url) else { + + guard let url, BTPayPalReturnURL.isValidURLAction(url: url, linkType: linkType) else { notifyFailure(with: BTPayPalError.invalidURLAction, completion: completion) return } - guard let response = responseDictionary(from: url) else { + guard !url.absoluteString.contains("cancel") else { notifyCancel(completion: completion) return } - - var account: [String: Any] = response + + let clientDictionary: [String: String] = [ + "platform": "iOS", + "product_name": "PayPal", + "paypal_sdk_version": "version" + ] + + let responseDictionary: [String: String] = ["webURL": url.absoluteString] + + var account: [String: Any] = [ + "client": clientDictionary, + "response": responseDictionary, + "response_type": "web" + ] if paymentType == .checkout { account["options"] = ["validate": false] @@ -256,14 +269,16 @@ import BraintreeDataCollector // MARK: - App Switch Methods func handleReturnURL(_ url: URL) { - guard let returnURL = BTPayPalAppSwitchReturnURL(url: url) else { + guard let returnURL = BTPayPalReturnURL(.payPalApp(url: url)) else { notifyFailure(with: BTPayPalError.invalidURL("App Switch return URL cannot be nil"), completion: appSwitchCompletion) return } switch returnURL.state { - case .succeeded, .canceled: + case .succeeded: handleReturn(url, paymentType: .vault, completion: appSwitchCompletion) + case .canceled: + notifyCancel(completion: appSwitchCompletion) case .unknownPath: notifyFailure(with: BTPayPalError.appSwitchReturnURLPathInvalid, completion: appSwitchCompletion) } @@ -409,7 +424,19 @@ import BraintreeDataCollector return } - handleReturn(url, paymentType: paymentType, completion: completion) + guard let url, let returnURL = BTPayPalReturnURL(.webBrowser(url: url)) else { + notifyFailure(with: BTPayPalError.invalidURL("ASWebAuthenticationSession return URL cannot be nil"), completion: completion) + return + } + + switch returnURL.state { + case .succeeded: + handleReturn(url, paymentType: .vault, completion: completion) + case .canceled: + notifyCancel(completion: completion) + case .unknownPath: + notifyFailure(with: BTPayPalError.asWebAuthenticationSessionURLInvalid(url.absoluteString), completion: completion) + } } sessionDidAppear: { [self] didAppear in if didAppear { apiClient.sendAnalyticsEvent( @@ -443,65 +470,6 @@ import BraintreeDataCollector return } } - - private func isValidURLAction(url: URL) -> Bool { - guard let host = url.host, let scheme = url.scheme, !scheme.isEmpty else { - return false - } - - var hostAndPath = host - .appending(url.path) - .components(separatedBy: "/") - .dropLast(1) // remove the action (`success`, `cancel`, etc) - .joined(separator: "/") - - if hostAndPath.count > 0 { - hostAndPath.append("/") - } - - if hostAndPath != BTPayPalRequest.callbackURLHostAndPath && (payPalRequest as? BTPayPalVaultRequest)?.universalLink == nil { - return false - } - - guard let action = action(from: url), - let query = url.query, - query.count > 0, - action.count >= 0, - ["success", "cancel", "authenticate"].contains(action) else { - return false - } - - return true - } - - private func responseDictionary(from url: URL) -> [String : Any]? { - if let action = action(from: url), action == "cancel" { - return nil - } - - let clientDictionary: [String: String] = [ - "platform": "iOS", - "product_name": "PayPal", - "paypal_sdk_version": "version" - ] - - let responseDictionary: [String: String] = ["webURL": url.absoluteString] - - return [ - "client": clientDictionary, - "response": responseDictionary, - "response_type": "web" - ] - } - - private func action(from url: URL) -> String? { - guard let action = url.lastPathComponent.components(separatedBy: "?").first, - !action.isEmpty else { - return url.host - } - - return action - } // MARK: - Analytics Helper Methods @@ -554,6 +522,6 @@ extension BTPayPalClient: BTAppContextSwitchClient { /// :nodoc: @_documentation(visibility: private) @objc public static func canHandleReturnURL(_ url: URL) -> Bool { - BTPayPalAppSwitchReturnURL.isValid(url) + BTPayPalReturnURL.isValid(url) } } diff --git a/Sources/BraintreePayPal/BTPayPalReturnURL.swift b/Sources/BraintreePayPal/BTPayPalReturnURL.swift new file mode 100644 index 0000000000..39033ed0aa --- /dev/null +++ b/Sources/BraintreePayPal/BTPayPalReturnURL.swift @@ -0,0 +1,86 @@ +import Foundation + +#if canImport(BraintreeCore) +import BraintreeCore +#endif + +enum BTPayPalAppSwitchReturnURLState { + case unknownPath + case succeeded + case canceled +} + +/// This class interprets URLs received from the PayPal app via app switch returns. +/// +/// PayPal app switch authorization requests should result in success or user-initiated cancelation. These states are communicated in the url. +struct BTPayPalReturnURL { + + /// The overall status of the app switch - success, cancelation, or an unknown path + var state: BTPayPalAppSwitchReturnURLState = .unknownPath + + /// Initializes a new `BTPayPalAppSwitchReturnURL` + /// - Parameter url: an incoming app switch url + init?(_ redirectType: PayPalRedirectType) { + switch redirectType { + case .payPalApp(let url), .webBrowser(let url): + if url.path.contains("success") { + state = .succeeded + } else if url.path.contains("cancel") { + state = .canceled + } else { + state = .unknownPath + } + } + } + + // MARK: - Static Methods + + /// Evaluates whether the url represents a valid PayPal return URL. + /// - Parameter url: an app switch return URL + /// - Returns: `true` if the url represents a valid PayPal app switch return + static func isValid(_ url: URL) -> Bool { + url.scheme == "https" && (url.path.contains("cancel") || url.path.contains("success")) + } + + static func isValidURLAction(url: URL, linkType: String?) -> Bool { + guard let host = url.host, let scheme = url.scheme, !scheme.isEmpty else { + return false + } + + var hostAndPath = host + .appending(url.path) + .components(separatedBy: "/") + .dropLast(1) // remove the action (`success`, `cancel`, etc) + .joined(separator: "/") + + if hostAndPath.count > 0 { + hostAndPath.append("/") + } + + /// If we are using the deeplink/ASWeb based PayPal flow we want to check that the host and path matches + /// the static callbackURLHostAndPath. For the universal link flow we do not care about this check. + if hostAndPath != BTPayPalRequest.callbackURLHostAndPath, linkType == "deeplink" { + return false + } + + guard let action = action(from: url), + let query = url.query, + query.count > 0, + action.count >= 0, + ["success", "cancel", "authenticate"].contains(action) else { + return false + } + + return true + } + + // MARK: - Private Methods + + private static func action(from url: URL) -> String? { + guard let action = url.lastPathComponent.components(separatedBy: "?").first, !action.isEmpty else { + return url.host + } + + return action + } +} diff --git a/UnitTests/BraintreePayPalTests/BTPayPalAppSwitchReturnURL_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalAppSwitchReturnURL_Tests.swift deleted file mode 100644 index 472b9b1ac0..0000000000 --- a/UnitTests/BraintreePayPalTests/BTPayPalAppSwitchReturnURL_Tests.swift +++ /dev/null @@ -1,25 +0,0 @@ -import XCTest -@testable import BraintreePayPal - -final class BTPayPalAppSwitchReturnURL_Tests: XCTestCase { - - func testInitWithURL_whenSuccessReturnURL_createsValuesAndSetsSuccessState() { - let returnURL = BTPayPalAppSwitchReturnURL(url: URL(string: "https://www.merchant-app.com/merchant-path/success?token=A_FAKE_EC_TOKEN&ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!) - XCTAssertEqual(returnURL?.state, .succeeded) - } - - func testInitWithURL_whenSuccessReturnURLWithoutToken_createsValuesAndSetsSuccessState() { - let returnURL = BTPayPalAppSwitchReturnURL(url: URL(string: "https://www.merchant-app.com/merchant-path/success?ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!) - XCTAssertEqual(returnURL?.state, .succeeded) - } - - func testInitWithURL_whenCancelURLWithoutToken_setsCancelState() { - let returnURL = BTPayPalAppSwitchReturnURL(url: URL(string: "https://www.merchant-app.com/merchant-path/cancel?ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!) - XCTAssertEqual(returnURL?.state, .canceled) - } - - func testInitWithURL_whenUnknownURLWithoutToken_setsUnknownState() { - let returnURL = BTPayPalAppSwitchReturnURL(url: URL(string: "https://www.merchant-app.com/merchant-path/garbage-url")!) - XCTAssertEqual(returnURL?.state, .unknownPath) - } -} diff --git a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift index 0b1d3a48f3..a2dd4ea608 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift @@ -218,7 +218,9 @@ class BTPayPalClient_Tests: XCTestCase { ] ]) - payPalClient.webAuthenticationSession = MockWebAuthenticationSession() + let mockWebAuthenticationSession = MockWebAuthenticationSession() + mockWebAuthenticationSession.cannedResponseURL = URL(string: "https://www.paypal.com/checkout/success") + payPalClient.webAuthenticationSession = mockWebAuthenticationSession let request = BTPayPalCheckoutRequest(amount: "1") payPalClient.tokenize(request) { _, _ in } @@ -278,7 +280,9 @@ class BTPayPalClient_Tests: XCTestCase { ] ]) - payPalClient.webAuthenticationSession = MockWebAuthenticationSession() + let mockWebAuthenticationSession = MockWebAuthenticationSession() + mockWebAuthenticationSession.cannedResponseURL = URL(string: "https://www.paypal.com/checkout/success") + payPalClient.webAuthenticationSession = mockWebAuthenticationSession let request = BTPayPalCheckoutRequest(amount: "1") payPalClient.tokenize(request) { _, _ in } @@ -286,6 +290,7 @@ class BTPayPalClient_Tests: XCTestCase { XCTAssertEqual(mockAPIClient.postedPayPalContextID, "BA-Random-Value") XCTAssertEqual(mockAPIClient.postedLinkType, "deeplink") XCTAssertEqual(mockAPIClient.postedPayPalAppInstalled, "false") + XCTAssertTrue(mockAPIClient.postedAnalyticsEvents.contains("paypal:tokenize:handle-return:started")) } // MARK: - Browser switch diff --git a/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift new file mode 100644 index 0000000000..2d178c4435 --- /dev/null +++ b/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift @@ -0,0 +1,27 @@ +import XCTest +@testable import BraintreePayPal + +final class BTPayPalReturnURL_Tests: XCTestCase { + + func testInitWithURL_whenSuccessReturnURL_createsValuesAndSetsSuccessState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "https://www.merchant-app.com/merchant-path/success?token=A_FAKE_EC_TOKEN&ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!)) + XCTAssertEqual(returnURL?.state, .succeeded) + } + + func testInitWithURL_whenSuccessReturnURLWithoutToken_createsValuesAndSetsSuccessState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "https://www.merchant-app.com/merchant-path/success?ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!)) + XCTAssertEqual(returnURL?.state, .succeeded) + } + + func testInitWithURL_whenCancelURLWithoutToken_setsCancelState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "https://www.merchant-app.com/merchant-path/cancel?ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!)) + XCTAssertEqual(returnURL?.state, .canceled) + } + + func testInitWithURL_whenUnknownURLWithoutToken_setsUnknownState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "https://www.merchant-app.com/merchant-path/garbage-url")!)) + XCTAssertEqual(returnURL?.state, .unknownPath) + } + + // TODO: add init for web based flow +} From f894f8a9a8e30ca58f771be214a06d0f21eefcb8 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 10 Apr 2024 15:41:14 -0500 Subject: [PATCH 2/8] add and check --- Sources/BraintreePayPal/BTPayPalReturnURL.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreePayPal/BTPayPalReturnURL.swift b/Sources/BraintreePayPal/BTPayPalReturnURL.swift index 39033ed0aa..d6b6ea5a5b 100644 --- a/Sources/BraintreePayPal/BTPayPalReturnURL.swift +++ b/Sources/BraintreePayPal/BTPayPalReturnURL.swift @@ -59,7 +59,7 @@ struct BTPayPalReturnURL { /// If we are using the deeplink/ASWeb based PayPal flow we want to check that the host and path matches /// the static callbackURLHostAndPath. For the universal link flow we do not care about this check. - if hostAndPath != BTPayPalRequest.callbackURLHostAndPath, linkType == "deeplink" { + if hostAndPath != BTPayPalRequest.callbackURLHostAndPath && linkType == "deeplink" { return false } From 7d5e65e97dee82660d09e6adad86f534e6c13417 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 10 Apr 2024 15:54:06 -0500 Subject: [PATCH 3/8] update fallback logic to handle properly --- Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift | 4 ++-- Sources/BraintreePayPal/BTPayPalClient.swift | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift b/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift index beb3f5518a..9e8e187277 100644 --- a/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift +++ b/Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift @@ -36,8 +36,8 @@ struct BTPayPalApprovalURLParser { } } - init?(body: BTJSON) { - if let payPalAppRedirectURL = body["agreementSetup"]["paypalAppApprovalUrl"].asURL() { + init?(body: BTJSON, linkType: String?) { + if linkType == "universal", let payPalAppRedirectURL = body["agreementSetup"]["paypalAppApprovalUrl"].asURL() { redirectType = .payPalApp(url: payPalAppRedirectURL) } else if let approvalURL = body["paymentResource"]["redirectUrl"].asURL() ?? body["agreementSetup"]["approvalUrl"].asURL() { diff --git a/Sources/BraintreePayPal/BTPayPalClient.swift b/Sources/BraintreePayPal/BTPayPalClient.swift index 6fb30ecc36..178407692f 100644 --- a/Sources/BraintreePayPal/BTPayPalClient.swift +++ b/Sources/BraintreePayPal/BTPayPalClient.swift @@ -48,6 +48,9 @@ import BraintreeDataCollector // MARK: - Private Properties + /// URL Scheme for PayPal In-App Checkout + private let payPalInAppScheme: String = "paypal-in-app-checkout://" + /// Indicates if the user returned back to the merchant app from the `BTWebAuthenticationSession` /// Will only be `true` if the user proceed through the `UIAlertController` private var webSessionReturned: Bool = false @@ -59,9 +62,6 @@ import BraintreeDataCollector /// Used for sending the type of flow, universal vs deeplink to FPTI private var linkType: String? = nil - /// URL Scheme for PayPal In-App Checkout - private let payPalInAppScheme: String = "paypal-in-app-checkout://" - // MARK: - Initializer /// Initialize a new PayPal client instance. @@ -314,7 +314,7 @@ import BraintreeDataCollector return } - guard let body, let approvalURL = BTPayPalApprovalURLParser(body: body) else { + guard let body, let approvalURL = BTPayPalApprovalURLParser(body: body, linkType: self.linkType) else { self.notifyFailure(with: BTPayPalError.invalidURL("Missing approval URL in gateway response."), completion: completion) return } From 32622160f3cff60a20fd0e3074c1835ce4c6d6db Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 10 Apr 2024 15:54:21 -0500 Subject: [PATCH 4/8] update plist for app installed check --- Demo/Application/Supporting Files/Braintree-Demo-Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Demo/Application/Supporting Files/Braintree-Demo-Info.plist b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist index 2ac3ed787a..9d9ccaa3e6 100644 --- a/Demo/Application/Supporting Files/Braintree-Demo-Info.plist +++ b/Demo/Application/Supporting Files/Braintree-Demo-Info.plist @@ -61,7 +61,7 @@ com.braintreepayments.Demo.payments com.venmo.touch.v2 - paypal + paypal-in-app-checkout LSRequiresIPhoneOS From e79b4167362220dddf9fe0aa5f3622ac41986da3 Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Wed, 10 Apr 2024 19:36:57 -0500 Subject: [PATCH 5/8] add webBrowser based BTPayPalReturnURL tests --- .../BTPayPalReturnURL_Tests.swift | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift b/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift index 2d178c4435..1e5bf9fcf6 100644 --- a/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift +++ b/UnitTests/BraintreePayPalTests/BTPayPalReturnURL_Tests.swift @@ -3,12 +3,12 @@ import XCTest final class BTPayPalReturnURL_Tests: XCTestCase { - func testInitWithURL_whenSuccessReturnURL_createsValuesAndSetsSuccessState() { + func testInitWithURL_whenSuccessReturnURL_setsSuccessState() { let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "https://www.merchant-app.com/merchant-path/success?token=A_FAKE_EC_TOKEN&ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!)) XCTAssertEqual(returnURL?.state, .succeeded) } - func testInitWithURL_whenSuccessReturnURLWithoutToken_createsValuesAndSetsSuccessState() { + func testInitWithURL_whenSuccessReturnURLWithoutToken_setsSuccessState() { let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "https://www.merchant-app.com/merchant-path/success?ba_token=A_FAKE_BA_TOKEN&switch_initiated_time=1234567890")!)) XCTAssertEqual(returnURL?.state, .succeeded) } @@ -23,5 +23,18 @@ final class BTPayPalReturnURL_Tests: XCTestCase { XCTAssertEqual(returnURL?.state, .unknownPath) } - // TODO: add init for web based flow + func testInitWithSchemeURL_whenSuccessReturnURL_setsSuccessState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "bar://onetouch/v1/success?token=hermes_token")!)) + XCTAssertEqual(returnURL?.state, .succeeded) + } + + func testInitWithSchemeURL_whenCancelURLWithoutToken_setsCancelState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "bar://onetouch/v1/cancel?token=hermes_token")!)) + XCTAssertEqual(returnURL?.state, .canceled) + } + + func testInitWithSchemeURL_whenUnknownURLWithoutToken_setsUnknownState() { + let returnURL = BTPayPalReturnURL(.payPalApp(url: URL(string: "bar://onetouch/v1/invalid")!)) + XCTAssertEqual(returnURL?.state, .unknownPath) + } } From 2131792cae7d25ac8ffd07ed173a600a0f493d6e Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 11 Apr 2024 09:07:20 -0500 Subject: [PATCH 6/8] cleanup logic --- Sources/BraintreePayPal/BTPayPalClient.swift | 10 +++------- Sources/BraintreePayPal/BTPayPalReturnURL.swift | 4 +--- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/Sources/BraintreePayPal/BTPayPalClient.swift b/Sources/BraintreePayPal/BTPayPalClient.swift index 30a3d9e7f1..000560e782 100644 --- a/Sources/BraintreePayPal/BTPayPalClient.swift +++ b/Sources/BraintreePayPal/BTPayPalClient.swift @@ -188,7 +188,7 @@ import BraintreeDataCollector return } - guard !url.absoluteString.contains("cancel") else { + guard let action = BTPayPalReturnURL.action(from: url), action != "cancel" else { notifyCancel(completion: completion) return } @@ -275,10 +275,8 @@ import BraintreeDataCollector } switch returnURL.state { - case .succeeded: + case .succeeded, .canceled: handleReturn(url, paymentType: .vault, completion: appSwitchCompletion) - case .canceled: - notifyCancel(completion: appSwitchCompletion) case .unknownPath: notifyFailure(with: BTPayPalError.appSwitchReturnURLPathInvalid, completion: appSwitchCompletion) } @@ -430,10 +428,8 @@ import BraintreeDataCollector } switch returnURL.state { - case .succeeded: + case .succeeded, .canceled: handleReturn(url, paymentType: .vault, completion: completion) - case .canceled: - notifyCancel(completion: completion) case .unknownPath: notifyFailure(with: BTPayPalError.asWebAuthenticationSessionURLInvalid(url.absoluteString), completion: completion) } diff --git a/Sources/BraintreePayPal/BTPayPalReturnURL.swift b/Sources/BraintreePayPal/BTPayPalReturnURL.swift index d6b6ea5a5b..4552c7f3a7 100644 --- a/Sources/BraintreePayPal/BTPayPalReturnURL.swift +++ b/Sources/BraintreePayPal/BTPayPalReturnURL.swift @@ -74,9 +74,7 @@ struct BTPayPalReturnURL { return true } - // MARK: - Private Methods - - private static func action(from url: URL) -> String? { + static func action(from url: URL) -> String? { guard let action = url.lastPathComponent.components(separatedBy: "?").first, !action.isEmpty else { return url.host } From 58403e1c50e1cd7ad2cea5deae6d46a6a3c898fe Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 11 Apr 2024 13:35:45 -0500 Subject: [PATCH 7/8] PR feedback: update docstrings and enum name --- Sources/BraintreePayPal/BTPayPalReturnURL.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/BraintreePayPal/BTPayPalReturnURL.swift b/Sources/BraintreePayPal/BTPayPalReturnURL.swift index 4552c7f3a7..e87c59073a 100644 --- a/Sources/BraintreePayPal/BTPayPalReturnURL.swift +++ b/Sources/BraintreePayPal/BTPayPalReturnURL.swift @@ -4,22 +4,22 @@ import Foundation import BraintreeCore #endif -enum BTPayPalAppSwitchReturnURLState { +enum BTPayPalReturnURLState { case unknownPath case succeeded case canceled } -/// This class interprets URLs received from the PayPal app via app switch returns. +/// This class interprets URLs received from the PayPal app via app switch returns and web returns via ASWebAuthenticationSession. /// -/// PayPal app switch authorization requests should result in success or user-initiated cancelation. These states are communicated in the url. +/// PayPal app switch and ASWebAuthenticationSession authorization requests should result in success or user-initiated cancelation. These states are communicated in the url. struct BTPayPalReturnURL { /// The overall status of the app switch - success, cancelation, or an unknown path - var state: BTPayPalAppSwitchReturnURLState = .unknownPath + var state: BTPayPalReturnURLState = .unknownPath - /// Initializes a new `BTPayPalAppSwitchReturnURL` - /// - Parameter url: an incoming app switch url + /// Initializes a new `BTPayPalReturnURL` + /// - Parameter url: an incoming app switch or ASWebAuthenticationSession url init?(_ redirectType: PayPalRedirectType) { switch redirectType { case .payPalApp(let url), .webBrowser(let url): From b3543f52e894eacb238be5ddd71b147d7ea50efc Mon Sep 17 00:00:00 2001 From: Jax DesMarais-Leder Date: Thu, 11 Apr 2024 14:45:13 -0500 Subject: [PATCH 8/8] Update Sources/BraintreePayPal/BTPayPalReturnURL.swift --- Sources/BraintreePayPal/BTPayPalReturnURL.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/BraintreePayPal/BTPayPalReturnURL.swift b/Sources/BraintreePayPal/BTPayPalReturnURL.swift index e87c59073a..ab4e964133 100644 --- a/Sources/BraintreePayPal/BTPayPalReturnURL.swift +++ b/Sources/BraintreePayPal/BTPayPalReturnURL.swift @@ -36,7 +36,7 @@ struct BTPayPalReturnURL { // MARK: - Static Methods /// Evaluates whether the url represents a valid PayPal return URL. - /// - Parameter url: an app switch return URL + /// - Parameter url: an app switch or ASWebAuthenticationSession return URL /// - Returns: `true` if the url represents a valid PayPal app switch return static func isValid(_ url: URL) -> Bool { url.scheme == "https" && (url.path.contains("cancel") || url.path.contains("success"))