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

Venmo Universal Link Return #1440

Merged
merged 23 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dced060
update Venmo flow to accept optional universal link
jaxdesmarais Sep 17, 2024
d814136
Merge branch 'main' into venmo-universal-link-return
scannillo Oct 17, 2024
912eb0f
Clarify Docstrings for universal link in BTVenmoClient
scannillo Oct 17, 2024
9780698
Add Universal Link return toggle to Demo
scannillo Oct 17, 2024
d4631ba
swiftlint cleanup
jaxdesmarais Oct 17, 2024
c78d293
Fixup - cleanup Demo toggle b/w universal link return & regular return
scannillo Oct 17, 2024
b191177
update redirectURL tests
jaxdesmarais Oct 17, 2024
de50a66
Merge branch 'venmo-universal-link-return' of https://github.com/brai…
jaxdesmarais Oct 17, 2024
43eeea5
Fixup - docstring clarification
scannillo Oct 17, 2024
f753a9c
Merge branch 'venmo-universal-link-return' of https://github.com/brai…
scannillo Oct 17, 2024
91ea18e
add CHANGELOG entry
jaxdesmarais Oct 21, 2024
2e8ba83
PR feedabck: update to handle formatting of merchant passed URL
jaxdesmarais Oct 21, 2024
eef74ed
PR feedback: deprecate returnURLScheme
jaxdesmarais Oct 21, 2024
264df28
Merge branch 'main' into venmo-universal-link-return
jaxdesmarais Oct 21, 2024
37005b2
cleanup after merge from main
jaxdesmarais Oct 21, 2024
30e60dc
update spacing and swiftlint disable scope
jaxdesmarais Oct 21, 2024
2fd461c
Update Sources/BraintreeVenmo/BTVenmoClient.swift
jaxdesmarais Oct 21, 2024
ed47550
Merge branch 'main' into venmo-universal-link-return
jaxdesmarais Oct 21, 2024
f8b3c8d
update to disable next
jaxdesmarais Oct 21, 2024
f02b13e
Merge branch 'venmo-universal-link-return' of https://github.com/brai…
jaxdesmarais Oct 21, 2024
22ab7c0
remove deprecated so pod lib lint passes
jaxdesmarais Oct 22, 2024
2705af7
add CHANGELOG and update deprecation message to work with cocoapods
jaxdesmarais Oct 22, 2024
6b90aee
update for swiftlint
jaxdesmarais Oct 22, 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
## unreleased
* BraintreePayPal
* Add `BTPayPalRequest.userPhoneNumber` optional property

* BraintreeVenmo
* Add `BTVenmoClient(apiClient:universalLink:)` to use Universal Links when redirecting back from the Venmo flow

## 6.24.0 (2024-10-15)
* BraintreePayPal
* Add `BTPayPalRecurringBillingDetails` and `BTPayPalRecurringBillingPlanType` opt-in request objects. Including these details will provide transparency to users on their billing schedule, dates, and amounts, as well as launch a modernized checkout UI.
Expand Down
30 changes: 27 additions & 3 deletions Demo/Application/Features/VenmoViewController.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import UIKit
import BraintreeVenmo

// swiftlint:disable implicitly_unwrapped_optional
class VenmoViewController: PaymentButtonBaseViewController {

// swiftlint:disable:next implicitly_unwrapped_optional
var venmoClient: BTVenmoClient!

lazy var universalLinkReturnToggleLabel: UILabel = {
let label = UILabel()
label.text = "Use Universal Link Return"
label.font = .preferredFont(forTextStyle: .footnote)
return label
}()

let universalLinkReturnToggle = UISwitch()

override func viewDidLoad() {
super.viewDidLoad()
venmoClient = BTVenmoClient(apiClient: apiClient)
Expand All @@ -17,7 +26,14 @@ class VenmoViewController: PaymentButtonBaseViewController {
let venmoECDButton = createButton(title: "Venmo (with ECD options)", action: #selector(tappedVenmoWithECD))
let venmoUniversalLinkButton = createButton(title: "Venmo Universal Links", action: #selector(tappedVenmoWithUniversalLinks))

let stackView = UIStackView(arrangedSubviews: [venmoButton, venmoECDButton, venmoUniversalLinkButton])
let stackView = UIStackView(
arrangedSubviews: [
UIStackView(arrangedSubviews: [universalLinkReturnToggleLabel, universalLinkReturnToggle]),
venmoButton,
venmoECDButton,
venmoUniversalLinkButton
]
)
stackView.axis = .vertical
stackView.spacing = 5
stackView.alignment = .center
Expand Down Expand Up @@ -66,9 +82,17 @@ class VenmoViewController: PaymentButtonBaseViewController {
}

func checkout(request: BTVenmoRequest) {
if universalLinkReturnToggle.isOn {
venmoClient = BTVenmoClient(
apiClient: apiClient,
// swiftlint:disable:next force_unwrapping
universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")!
)
}

Task {
do {
let venmoAccount = try await venmoClient.tokenize(request)
let venmoAccount = try await (venmoClient).tokenize(request)
progressBlock("Got a nonce 💎!")
completionBlock(venmoAccount)
} catch {
Expand Down
16 changes: 12 additions & 4 deletions Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ struct BTVenmoAppSwitchRedirectURL {
// MARK: - Initializer

init(
returnURLScheme: String,
paymentContextID: String,
metadata: BTClientMetadata,
returnURLScheme: String?,
universalLink: URL?,
forMerchantID merchantID: String?,
accessToken: String?,
bundleDisplayName: String?,
Expand All @@ -46,9 +47,6 @@ struct BTVenmoAppSwitchRedirectURL {
let base64EncodedBraintreeData = serializedBraintreeData?.base64EncodedString()

queryParameters = [
"x-success": constructRedirectURL(with: returnURLScheme, result: "success"),
"x-error": constructRedirectURL(with: returnURLScheme, result: "error"),
"x-cancel": constructRedirectURL(with: returnURLScheme, result: "cancel"),
"x-source": bundleDisplayName,
"braintree_merchant_id": merchantID,
"braintree_access_token": accessToken,
Expand All @@ -57,6 +55,16 @@ struct BTVenmoAppSwitchRedirectURL {
"braintree_sdk_data": base64EncodedBraintreeData ?? "",
"customerClient": "MOBILE_APP"
]

if let universalLink {
queryParameters["x-success"] = universalLink.absoluteString + "/success"
queryParameters["x-error"] = universalLink.absoluteString + "/error"
queryParameters["x-cancel"] = universalLink.absoluteString + "/cancel"
scannillo marked this conversation as resolved.
Show resolved Hide resolved
} else if let returnURLScheme {
queryParameters["x-success"] = constructRedirectURL(with: returnURLScheme, result: "success")
queryParameters["x-error"] = constructRedirectURL(with: returnURLScheme, result: "error")
queryParameters["x-cancel"] = constructRedirectURL(with: returnURLScheme, result: "cancel")
}
}

// MARK: - Internal Methods
Expand Down
9 changes: 5 additions & 4 deletions Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct BTVenmoAppSwitchReturnURL {
init?(url: URL) {
let parameters = BTURLUtils.queryParameters(for: url)

if url.path == "/vzero/auth/venmo/success" {
if url.path.contains("success") {
if let resourceID = parameters["resource_id"] {
state = .succeededWithPaymentContext
paymentContextID = resourceID
Expand All @@ -50,12 +50,12 @@ struct BTVenmoAppSwitchReturnURL {
nonce = parameters["paymentMethodNonce"] ?? parameters["payment_method_nonce"]
username = parameters["username"]
}
} else if url.path == "/vzero/auth/venmo/error" {
} else if url.path.contains("error") {
state = .failed
let errorMessage: String? = parameters["errorMessage"] ?? parameters["error_message"]
let errorCode = Int(parameters["errorCode"] ?? parameters["error_code"] ?? "0")
error = BTVenmoAppSwitchError.returnURLError(errorCode ?? 0, errorMessage)
} else if url.path == "/vzero/auth/venmo/cancel" {
} else if url.path.contains("cancel") {
state = .canceled
} else {
state = .unknown
Expand All @@ -68,6 +68,7 @@ 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 == "https" && (url.path.contains("cancel") || url.path.contains("success") || url.path.contains("error")))
|| (url.host == "x-callback-url" && url.path.hasPrefix("/vzero/auth/venmo/"))
}
}
17 changes: 15 additions & 2 deletions Sources/BraintreeVenmo/BTVenmoClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,28 @@ import BraintreeCore
/// Used for sending the type of flow, universal vs deeplink to FPTI
private var linkType: LinkType?

private var universalLink: URL?

// MARK: - Initializer

/// Creates an Apple Pay client
/// Creates an Venmo client
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
/// - Parameter apiClient: An API client
@objc(initWithAPIClient:)
public init(apiClient: BTAPIClient) {
BTAppContextSwitcher.sharedInstance.register(BTVenmoClient.self)
self.apiClient = apiClient
}

/// Initialize a new Venmo client instance.
/// - Parameters:
/// - apiClient: The API Client
/// - universalLink: The URL for the Venmo app to redirect to after user authentication completes. Must be a valid HTTPS URL dedicated to Braintree app switch returns.
@objc(initWithAPIClient:universalLink:)
public convenience init(apiClient: BTAPIClient, universalLink: URL) {
self.init(apiClient: apiClient)
self.universalLink = universalLink
}

// MARK: - Public Methods

/// Initiates Venmo login via app switch, which returns a BTVenmoAccountNonce when successful.
Expand Down Expand Up @@ -151,9 +163,10 @@ import BraintreeCore

do {
let appSwitchURL = try BTVenmoAppSwitchRedirectURL(
returnURLScheme: returnURLScheme,
paymentContextID: paymentContextID,
metadata: metadata,
returnURLScheme: returnURLScheme,
universalLink: self.universalLink,
forMerchantID: merchantProfileID,
accessToken: configuration.venmoAccessToken,
bundleDisplayName: bundleDisplayName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
func testUrlSchemeURL_whenAllValuesAreInitialized_returnsURLWithPaymentContextID() {
do {
let requestURL = try BTVenmoAppSwitchRedirectURL(
returnURLScheme: "url-scheme",
paymentContextID: "12345",
metadata: BTClientMetadata(),
returnURLScheme: "url-scheme",
universalLink: nil,
forMerchantID: "merchant-id",
accessToken: "access-token",
bundleDisplayName: "display-name",
Expand All @@ -29,9 +30,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
func testAppSwitchURL_whenMerchantIDNil_throwsError() {
do {
_ = try BTVenmoAppSwitchRedirectURL(
returnURLScheme: "url-scheme",
paymentContextID: "12345",
metadata: BTClientMetadata(),
returnURLScheme: "url-scheme",
universalLink: nil,
forMerchantID: nil,
accessToken: "access-token",
bundleDisplayName: "display-name",
Expand All @@ -47,9 +49,10 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {
func testUniversalLinkURL_whenAllValuesInitialized_returnsURLWithAllValues() {
do {
let requestURL = try BTVenmoAppSwitchRedirectURL(
returnURLScheme: "url-scheme",
paymentContextID: "12345",
metadata: BTClientMetadata(),
returnURLScheme: nil,
universalLink: URL(string: "https://mywebsite.com/braintree-payments"),
forMerchantID: "merchant-id",
accessToken: "access-token",
bundleDisplayName: "display-name",
Expand All @@ -60,9 +63,9 @@ class BTVenmoAppSwitchRedirectURL_Tests: XCTestCase {

let components = URLComponents(string: requestURL.universalLinksURL()!.absoluteString)
guard let queryItems = components?.queryItems else { XCTFail(); return }
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "url-scheme://x-callback-url/vzero/auth/venmo/success")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "url-scheme://x-callback-url/vzero/auth/venmo/error")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "url-scheme://x-callback-url/vzero/auth/venmo/cancel")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-success", value: "https://mywebsite.com/braintree-payments/success")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-error", value: "https://mywebsite.com/braintree-payments/error")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-cancel", value: "https://mywebsite.com/braintree-payments/cancel")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "x-source", value: "display-name")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_merchant_id", value: "merchant-id")))
XCTAssertTrue(queryItems.contains(URLQueryItem(name: "braintree_access_token", value: "access-token")))
Expand Down
Loading