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] Conform BTPayPalClient to BTAppContextSwitchClient Protocol #1218

Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c6dddfa
add BTAppContextSwitcher.sharedInstance.universalLink; add implementa…
jaxdesmarais Mar 11, 2024
8bfb2bf
add universal link tests
jaxdesmarais Mar 11, 2024
afe1923
Merge branch 'paypal-app-switch-feature' into app-context-switcher-un…
jaxdesmarais Mar 12, 2024
f69edbb
add conformance of BTPayPalClient to BTAppContextSwitchClient
jaxdesmarais Mar 12, 2024
666324a
add unit tests for canHandleReturnURL
jaxdesmarais Mar 12, 2024
3fd897b
add test for BTPayPalClient.handleReturnURL
jaxdesmarais Mar 12, 2024
62b57bc
update VenmoClient to only handle return URL schemes
jaxdesmarais Mar 13, 2024
bb8a1f7
update Venmo Unit Test to set returnURLScheme
jaxdesmarais Mar 13, 2024
ef40241
add additional venmo client test with invalid scheme
jaxdesmarais Mar 13, 2024
9efbff2
register BTPayPalClient in BTAppContextSwitcher; update VC to not dea…
jaxdesmarais Mar 13, 2024
49879e4
update scene delegate to use contains instead of checking exact path
jaxdesmarais Mar 14, 2024
adfb1df
Merge branch 'paypal-app-switch-feature' into app-context-switcher-cl…
jaxdesmarais Mar 14, 2024
14a35df
Merge branch 'paypal-app-switch-feature' into app-context-switcher-cl…
jaxdesmarais Mar 18, 2024
429533e
PR feedback: revert BTPayPalClient to lazy instantiation in VC
jaxdesmarais Mar 18, 2024
a6aabdd
PR feedback: revert isValid change in Venmo flow
jaxdesmarais Mar 19, 2024
4b39541
add new BTPayPalAppSwitchReturnURL struct to handle return URL logic
jaxdesmarais Mar 19, 2024
2d53f54
PR feedback: rename handleOpen to handleReturnURL
jaxdesmarais Mar 19, 2024
d4e6e3e
cleanup unneeded tests
jaxdesmarais Mar 19, 2024
ff61d88
sort files by name
jaxdesmarais Mar 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Demo/Application/Base/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import BraintreeCore
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {

private let returnURLScheme = "com.braintreepayments.Demo.payments"
private let universalLinkURL = "https://braintree-ios-demo.fly.dev/braintree-payments"
private let processInfoArgs = ProcessInfo.processInfo.arguments
private let userDefaults = UserDefaults.standard

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
registerDefaultsFromSettings()
persistDemoSettings()
BTAppContextSwitcher.sharedInstance.returnURLScheme = returnURLScheme

BTAppContextSwitcher.sharedInstance.universalLink = universalLinkURL

userDefaults.setValue(true, forKey: "magnes.debug.mode")

return true
Expand Down
4 changes: 2 additions & 2 deletions Demo/Application/Base/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if let returnURL = userActivity.webpageURL {
// TODO: implementation - pass full URL to BT SDK
if let returnURL = userActivity.webpageURL, returnURL.path == "/braintree-payments" {
print("Returned to Demo app via universal link: \(returnURL)")
_ = BTAppContextSwitcher.sharedInstance.handleOpen(returnURL)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,6 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
}

@objc func universalLinkFlow(_ sender: UIButton) {
UIApplication.shared.open(URL(string: "https://braintree-ios-demo.fly.dev")!)
UIApplication.shared.open(URL(string: "https://braintree-ios-demo.fly.dev/braintree-payments")!)
}
}
5 changes: 4 additions & 1 deletion Sources/BraintreeCore/BTAppContextSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ import UIKit
/// The URL scheme to return to this app after switching to another app or opening a SFSafariViewController.
/// This URL scheme must be registered as a URL Type in the app's info.plist, and it must start with the app's bundle ID.
public var returnURLScheme: String = ""


/// The URL to use for the PayPal universal link flow. Must contain the path `braintree-payments`.
public var universalLink: String = ""

// MARK: - Private Properties

private var appContextSwitchClients = [BTAppContextSwitchClient.Type]()
Expand Down
27 changes: 27 additions & 0 deletions Sources/BraintreePayPal/BTPayPalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import BraintreeDataCollector
/// Exposed for testing, the ASWebAuthenticationSession instance used for the PayPal flow
var webAuthenticationSession: BTWebAuthenticationSession

// MARK: - Static Properties

/// This static instance of `BTPayPalClient` is used during the app switch process.
/// We require a static reference of the client to call `handleReturnURL` and return to the app.
static var payPalClient: BTPayPalClient? = nil

// MARK: - Private Properties

/// Indicates if the user returned back to the merchant app from the `BTWebAuthenticationSession`
Expand Down Expand Up @@ -221,6 +227,12 @@ import BraintreeDataCollector
performSwitchRequest(appSwitchURL: url, paymentType: paymentType, completion: completion)
}

// MARK: - App Switch Methods

func handleOpen(_ url: URL) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I know this is the naming we use in Venmo, but it's a little confusing to me since without looking through the code callsites you don't know if this method is for opening the Venmo app, or opening the merchant app.

Take it or leave it comment - but we could rename to handleReturnURL() or something similar

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense to me too: 2d53f54

// TODO: implement handling return URL in a follow up PR
}

// MARK: - Private Methods

private func tokenize(
Expand Down Expand Up @@ -431,3 +443,18 @@ import BraintreeDataCollector
completion(nil, BTPayPalError.canceled)
}
}

extension BTPayPalClient: BTAppContextSwitchClient {
/// :nodoc:
@_documentation(visibility: private)
@objc public static func handleReturnURL(_ url: URL) {
payPalClient?.handleOpen(url)
BTPayPalClient.payPalClient = nil
}

/// :nodoc:
@_documentation(visibility: private)
@objc public static func canHandleReturnURL(_ url: URL) -> Bool {
url.scheme == "https" && (url.path.contains("cancel") || url.path.contains("success"))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Based on the current contracts I don't believe we will ever get an error path back. In the future if we do we can consider adding it here. We can also consider adding a check here that the host contains the universalLink set on BTAppContextSwitcher. What do folks think?

Copy link
Contributor

Choose a reason for hiding this comment

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

There won't be an error specific scenario in the URL, no.

Though we will want to surface the cancel as an error to the merchant, as well as continue with the flow to call the paypal_accounts API in the case of success. Are you planning to add those paths in this PR? If not, can we add TODOs?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Though we will want to surface the cancel as an error to the merchant

🤔 how would we both surface the cancel as an error and continue the flow? If we surfaced it in our completion I would expect the execution of that path would also end? Or do we want to diverge from that pattern and latter in the flow return both a nonce and an error?

Are you planning to add those paths in this PR? If not, can we add TODOs?

There is a TODO in handleOpen above where we will build out the logic in a future PR to handle the return URL. This is just the logic that determines if our SDK can process the URL we get back (though I did notice just now BTAppContextSwitcher wasn't registered properly and updated here along with deploying a success path so we can test that these methods are hit: 9efbff2)

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 how would we both surface the cancel as an error and continue the flow? If we surfaced it in our completion I would expect the execution of that path would also end?

Yes! Like we do today when we get back cancel in the URL for the web-based flow, we end the flow for the merchant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes since, sorry I read that wrong at first as we want to continue the flow on cancel, but that portion was for success. 🙈

Do we think it'd be helpful to also validate that the host contains the value passed into BTAppContextSwitcher.shared.universalLink or do we think this is sufficient (checking for https and cancel or success)?

Copy link
Contributor

Choose a reason for hiding this comment

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

Good question - maybe we can assume that if the switch successfully made it into the merchant app, that the host of the URL is fine. I vote for not doing that check, for now. But don't feel strongly if you have other thoughts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with that too, we can always add it in a future PR if we decide we need additional validation.

Copy link
Contributor

@scannillo scannillo Mar 19, 2024

Choose a reason for hiding this comment

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

Another thought - might be more readable to keep this similar to Venmo which has a separate class (BTVenmoAppSwitchReturnURL) for the isValid() logic & eventual param parsing? So adding something like a BTVeniceAppSwitchReturnURL class

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm down, updated here (I opted to call it BTPayPalAppSwitchReturnURL since I assume Venice is somewhat internal): 4b39541

}
}
4 changes: 3 additions & 1 deletion Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ struct BTVenmoAppSwitchReturnURL {
/// - Parameter url: an app switch return URL
/// - Returns: `true` if the url represents a Venmo Touch app switch return
static func isValid(url: URL) -> Bool {
url.host == "x-callback-url" && url.path.hasPrefix("/vzero/auth/venmo/")
url.scheme?.caseInsensitiveCompare(BTAppContextSwitcher.sharedInstance.returnURLScheme) == .orderedSame
scannillo marked this conversation as resolved.
Show resolved Hide resolved
&& url.host == "x-callback-url"
&& url.path.hasPrefix("/vzero/auth/venmo/")
}
}
10 changes: 8 additions & 2 deletions UnitTests/BraintreeCoreTests/BTAppContextSwitcher_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class BTAppContextSwitcher_Tests: XCTestCase {
appSwitch.register(MockAppContextSwitchClient.self)
let expectedURL = URL(string: "fake://url")!

BTAppContextSwitcher.sharedInstance.handleOpen(expectedURL)
_ = BTAppContextSwitcher.sharedInstance.handleOpen(expectedURL)

XCTAssertEqual(MockAppContextSwitchClient.lastCanHandleURL!, expectedURL)
}
Expand Down Expand Up @@ -80,13 +80,19 @@ class BTAppContextSwitcher_Tests: XCTestCase {
XCTAssertFalse(handled)
XCTAssertNil(MockAppContextSwitchClient.lastHandleReturnURL)
}

func testHandleOpenURLContext_withNoAppSwitching_returnsFalse() {
let mockURLContext = BTMockOpenURLContext(url: URL(string: "fake://url")!).mock
let handled = BTAppContextSwitcher.sharedInstance.handleOpenURL(context: mockURLContext)
XCTAssertFalse(handled)
}

// MARK: - universalLink Tests

func testSetUniversalLink() {
BTAppContextSwitcher.sharedInstance.universalLink = "https://fake.com"
XCTAssertEqual(appSwitch.universalLink, "https://fake.com")
}
}

@objcMembers class MockAppContextSwitchClient: BTAppContextSwitchClient {
Expand Down
37 changes: 37 additions & 0 deletions UnitTests/BraintreePayPalTests/BTPayPalClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,43 @@ class BTPayPalClient_Tests: XCTestCase {
XCTAssertEqual(metaParameters["sessionId"] as? String, mockAPIClient.metadata.sessionID)
}

// MARK: - App Switch

func testCanHandleReturnURL_whenHostIsURLScheme_returnsFalse() {
let url = URL(string: "fake-scheme://success")!
XCTAssertFalse(BTPayPalClient.canHandleReturnURL(url))
}

func testCanHandleReturnURL_whenPathIsInvalid_returnsFalse() {
let url = URL(string: "https://mycoolwebsite.com/junkpath")!
XCTAssertFalse(BTPayPalClient.canHandleReturnURL(url))
}

func testCanHandleReturnURL_whenSchemeIsHTTP_returnsFalse() {
let url = URL(string: "http://mycoolwebsite.com/success")!
XCTAssertFalse(BTPayPalClient.canHandleReturnURL(url))
}

func testCanHandleReturnURL_whenPathIsValidSuccess_returnsTrue() {
let url = URL(string: "https://mycoolwebsite.com/braintree-payments/success")!
XCTAssertTrue(BTPayPalClient.canHandleReturnURL(url))
}

func testCanHandleReturnURL_whenPathIsValidCancel_returnsTrue() {
let url = URL(string: "https://mycoolwebsite.com/braintree-payments/cancel")!
XCTAssertTrue(BTPayPalClient.canHandleReturnURL(url))
}

func testCanHandleReturnURL_whenPathIsValidWithQueryParameters_returnsTrue() {
let url = URL(string: "https://mycoolwebsite.com/braintree-payments/success?token=112233")!
XCTAssertTrue(BTPayPalClient.canHandleReturnURL(url))
}

func testHandleReturnURL_whenURLIsValid_setsBTPayPalClientToNil() {
BTPayPalClient.handleReturnURL(URL(string: "https://mycoolwebsite.com/braintree-payments/success")!)
XCTAssertNil(BTPayPalClient.payPalClient)
}

// MARK: - Analytics

func testAPIClientMetadata_hasIntegrationSetToCustom() {
Expand Down
3 changes: 2 additions & 1 deletion UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,8 @@ class BTVenmoClient_Tests: XCTestCase {
XCTAssertFalse(venmoClient.isVenmoAppInstalled())
}

func testCanHandleReturnURL_withValidHost_andValidPath_returnsTrue() {
func testCanHandleReturnURL_withValidSchemeHostAndValidPath_returnsTrue() {
BTAppContextSwitcher.sharedInstance.returnURLScheme = "fake-scheme"
let host = "x-callback-url"
let path = "/vzero/auth/venmo/"
XCTAssertTrue(BTVenmoClient.canHandleReturnURL(URL(string: "fake-scheme://\(host)\(path)fake-result")!))
Expand Down
Loading