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

Add BTVenmoError.canceled case #1087

Merged
merged 11 commits into from
Aug 10, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## unreleased
* BraintreeVenmo
* Add additional error parsing for Venmo errors
* Throw cancelation specific error for `BTVenmoClient.tokenize()` (fixes #1085)
jaxdesmarais marked this conversation as resolved.
Show resolved Hide resolved
* BraintreeCore
* Send `live` instead of `production` for the `merchant_sdk_env` tag to PayPal's analytics service (FPTI)

Expand Down
2 changes: 1 addition & 1 deletion Demo/Application/Base/Settings/Settings.bundle/Root.plist
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<string>BraintreeDemoPayPalCheckoutViewController</string>
<string>BraintreeDemoPayPalPayLaterViewController</string>
<string>BraintreeDemoCardTokenizationViewController</string>
<string>BraintreeDemoCustomVenmoButtonViewController</string>
<string>BraintreeDemoVenmoViewController</string>
<string>BraintreeDemoApplePayPassKitViewController</string>
<string>BraintreeDemoThreeDSecurePaymentFlowViewController</string>
<string>BraintreeDemoBTDataCollectorViewController</string>
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import UIKit
import BraintreeVenmo

class BraintreeDemoVenmoViewController: BraintreeDemoPaymentButtonBaseViewController {

var venmoClient: BTVenmoClient!

override func viewDidLoad() {
super.viewDidLoad()
venmoClient = BTVenmoClient(apiClient: apiClient)
title = "Custom Venmo Button"
}

override func createPaymentButton() -> UIView! {
let venmoButton = UIButton(type: .system)
venmoButton.setTitle("Venmo", for: .normal)
venmoButton.setTitleColor(.blue, for: .normal)
venmoButton.setTitleColor(.lightGray, for: .highlighted)
venmoButton.setTitleColor(.lightGray, for: .disabled)
venmoButton.addTarget(self, action: #selector(tappedVenmo), for: .touchUpInside)
venmoButton.translatesAutoresizingMaskIntoConstraints = false

let venmoECDButton = UIButton(type: .system)
venmoECDButton.setTitle("Venmo (with ECD options)", for: .normal)
venmoECDButton.setTitleColor(.blue, for: .normal)
venmoECDButton.setTitleColor(.lightGray, for: .highlighted)
venmoECDButton.setTitleColor(.lightGray, for: .disabled)
venmoECDButton.addTarget(self, action: #selector(tappedVenmoWithECD), for: .touchUpInside)
venmoECDButton.translatesAutoresizingMaskIntoConstraints = false

let stackView = UIStackView(arrangedSubviews: [venmoButton, venmoECDButton])
stackView.axis = .vertical
stackView.spacing = 5
stackView.alignment = .center
stackView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
venmoButton.topAnchor.constraint(equalTo: stackView.topAnchor),
venmoButton.heightAnchor.constraint(equalToConstant: 19.5),

venmoECDButton.topAnchor.constraint(equalTo: venmoButton.bottomAnchor, constant: 5),
venmoECDButton.heightAnchor.constraint(equalToConstant: 19.5)
])

return stackView
}

@objc func tappedVenmo() {
self.progressBlock("Tapped Venmo - initiating Venmo auth")

let venmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse)
venmoRequest.vault = true

checkout(request: venmoRequest)
}

@objc func tappedVenmoWithECD() {
self.progressBlock("Tapped Venmo ECD - initiating Venmo auth")

let venmoRequest = BTVenmoRequest(paymentMethodUsage: .multiUse)
venmoRequest.vault = true
venmoRequest.collectCustomerBillingAddress = true
venmoRequest.collectCustomerShippingAddress = true
venmoRequest.totalAmount = "30.00"
venmoRequest.taxAmount = "1.10"
venmoRequest.discountAmount = "1.10"
venmoRequest.shippingAmount = "0.00"

let lineItem = BTVenmoLineItem(quantity: 1, unitAmount: "30.00", name: "item-1", kind: .debit)
lineItem.unitTaxAmount = "1.00"
venmoRequest.lineItems = [lineItem]

checkout(request: venmoRequest)
}

func checkout(request: BTVenmoRequest) {
Task {
do {
let venmoAccount = try await venmoClient.tokenize(request)
progressBlock("Got a nonce 💎!")
completionBlock(venmoAccount)
} catch {
if (error as NSError).code == 10 {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I know we can reference the error codes markdown, but this code would read better if there was a static constant used in place of 10.

Copy link
Contributor Author

@scannillo scannillo Aug 2, 2023

Choose a reason for hiding this comment

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

That's a good call. And on top of that, I think it should be easier for a merchant to check against our error codes (a merchant asked about it in #1080) versus needing them to check our SDK_ERROR_CODES.md file list.

Something like : if error.code == BTVenmoError.cancel.code { // do things } would be nice to expose for merchants.

@jaxdesmarais What do you think? Should we ticket this work?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think I would still lean towards exposing the errors as a whole publicly for merchants using Swift. Since many of our errors use associated types, we can't expose those to Obj-C publicly as only Int type enums can be exposed.

If we wanted to expose them publicly for Obj-C merchants we would need to move the error codes into their own enum without associated types and construct the error messages at the callsite or in a method where we can include the details we are currently including with the associated type when constructing the localizedDescription.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeh - there are ways to do it without having to move the error creation to the callsite, though it makes our internal <Feature>Error.swift files a little uglier. Something like this:

enum BTAmericanExpressError: Int, Error, CustomNSError, LocalizedError {

    case unknown

    case noRewardsData

    case deallocated

    static var errorDomain: String {
        "com.braintreepayments.BTAmericanExpressErrorDomain"
    }
    
    var errorCode: Int {
        switch self {
        case .unknown:
            return BTAmericanExpressErrorCode.unknown.rawValue
        case .noRewardsData:
            return BTAmericanExpressErrorCode.noRewardsData.rawValue
        case .deallocated:
            return BTAmericanExpressErrorCode.deallocated.rawValue
        }
    }
    
    var errorDescription: String? {
        switch self {
        case .unknown:
            return "An unknown error occurred. Please contact support."

        case .noRewardsData:
            return "No American Express Rewards data was returned. Please contact support."

        case .deallocated:
            return "BTAmericanExpressClient has been deallocated."
        }
    }
}


/// Public for merchants to reference (accessible in both Swift & ObjC)
@objc public enum BTAmericanExpressErrorCode: Int {
    
    case unknown = 0
    case noRewardsData = 1
    case deallocated = 2
}

There might even be a slicker way to do that switch statement, but something like this would solve the issue

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, yeah I hadn't thought about just extracting the code portion.

Yeah, I guess the question in that case would be the merchant experience. With the above the callsite would look something like:

if (error as? NSError)?.code == BTAmericanExpressErrorCode.noRewardsData.rawValue {
    // do something for this specific error
}

vs making the enum public the callsite could look something like:

switch error as? BTAmericanExpressError {
case . noRewardsData:
    // do something for this specific error
default:
    // do something else for other errors
}

I think you could technically use a switch for the first but it'd be something like switch ((error as? NSError)?.code as? BTJSONErrorCode). It really comes down to how we want to support Obj-C vs Swift first in the future, which is up to us! We can certainly play around with some other options and see what feels best or engage the merchant that opened the issue to see what their ideal world would be.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thats a great point. I added this convo to our "iOS Minor Version Improvements" doc!

progressBlock("Canceled 🔰")
} else {
progressBlock(error.localizedDescription)
}
}
}
}
}
12 changes: 5 additions & 7 deletions Demo/Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 52;
objectVersion = 54;
objects = {

/* Begin PBXBuildFile section */
Expand Down Expand Up @@ -40,6 +40,7 @@
803D6508256DAF9A00ACE692 /* BraintreeVenmo.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 803D64F3256DAF9A00ACE692 /* BraintreeVenmo.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
803FB9DE26D93146002BF92D /* BTCardFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803FB9DD26D93146002BF92D /* BTCardFormView.swift */; };
80581A5F2553170000006F53 /* Venmo_UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80581A5D2553152A00006F53 /* Venmo_UITests.swift */; };
80D36DEE2A7967F20035380E /* BraintreeDemoVenmoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80D36DED2A7967F20035380E /* BraintreeDemoVenmoViewController.swift */; };
9C36BD2926B3071B00F0A559 /* PPRiskMagnes.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C36BD2826B3071A00F0A559 /* PPRiskMagnes.xcframework */; };
9C36BD4C26B311D900F0A559 /* CardinalMobile.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9C36BD4826B3102B00F0A559 /* CardinalMobile.xcframework */; };
9C36BD4D26B311D900F0A559 /* CardinalMobile.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9C36BD4826B3102B00F0A559 /* CardinalMobile.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand All @@ -60,7 +61,6 @@
A0988FF024DB44B20095EEEE /* BraintreeDemoApplePayPassKitViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A0988F7424DB44B10095EEEE /* BraintreeDemoApplePayPassKitViewController.m */; };
A0988FF224DB44B20095EEEE /* BraintreeDemoThreeDSecurePaymentFlowViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A0988F7A24DB44B10095EEEE /* BraintreeDemoThreeDSecurePaymentFlowViewController.m */; };
A0988FF424DB44B20095EEEE /* BraintreeDemoAmexViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A0988F7F24DB44B10095EEEE /* BraintreeDemoAmexViewController.m */; };
A0988FF524DB44B20095EEEE /* BraintreeDemoCustomVenmoButtonViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = A0988F8324DB44B10095EEEE /* BraintreeDemoCustomVenmoButtonViewController.m */; };
A0988FF924DB44B20095EEEE /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A0988F8D24DB44B10095EEEE /* Images.xcassets */; };
A0988FFA24DB44B20095EEEE /* BraintreeDemoMerchantAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0988F9024DB44B20095EEEE /* BraintreeDemoMerchantAPIClient.swift */; };
A9C4E07924EC28F7002F6FF2 /* PayPal_Checkout_UITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C4E07824EC28F7002F6FF2 /* PayPal_Checkout_UITests.swift */; };
Expand Down Expand Up @@ -154,6 +154,7 @@
803D64F3256DAF9A00ACE692 /* BraintreeVenmo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = BraintreeVenmo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
803FB9DD26D93146002BF92D /* BTCardFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCardFormView.swift; sourceTree = "<group>"; };
80581A5D2553152A00006F53 /* Venmo_UITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Venmo_UITests.swift; sourceTree = "<group>"; };
80D36DED2A7967F20035380E /* BraintreeDemoVenmoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BraintreeDemoVenmoViewController.swift; sourceTree = "<group>"; };
9C030582267BDF9E00DB7A68 /* Demo.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Demo.entitlements; sourceTree = "<group>"; };
9C36BD2826B3071A00F0A559 /* PPRiskMagnes.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = PPRiskMagnes.xcframework; path = ../Frameworks/XCFrameworks/PPRiskMagnes.xcframework; sourceTree = "<group>"; };
9C36BD4826B3102B00F0A559 /* CardinalMobile.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CardinalMobile.xcframework; path = ../Frameworks/XCFrameworks/CardinalMobile.xcframework; sourceTree = "<group>"; };
Expand Down Expand Up @@ -188,8 +189,6 @@
A0988F7A24DB44B10095EEEE /* BraintreeDemoThreeDSecurePaymentFlowViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BraintreeDemoThreeDSecurePaymentFlowViewController.m; sourceTree = "<group>"; };
A0988F7F24DB44B10095EEEE /* BraintreeDemoAmexViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BraintreeDemoAmexViewController.m; sourceTree = "<group>"; };
A0988F8024DB44B10095EEEE /* BraintreeDemoAmexViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BraintreeDemoAmexViewController.h; sourceTree = "<group>"; };
A0988F8224DB44B10095EEEE /* BraintreeDemoCustomVenmoButtonViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BraintreeDemoCustomVenmoButtonViewController.h; sourceTree = "<group>"; };
A0988F8324DB44B10095EEEE /* BraintreeDemoCustomVenmoButtonViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BraintreeDemoCustomVenmoButtonViewController.m; sourceTree = "<group>"; };
A0988F8D24DB44B10095EEEE /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = "<group>"; };
A0988F8E24DB44B10095EEEE /* Braintree-Demo.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = "Braintree-Demo.entitlements"; sourceTree = "<group>"; };
A0988F9024DB44B20095EEEE /* BraintreeDemoMerchantAPIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BraintreeDemoMerchantAPIClient.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -481,8 +480,7 @@
A0988F8124DB44B10095EEEE /* Venmo - Custom Button */ = {
isa = PBXGroup;
children = (
A0988F8224DB44B10095EEEE /* BraintreeDemoCustomVenmoButtonViewController.h */,
A0988F8324DB44B10095EEEE /* BraintreeDemoCustomVenmoButtonViewController.m */,
80D36DED2A7967F20035380E /* BraintreeDemoVenmoViewController.swift */,
);
path = "Venmo - Custom Button";
sourceTree = "<group>";
Expand Down Expand Up @@ -816,11 +814,11 @@
A0988FE824DB44B20095EEEE /* BraintreeDemoBTDataCollectorViewController.m in Sources */,
A0988FF224DB44B20095EEEE /* BraintreeDemoThreeDSecurePaymentFlowViewController.m in Sources */,
A0988F9424DB44B20095EEEE /* BraintreeDemoPaymentButtonBaseViewController.m in Sources */,
80D36DEE2A7967F20035380E /* BraintreeDemoVenmoViewController.swift in Sources */,
A0988F9624DB44B20095EEEE /* BraintreeDemoAppDelegate.m in Sources */,
BEAAAD052970A70D000BD296 /* BTSEPADirectDebitTestHelper.swift in Sources */,
A0988FF024DB44B20095EEEE /* BraintreeDemoApplePayPassKitViewController.m in Sources */,
57108A152832E789004EB870 /* BraintreeDemoPayPalNativeCheckoutViewController.swift in Sources */,
A0988FF524DB44B20095EEEE /* BraintreeDemoCustomVenmoButtonViewController.m in Sources */,
BE4F788E27EE394600FF4C0E /* BraintreeDemoPayPalCheckoutViewController.swift in Sources */,
A0988F9224DB44B20095EEEE /* BraintreeDemoSettings.swift in Sources */,
);
Expand Down
25 changes: 21 additions & 4 deletions Demo/UI Tests/Venmo UI Tests/Venmo_UITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,52 @@ class Venmo_UITests: XCTestCase {
demoApp = XCUIApplication(bundleIdentifier: "com.braintreepayments.Demo")
demoApp.launchArguments.append("-EnvironmentSandbox")
demoApp.launchArguments.append("-ClientToken")
demoApp.launchArguments.append("-Integration:BraintreeDemoCustomVenmoButtonViewController")
demoApp.launchArguments.append("-Integration:BraintreeDemoVenmoViewController")
demoApp.launch()

waitForElementToBeHittable(demoApp.buttons["Venmo (custom button)"])
demoApp.buttons["Venmo (custom button)"].tap()
waitForElementToBeHittable(demoApp.buttons["Venmo"])
waitForElementToBeHittable(demoApp.buttons["Venmo (with ECD options)"])
}

func testTokenizeVenmo_whenSignInSuccessfulWithPaymentContext_returnsNonce() {
demoApp.buttons["Venmo"].tap()

waitForElementToBeHittable(mockVenmo.buttons["SUCCESS WITH PAYMENT CONTEXT"])
mockVenmo.buttons["SUCCESS WITH PAYMENT CONTEXT"].tap()

XCTAssertTrue(demoApp.buttons["Got a nonce. Tap to make a transaction."].waitForExistence(timeout: 15))
}

func testTokenizeVenmo_withECDOptions_whenSignInSuccessfulWithPaymentContext_returnsNonce() {
demoApp.buttons["Venmo (with ECD options)"].tap()

waitForElementToBeHittable(mockVenmo.buttons["SUCCESS WITH PAYMENT CONTEXT"])
mockVenmo.buttons["SUCCESS WITH PAYMENT CONTEXT"].tap()

XCTAssertTrue(demoApp.buttons["Got a nonce. Tap to make a transaction."].waitForExistence(timeout: 15))
}

func testTokenizeVenmo_whenSignInSuccessfulWithoutPaymentContext_returnsNonce() {
demoApp.buttons["Venmo"].tap()

waitForElementToBeHittable(mockVenmo.buttons["SUCCESS WITHOUT PAYMENT CONTEXT"])
mockVenmo.buttons["SUCCESS WITHOUT PAYMENT CONTEXT"].tap()

XCTAssertTrue(demoApp.buttons["Got a nonce. Tap to make a transaction."].waitForExistence(timeout: 15))
}

func testTokenizeVenmo_whenErrorOccurs_returnsError() {
demoApp.buttons["Venmo"].tap()

waitForElementToBeHittable(mockVenmo.buttons["ERROR"])
mockVenmo.buttons["ERROR"].tap()

XCTAssertTrue(demoApp.buttons["An error occurred during the Venmo flow"].waitForExistence(timeout: 15))
}

func testTokenizeVenmo_whenUserCancels_returnsCancel() {
demoApp.buttons["Venmo"].tap()

waitForElementToBeHittable(mockVenmo.buttons["Cancel"])
mockVenmo.buttons["Cancel"].tap()

Expand Down
1 change: 1 addition & 0 deletions SDK_ERROR_CODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,3 +136,4 @@ See below for a comprehensive list of error types and codes thrown by each featu
| BTVenmoError.invalidRedirectURL | 7 |
| BTVenmoError.fetchConfigurationFailed | 8 |
| BTVenmoError.enrichedCustomerDataDisabled | 9 |
| BTVenmoError.canceled | 10 |
5 changes: 3 additions & 2 deletions Sources/BraintreeVenmo/BTVenmoClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ import BraintreeCore
/// - Parameters:
/// - request: A Venmo request.
/// - completion: This completion will be invoked when app switch is complete or an error occurs. On success, you will receive
/// an instance of `BTVenmoAccountNonce`; on failure, an error; on user cancellation, you will receive `nil` for both parameters.
/// 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:)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The docs for the async method also needs this update!

public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) {
apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted)
Expand Down Expand Up @@ -415,7 +416,7 @@ import BraintreeCore

private func notifyCancel(completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) {
apiClient.sendAnalyticsEvent(BTVenmoAnalytics.appSwitchCanceled)
completion(nil, nil)
completion(nil, BTVenmoError.canceled)
}
}

Expand Down
7 changes: 7 additions & 0 deletions Sources/BraintreeVenmo/BTVenmoError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ enum BTVenmoError: Error, CustomNSError, LocalizedError {

/// 9. Enriched Customer Data is disabled
case enrichedCustomerDataDisabled

/// 10. The Venmo flow was canceled by the user
case canceled
Copy link
Contributor

Choose a reason for hiding this comment

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

q: is this a breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, since we are only adding an error code & not changing existing ones we didn't think this was breaking


static var errorDomain: String {
"com.braintreepayments.BTVenmoErrorDomain"
Expand Down Expand Up @@ -84,6 +87,8 @@ enum BTVenmoError: Error, CustomNSError, LocalizedError {
return 8
case .enrichedCustomerDataDisabled:
return 9
case .canceled:
return 10
}
}

Expand All @@ -109,6 +114,8 @@ enum BTVenmoError: Error, CustomNSError, LocalizedError {
return "Failed to fetch Braintree configuration."
case .enrichedCustomerDataDisabled:
return "Cannot collect customer data when ECD is disabled. Enable this feature in the Control Panel to collect this data."
case .canceled:
return "Venmo flow was canceled by the user."
}
}
}
Loading
Loading