-
Notifications
You must be signed in to change notification settings - Fork 293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[QL] Consolidate Return URL Logic #1271
Changes from 9 commits
510053e
ddf266e
f894f8a
7d5e65e
3262216
b81d4bb
63ffc97
e79b416
2131792
58403e1
b3543f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
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] | ||
|
@@ -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 | ||
} | ||
|
@@ -314,7 +327,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 | ||
} | ||
|
@@ -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( | ||
|
@@ -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 | ||
|
||
|
@@ -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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,84 @@ | ||||||
import Foundation | ||||||
|
||||||
#if canImport(BraintreeCore) | ||||||
import BraintreeCore | ||||||
#endif | ||||||
|
||||||
enum BTPayPalAppSwitchReturnURLState { | ||||||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
case unknownPath | ||||||
case succeeded | ||||||
case canceled | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. small nit:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our SDKs always use the 1 L canceled/cancelation |
||||||
} | ||||||
|
||||||
/// 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 { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This file was renamed but is showing additional diff. Adding logic for the web flow in the |
||||||
|
||||||
/// The overall status of the app switch - success, cancelation, or an unknown path | ||||||
var state: BTPayPalAppSwitchReturnURLState = .unknownPath | ||||||
|
||||||
/// Initializes a new `BTPayPalAppSwitchReturnURL` | ||||||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
/// - 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 | ||||||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
/// - Returns: `true` if the url represents a valid PayPal app switch return | ||||||
jaxdesmarais marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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" { | ||||||
Comment on lines
+60
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @agedd I know you mentioned this logic for this specific if block being confusing before. I added the docstrings as well as passing/checking the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this looks great! @jaxdesmarais imo definitely a lot more clearer to read :) |
||||||
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 | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit for another time - maybe another indicator we should move this to an enum
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I don't think the
BTPayPalApprovalURLParser
class should care if the merchant has opted into universal links or not. It's only determining which URL type it got back from the GW response.The flow logic for falling back to the web flow already is taken care of in
BTPayPalClient
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of Thursday, we were still getting the
paypalAppApprovalUrl
from the Gateway regardless of theBTPayPalClient
. This may have been because we were working with a mock and an update may have been deployed to just allow our client side handling. Worth digging into but I was still getting this URL with thePPBeta
app uninstalled.