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 all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
* Add `BTPayPalRequest.userPhoneNumber` optional property
* BraintreeVenmo
* Send `url` in `event_params` for App Switch events to PayPal's analytics service (FPTI)
* Add `BTVenmoClient(apiClient:universalLink:)` to use Universal Links when redirecting back from the Venmo flow
* BraintreeCore
* Deprecate `BTAppContextSwitcher.sharedInstance.returnURLScheme`

## 6.24.0 (2024-10-15)
* BraintreePayPal
Expand Down
18 changes: 14 additions & 4 deletions Demo/Application/Features/VenmoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import UIKit
import BraintreeVenmo

class VenmoViewController: PaymentButtonBaseViewController {

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

let webFallbackToggle = Toggle(title: "Enable Web Fallback")
let vaultToggle = Toggle(title: "Vault")

let universalLinkReturnToggle = Toggle(title: "Use Universal Link Return")

override func viewDidLoad() {
super.heightConstraint = 150
super.viewDidLoad()
venmoClient = BTVenmoClient(apiClient: apiClient)
title = "Custom Venmo Button"
Expand All @@ -18,7 +20,7 @@ class VenmoViewController: PaymentButtonBaseViewController {
override func createPaymentButton() -> UIView {
let venmoButton = createButton(title: "Venmo", action: #selector(tappedVenmo))

let stackView = UIStackView(arrangedSubviews: [webFallbackToggle, vaultToggle, venmoButton])
let stackView = UIStackView(arrangedSubviews: [webFallbackToggle, vaultToggle, universalLinkReturnToggle, venmoButton])
stackView.axis = .vertical
stackView.spacing = 15
stackView.alignment = .fill
Expand All @@ -40,7 +42,15 @@ class VenmoViewController: PaymentButtonBaseViewController {
if vaultToggle.isOn {
venmoRequest.vault = true
}


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(venmoRequest)
Expand Down
17 changes: 16 additions & 1 deletion Sources/BraintreeCore/BTAppContextSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,22 @@ 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.
/// - Note: This property should only be used for the Venmo flow.
public var returnURLScheme: String = ""
@available(
*,
deprecated,
message: "returnURLScheme is deprecated and will be removed in a future version. Use BTVenmoClient(apiClient:universalLink:)."
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
)
public var returnURLScheme: String {
get { _returnURLScheme }
set { _returnURLScheme = newValue }
}

// swiftlint:disable identifier_name
/// :nodoc: This method is exposed for internal Braintree use only. Do not use. It is not covered by Semantic Versioning and may change or be removed at any time.
/// Property for `returnURLScheme`. Created to avoid deprecation warnings upon accessing
/// `returnURLScheme` directly within our SDK. Use this value instead.
public var _returnURLScheme: String = ""
// swiftlint:enable identifier_name

// MARK: - Private Properties

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.appendingPathComponent("success").absoluteString
queryParameters["x-error"] = universalLink.appendingPathComponent("error").absoluteString
queryParameters["x-cancel"] = universalLink.appendingPathComponent("cancel").absoluteString
} 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/"))
}
}
19 changes: 16 additions & 3 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 a Venmo client
/// - 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 All @@ -69,7 +81,7 @@ import BraintreeCore
public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) {
linkType = request.fallbackToWeb ? .universal : .deeplink
apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted, isVaultRequest: shouldVault, linkType: linkType)
let returnURLScheme = BTAppContextSwitcher.sharedInstance.returnURLScheme
let returnURLScheme = BTAppContextSwitcher.sharedInstance._returnURLScheme

if returnURLScheme.isEmpty {
NSLog(
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