diff --git a/CHANGELOG.md b/CHANGELOG.md index 764aeadd62..97abb5cfd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## unreleased * BraintreeVenmo * Add additional error parsing for Venmo errors + * Throw cancelation specific error for `BTVenmoClient.tokenize()` (fixes #1085) + * _The callback style version of this function previously returned `(nil, nil)` for the cancel scenario, but will now return `(nil, error)` instead._ * BraintreeCore * Send `live` instead of `production` for the `merchant_sdk_env` tag to PayPal's analytics service (FPTI) diff --git a/Demo/Application/Features/Venmo - Custom Button/BraintreeDemoVenmoViewController.swift b/Demo/Application/Features/Venmo - Custom Button/BraintreeDemoVenmoViewController.swift index 01f7dd0055..2f89c4a6c3 100644 --- a/Demo/Application/Features/Venmo - Custom Button/BraintreeDemoVenmoViewController.swift +++ b/Demo/Application/Features/Venmo - Custom Button/BraintreeDemoVenmoViewController.swift @@ -80,7 +80,11 @@ class BraintreeDemoVenmoViewController: BraintreeDemoPaymentButtonBaseViewContro progressBlock("Got a nonce 💎!") completionBlock(venmoAccount) } catch { - progressBlock(error.localizedDescription) + if (error as NSError).code == 10 { + progressBlock("Canceled 🔰") + } else { + progressBlock(error.localizedDescription) + } } } } diff --git a/Demo/Demo.xcodeproj/project.pbxproj b/Demo/Demo.xcodeproj/project.pbxproj index 57223dc442..189116131e 100644 --- a/Demo/Demo.xcodeproj/project.pbxproj +++ b/Demo/Demo.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/Demo/UI Tests/Venmo UI Tests/Venmo_UITests.swift b/Demo/UI Tests/Venmo UI Tests/Venmo_UITests.swift index fc327dde07..3953618f5f 100644 --- a/Demo/UI Tests/Venmo UI Tests/Venmo_UITests.swift +++ b/Demo/UI Tests/Venmo UI Tests/Venmo_UITests.swift @@ -58,13 +58,12 @@ class Venmo_UITests: XCTestCase { XCTAssertTrue(demoApp.buttons["An error occurred during the Venmo flow"].waitForExistence(timeout: 15)) } - // TODO: - Uncomment test once cancel case is handled as an error -// func testTokenizeVenmo_whenUserCancels_returnsCancel() { -// demoApp.buttons["Venmo"].tap() -// -// waitForElementToBeHittable(mockVenmo.buttons["Cancel"]) -// mockVenmo.buttons["Cancel"].tap() -// -// XCTAssertTrue(demoApp.buttons["Canceled 🔰"].waitForExistence(timeout: 15)) -// } + func testTokenizeVenmo_whenUserCancels_returnsCancel() { + demoApp.buttons["Venmo"].tap() + + waitForElementToBeHittable(mockVenmo.buttons["Cancel"]) + mockVenmo.buttons["Cancel"].tap() + + XCTAssertTrue(demoApp.buttons["Canceled 🔰"].waitForExistence(timeout: 15)) + } } diff --git a/SDK_ERROR_CODES.md b/SDK_ERROR_CODES.md index 5f18efc090..4a59bf9baa 100644 --- a/SDK_ERROR_CODES.md +++ b/SDK_ERROR_CODES.md @@ -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 | \ No newline at end of file diff --git a/Sources/BraintreeVenmo/BTVenmoClient.swift b/Sources/BraintreeVenmo/BTVenmoClient.swift index 4492dd6e1c..4b3555e54b 100644 --- a/Sources/BraintreeVenmo/BTVenmoClient.swift +++ b/Sources/BraintreeVenmo/BTVenmoClient.swift @@ -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:) public func tokenize(_ request: BTVenmoRequest, completion: @escaping (BTVenmoAccountNonce?, Error?) -> Void) { apiClient.sendAnalyticsEvent(BTVenmoAnalytics.tokenizeStarted) @@ -205,7 +206,7 @@ import BraintreeCore /// Initiates Venmo login via app switch, which returns a BTVenmoAccountNonce when successful. /// - Parameter request: A `BTVenmoRequest` /// - Returns: On success, you will receive an instance of `BTVenmoAccountNonce` - /// - Throws: An `Error` describing the failure + /// - Throws: An `Error` describing the failure. If the user cancels out of the flow, the error code will be `.canceled`. public func tokenize(_ request: BTVenmoRequest) async throws -> BTVenmoAccountNonce { try await withCheckedThrowingContinuation { continuation in tokenize(request) { nonce, error in @@ -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) } } diff --git a/Sources/BraintreeVenmo/BTVenmoError.swift b/Sources/BraintreeVenmo/BTVenmoError.swift index 44ccece9d3..485dfeebcf 100644 --- a/Sources/BraintreeVenmo/BTVenmoError.swift +++ b/Sources/BraintreeVenmo/BTVenmoError.swift @@ -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 static var errorDomain: String { "com.braintreepayments.BTVenmoErrorDomain" @@ -84,6 +87,8 @@ enum BTVenmoError: Error, CustomNSError, LocalizedError { return 8 case .enrichedCustomerDataDisabled: return 9 + case .canceled: + return 10 } } @@ -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." } } } diff --git a/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift b/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift index c943946e18..b95e3e81ec 100644 --- a/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift +++ b/UnitTests/BraintreeVenmoTests/BTVenmoClient_Tests.swift @@ -613,7 +613,7 @@ class BTVenmoClient_Tests: XCTestCase { XCTAssertEqual(mockAPIClient.postedAnalyticsEvents.last!, BTVenmoAnalytics.tokenizeFailed) } - func testTokenizeVenmoAccount_whenAppSwitchCanceled_callsBackWithNoError() { + func testTokenizeVenmoAccount_whenAppSwitchCanceled_callsBackWithCancelError() { let venmoClient = BTVenmoClient(apiClient: mockAPIClient) venmoClient.application = FakeApplication() venmoClient.bundle = FakeBundle() @@ -622,7 +622,12 @@ class BTVenmoClient_Tests: XCTestCase { let expectation = expectation(description: "Callback invoked") venmoClient.tokenize(venmoRequest) { venmoAccount, error in XCTAssertNil(venmoAccount) - XCTAssertNil(error) + XCTAssertNotNil(error) + + let error = error! as NSError + XCTAssertEqual(error.localizedDescription, BTVenmoError.canceled.localizedDescription) + XCTAssertEqual(error.code, 10) + expectation.fulfill() } BTVenmoClient.handleReturnURL(URL(string: "scheme://x-callback-url/vzero/auth/venmo/cancel")!)