Skip to content

Commit

Permalink
[v7] Update Venmo to Support Only Universal Links (#1489)
Browse files Browse the repository at this point in the history
* Remove BTAppContextSwitcher.sharedInstance.returnURLScheme
* Remove BTVenmoClient(apiClient:) init in favor of universalLink init
* Cleanup demo app
* Remove logic around returnURLScheme in the Venmo flow since this is no longer needed
* Remove no longer needed error case
* Cleanup unit tests based on updated logic
  • Loading branch information
jaxdesmarais authored Dec 20, 2024
1 parent 12318d6 commit 830a48f
Show file tree
Hide file tree
Showing 18 changed files with 115 additions and 273 deletions.
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
}
}
8 changes: 6 additions & 2 deletions Demo/Application/Features/ShopperInsightsViewController.swift
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
31 changes: 14 additions & 17 deletions Demo/Application/Features/VenmoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,31 @@ 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.spacing = 12
venmoStackView.translatesAutoresizingMaskIntoConstraints = false

return stackView
return venmoStackView
}

@objc func tappedVenmo() {
Expand All @@ -38,14 +43,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

0 comments on commit 830a48f

Please sign in to comment.