Skip to content

Commit

Permalink
[QL] Consolidate Return URL Logic (#1271)
Browse files Browse the repository at this point in the history
* updates to move ReturnURL logic to one place
* update fallback logic to handle properly
* update plist for app installed check
* add webBrowser based BTPayPalReturnURL tests
* PR feedback: update docstrings and enum name
  • Loading branch information
jaxdesmarais authored Apr 11, 2024
1 parent 3121388 commit 346e4e9
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 142 deletions.
16 changes: 8 additions & 8 deletions Braintree.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -870,7 +870,7 @@
BE698EA528B3CDAD001D9B10 /* BTCacheDateValidator_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCacheDateValidator_Tests.swift; sourceTree = "<group>"; };
BE698EAA28B50F41001D9B10 /* BTClientToken_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTClientToken_Tests.swift; sourceTree = "<group>"; };
BE6BC22B2BA9C67600C3E321 /* BTPayPalVaultBaseRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalVaultBaseRequest.swift; sourceTree = "<group>"; };
BE6BC22D2BA9CFFC00C3E321 /* BTPayPalAppSwitchReturnURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalAppSwitchReturnURL.swift; sourceTree = "<group>"; };
BE6BC22D2BA9CFFC00C3E321 /* BTPayPalReturnURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalReturnURL.swift; sourceTree = "<group>"; };
BE70A962284FA3F000F6D3F7 /* BTDataCollectorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTDataCollectorError.swift; sourceTree = "<group>"; };
BE70A964284FA9DE00F6D3F7 /* MockBTDataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBTDataCollector.swift; sourceTree = "<group>"; };
BE7A9643299FC5DE009AB920 /* BTConfiguration+ApplePay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BTConfiguration+ApplePay.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -902,7 +902,7 @@
BE9FB82A2898324C00D6FE2F /* BTPaymentMethodNonce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPaymentMethodNonce.swift; sourceTree = "<group>"; };
BE9FB82C28984ADE00D6FE2F /* BTPaymentMethodNonceParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPaymentMethodNonceParser.swift; sourceTree = "<group>"; };
BEB9BF522A26872B00A3673E /* BTWebAuthenticationSessionClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTWebAuthenticationSessionClient.swift; sourceTree = "<group>"; };
BEBA590E2BB1B5B9005FA8A2 /* BTPayPalAppSwitchReturnURL_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalAppSwitchReturnURL_Tests.swift; sourceTree = "<group>"; };
BEBA590E2BB1B5B9005FA8A2 /* BTPayPalReturnURL_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTPayPalReturnURL_Tests.swift; sourceTree = "<group>"; };
BEBC6E5D2927CF59004E25A0 /* Braintree.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Braintree.h; sourceTree = "<group>"; };
BEBC6F252937A510004E25A0 /* BTClientMetadata_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTClientMetadata_Tests.swift; sourceTree = "<group>"; };
BEBC6F272937BD1F004E25A0 /* BTGraphQLHTTP_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTGraphQLHTTP_Tests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
};
Expand Down
41 changes: 0 additions & 41 deletions Sources/BraintreePayPal/BTPayPalAppSwitchReturnURL.swift

This file was deleted.

96 changes: 30 additions & 66 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 let action = BTPayPalReturnURL.action(from: url), action != "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]
Expand Down Expand Up @@ -256,7 +269,7 @@ 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
}
Expand Down Expand Up @@ -409,7 +422,17 @@ 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, .canceled:
handleReturn(url, paymentType: .vault, completion: completion)
case .unknownPath:
notifyFailure(with: BTPayPalError.asWebAuthenticationSessionURLInvalid(url.absoluteString), completion: completion)
}
} sessionDidAppear: { [self] didAppear in
if didAppear {
apiClient.sendAnalyticsEvent(
Expand Down Expand Up @@ -443,65 +466,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

Expand Down Expand Up @@ -554,6 +518,6 @@ extension BTPayPalClient: BTAppContextSwitchClient {
/// :nodoc:
@_documentation(visibility: private)
@objc public static func canHandleReturnURL(_ url: URL) -> Bool {
BTPayPalAppSwitchReturnURL.isValid(url)
BTPayPalReturnURL.isValid(url)
}
}
84 changes: 84 additions & 0 deletions Sources/BraintreePayPal/BTPayPalReturnURL.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation

#if canImport(BraintreeCore)
import BraintreeCore
#endif

enum BTPayPalReturnURLState {
case unknownPath
case succeeded
case canceled
}

/// This class interprets URLs received from the PayPal app via app switch returns and web returns via ASWebAuthenticationSession.
///
/// 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: BTPayPalReturnURLState = .unknownPath

/// 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):
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 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"))
}

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
}

static func action(from url: URL) -> String? {
guard let action = url.lastPathComponent.components(separatedBy: "?").first, !action.isEmpty else {
return url.host
}

return action
}
}

This file was deleted.

Loading

0 comments on commit 346e4e9

Please sign in to comment.