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

[v7] Update Venmo to Support Only Universal Links #1489

Merged
merged 12 commits into from
Dec 20, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* BraintreeVenmo
* Update `BTVenmoRequest` to make all properties accessible on the initializer only vs via the dot syntax.
* Remove `fallbacktoWeb` property from `BTVenmoRequest`. All Venmo flows will now use universal links to switch to the Venmo app or fallback to the web flow if the Venmo app is not installed
* Remove `BTAppContextSwitcher.sharedInstance.returnURLScheme`
* `BTVenmoClient` initializer now requires a `universalLink` for switching to and from the Venmo app or web fallback flow
* BraintreeSEPADirectDebit
* Update `BTSEPADirectDebitRequest` to make all properties accessible on the initializer only vs via the dot syntax.
* BraintreeLocalPayment
Expand Down
2 changes: 0 additions & 2 deletions Demo/Application/Base/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import BraintreeCore

@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {

private let returnURLScheme = "com.braintreepayments.Demo.payments"
private let processInfoArgs = ProcessInfo.processInfo.arguments
private let userDefaults = UserDefaults.standard

Expand All @@ -13,7 +12,6 @@ import BraintreeCore
) -> Bool {
registerDefaultsFromSettings()
persistDemoSettings()
BTAppContextSwitcher.sharedInstance.returnURLScheme = returnURLScheme

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

Expand Down
16 changes: 16 additions & 0 deletions Demo/Application/Base/PaymentButtonBaseViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,20 @@ class PaymentButtonBaseViewController: BaseViewController {
button.addTarget(self, action: action, for: .touchUpInside)
return button
}

// MARK: - Helpers

func buttonsStackView(label: String, views: [UIView]) -> UIStackView {
let titleLabel = UILabel()
titleLabel.text = label

let buttonsStackView = UIStackView(arrangedSubviews: [titleLabel] + views)
buttonsStackView.axis = .vertical
buttonsStackView.distribution = .fillProportionally
buttonsStackView.backgroundColor = .systemGray6
buttonsStackView.layoutMargins = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
buttonsStackView.isLayoutMarginsRelativeArrangement = true

return buttonsStackView
}
}
16 changes: 0 additions & 16 deletions Demo/Application/Features/PayPalWebCheckoutViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,20 +250,4 @@ class PayPalWebCheckoutViewController: PaymentButtonBaseViewController {
self.completionBlock(nonce)
}
}

// MARK: - Helpers

private func buttonsStackView(label: String, views: [UIView]) -> UIStackView {
let titleLabel = UILabel()
titleLabel.text = label

let buttonsStackView = UIStackView(arrangedSubviews: [titleLabel] + views)
buttonsStackView.axis = .vertical
buttonsStackView.distribution = .fillProportionally
buttonsStackView.backgroundColor = .systemGray6
buttonsStackView.layoutMargins = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
buttonsStackView.isLayoutMarginsRelativeArrangement = true

return buttonsStackView
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ class ShopperInsightsViewController: PaymentButtonBaseViewController {

lazy var shopperInsightsClient = BTShopperInsightsClient(apiClient: apiClient)
lazy var payPalClient = BTPayPalClient(apiClient: apiClient)
lazy var venmoClient = BTVenmoClient(apiClient: apiClient)

lazy var venmoClient = BTVenmoClient(
apiClient: apiClient,
// swiftlint:disable:next force_unwrapping
universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")!
)

lazy var payPalVaultButton = createButton(title: "PayPal Vault", action: #selector(payPalVaultButtonTapped))
lazy var venmoButton = createButton(title: "Venmo", action: #selector(venmoButtonTapped))

Expand Down
33 changes: 16 additions & 17 deletions Demo/Application/Features/VenmoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,33 @@ class VenmoViewController: PaymentButtonBaseViewController {
var venmoClient: BTVenmoClient!

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)
venmoClient = BTVenmoClient(
apiClient: apiClient,
// swiftlint:disable:next force_unwrapping
universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")!
)

title = "Custom Venmo Button"
}

override func createPaymentButton() -> UIView {
let venmoButton = createButton(title: "Venmo", action: #selector(tappedVenmo))

let stackView = UIStackView(arrangedSubviews: [vaultToggle, universalLinkReturnToggle, venmoButton])
stackView.axis = .vertical
stackView.spacing = 15
stackView.alignment = .fill
stackView.distribution = .fillEqually
stackView.translatesAutoresizingMaskIntoConstraints = false
let venmoStackView = buttonsStackView(label: "Venmo Payment Flow", views: [
vaultToggle,
venmoButton
])

venmoStackView.axis = .vertical
venmoStackView.distribution = .fillProportionally
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
venmoStackView.spacing = 12
venmoStackView.translatesAutoresizingMaskIntoConstraints = false

return stackView
return venmoStackView
}

@objc func tappedVenmo() {
Expand All @@ -38,14 +45,6 @@ class VenmoViewController: PaymentButtonBaseViewController {
vault: isVaultingEnabled
)

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
5 changes: 4 additions & 1 deletion SampleApps/CarthageTest/CarthageTest/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class ViewController: UIViewController {
let payPalClient = BTPayPalClient(apiClient: apiClient)
let payPalMessagingView = BTPayPalMessagingView(apiClient: apiClient)
let threeDSecureClient = BTThreeDSecureClient(apiClient: apiClient)
let venmoClient = BTVenmoClient(apiClient: apiClient)
let venmoClient = BTVenmoClient(
apiClient: apiClient,
universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")!
)
let sepaDirectDebitClient = BTSEPADirectDebitClient(apiClient: apiClient)
}
}
Expand Down
5 changes: 4 additions & 1 deletion SampleApps/SPMTest/SPMTest/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class ViewController: UIViewController {
let payPalClient = BTPayPalClient(apiClient: apiClient)
let payPalMessagingView = BTPayPalMessagingView(apiClient: apiClient)
let threeDSecureClient = BTThreeDSecureClient(apiClient: apiClient)
let venmoClient = BTVenmoClient(apiClient: apiClient)
let venmoClient = BTVenmoClient(
apiClient: apiClient,
universalLink: URL(string: "https://mobile-sdk-demo-site-838cead5d3ab.herokuapp.com/braintree-payments")!
)
let sepaDirectDebitClient = BTSEPADirectDebitClient(apiClient: apiClient)
}
}
21 changes: 0 additions & 21 deletions Sources/BraintreeCore/BTAppContextSwitcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,6 @@ import UIKit

/// Singleton for shared instance of `BTAppContextSwitcher`
public static let sharedInstance = BTAppContextSwitcher()

// NEXT_MAJOR_VERSION: move this property into the feature client request where it is used
/// 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.
@available(
*,
deprecated,
message: "returnURLScheme is deprecated and will be removed in a future version. Use BTVenmoClient(apiClient:universalLink:)."
)
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
43 changes: 5 additions & 38 deletions Sources/BraintreeVenmo/BTVenmoAppSwitchRedirectURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,16 @@ import BraintreeCore

struct BTVenmoAppSwitchRedirectURL {

// MARK: - Internal Properties

/// The base app switch URL for Venmo. Does not include specific parameters.
static var baseAppSwitchURL: URL? {
appSwitchBaseURLComponents().url
}

// MARK: - Private Properties

static private let xCallbackTemplate: String = "scheme://x-callback-url/path"

private var queryParameters: [String: Any?] = [:]

// MARK: - Initializer

init(
paymentContextID: String,
metadata: BTClientMetadata,
returnURLScheme: String?,
universalLink: URL?,
universalLink: URL,
forMerchantID merchantID: String?,
accessToken: String?,
bundleDisplayName: String?,
Expand Down Expand Up @@ -53,18 +43,11 @@ struct BTVenmoAppSwitchRedirectURL {
"braintree_environment": environment,
"resource_id": paymentContextID,
"braintree_sdk_data": base64EncodedBraintreeData ?? "",
"customerClient": "MOBILE_APP"
"customerClient": "MOBILE_APP",
"x-success": universalLink.appendingPathComponent("success").absoluteString,
"x-error": universalLink.appendingPathComponent("error").absoluteString,
"x-cancel": universalLink.appendingPathComponent("cancel").absoluteString
]

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 All @@ -82,20 +65,4 @@ struct BTVenmoAppSwitchRedirectURL {

return urlComponent.url
}

// MARK: - Private Helper Methods

private func constructRedirectURL(with scheme: String, result: String) -> URL? {
var components = URLComponents(string: BTVenmoAppSwitchRedirectURL.xCallbackTemplate)
components?.scheme = scheme
components?.percentEncodedPath = "/vzero/auth/venmo/\(result)"
return components?.url
}

private static func appSwitchBaseURLComponents() -> URLComponents {
var components: URLComponents = URLComponents(string: xCallbackTemplate) ?? URLComponents()
components.scheme = BTCoreConstants.venmoURLScheme
components.percentEncodedPath = "/vzero/auth"
return components
}
}
1 change: 0 additions & 1 deletion Sources/BraintreeVenmo/BTVenmoAppSwitchReturnURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,5 @@ struct BTVenmoAppSwitchReturnURL {
/// - Returns: `true` if the url represents a Venmo Touch app switch return
static func isValid(url: URL) -> Bool {
(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/"))
}
}
53 changes: 14 additions & 39 deletions Sources/BraintreeVenmo/BTVenmoClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ import BraintreeCore
/// Stored property used to determine whether a Venmo account nonce should be vaulted after an app switch return
var shouldVault: Bool = false

/// Used for linking events from the client to server side request
/// In the Venmo flow this will be the payment context ID
private var payPalContextID: String?

/// Used for sending the type of flow, universal vs deeplink to FPTI
private var linkType: LinkType?

private var universalLink: URL

/// Used internally as a holder for the completion in methods that do not pass a completion such as `handleOpen`.
/// This allows us to set and return a completion in our methods that otherwise cannot require a completion.
var appSwitchCompletion: (BTVenmoAccountNonce?, Error?) -> Void = { _, _ in }
Expand All @@ -39,32 +48,17 @@ import BraintreeCore
/// We require a static reference of the client to call `handleReturnURL` and return to the app.
static var venmoClient: BTVenmoClient?

/// Used for linking events from the client to server side request
/// In the Venmo flow this will be the payment context ID
private var payPalContextID: String?

/// Used for sending the type of flow, universal vs deeplink to FPTI
private var linkType: LinkType?

private var universalLink: URL?

// MARK: - Initializer

/// 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)
public init(apiClient: BTAPIClient, universalLink: URL) {
BTAppContextSwitcher.sharedInstance.register(BTVenmoClient.self)

self.apiClient = apiClient
self.universalLink = universalLink
}

Expand All @@ -77,27 +71,9 @@ import BraintreeCore
/// an instance of `BTVenmoAccountNonce`; on failure or user cancelation you will receive an error.
/// If the user cancels out of the flow, the error code will be `.canceled`.
@objc(tokenizeWithVenmoRequest:completion:)
// swiftlint:disable:next function_body_length cyclomatic_complexity
// swiftlint:disable:next function_body_length
public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) {
apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted, isVaultRequest: shouldVault)
let returnURLScheme = BTAppContextSwitcher.sharedInstance._returnURLScheme

if returnURLScheme.isEmpty {
NSLog(
"%@ Venmo requires a return URL scheme to be configured via [BTAppContextSwitcher setReturnURLScheme:]",
BTLogLevelDescription.string(for: .critical)
)
notifyFailure(with: BTVenmoError.appNotAvailable, completion: completion)
return
} else if let bundleIdentifier = bundle.bundleIdentifier, !returnURLScheme.hasPrefix(bundleIdentifier) {
NSLog(
// swiftlint:disable:next line_length
"%@ Venmo requires [BTAppContextSwitcher setReturnURLScheme:] to be configured to begin with your app's bundle ID (%@). Currently, it is set to (%@)",
BTLogLevelDescription.string(for: .critical),
bundleIdentifier,
returnURLScheme
)
}

apiClient.fetchOrReturnRemoteConfiguration { configuration, error in
if let error {
Expand Down Expand Up @@ -164,7 +140,6 @@ import BraintreeCore
let appSwitchURL = try BTVenmoAppSwitchRedirectURL(
paymentContextID: paymentContextID,
metadata: metadata,
returnURLScheme: returnURLScheme,
universalLink: self.universalLink,
forMerchantID: merchantProfileID,
accessToken: configuration.venmoAccessToken,
Expand Down
Loading