Skip to content
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

Merged
merged 11 commits into from
Apr 11, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
<array>
<string>com.braintreepayments.Demo.payments</string>
<string>com.venmo.touch.v2</string>
<string>paypal</string>
<string>paypal-in-app-checkout</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
Expand Down
41 changes: 0 additions & 41 deletions Sources/BraintreePayPal/BTPayPalAppSwitchReturnURL.swift

This file was deleted.

4 changes: 2 additions & 2 deletions Sources/BraintreePayPal/BTPayPalApprovalURLParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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 the BTPayPalClient. 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 the PPBeta app uninstalled.

redirectType = .payPalApp(url: payPalAppRedirectURL)
} else if let approvalURL = body["paymentResource"]["redirectUrl"].asURL() ??
body["agreementSetup"]["approvalUrl"].asURL() {
Expand Down
104 changes: 34 additions & 70 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The responseDictionary method was a bit odd because it was both building the dictionary to pass into the post parameters as well as doing a check that the URL did not contain cancel. The only part that used the return URL was the cancel check so I broke this out to build the dictionary here which made more sense.

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 @@ -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
}
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 BTPayPalAppSwitchReturnURLState {
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
case unknownPath
case succeeded
case canceled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small nit:

Suggested change
case canceled
case cancelled

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 {
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 init as well as adding isValidURLAction plus action methods here are the actual changes.


/// 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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 linkType here as deeplink instead of passing in the request and doing some weird stuff with that. Please let me know if this makes it clearer what this check is doing!

Copy link
Contributor

Choose a reason for hiding this comment

The 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
}
}
Loading
Loading