Skip to content

Commit

Permalink
Cashapp/Twint error events (#1931)
Browse files Browse the repository at this point in the history
# Summary

Third party related error events (Twint/Cashapp)

# Ticket

<ticket>
COIOS-841
</ticket>

---------

Co-authored-by: Alex Guretzki <goergisn@me.com>
  • Loading branch information
erenbesel and goergisn authored Jan 6, 2025
1 parent 66bfdc3 commit 860ac8e
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 14 deletions.
29 changes: 25 additions & 4 deletions AdyenActions/Components/SDK/TwintSDKActionComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ import Foundation
.twintNoAppsInstalledMessage,
self.configuration.localizationParameters
)
self.handleShowError(errorMessage)
self.handleShowError(
errorMessage,
componentName: action.paymentMethodType
)
return
}

Expand All @@ -133,7 +136,10 @@ import Foundation
let completionHandler: (Error?) -> Void = { [weak self] error in
guard let self else { return }
if let error {
self.handleShowError(error.localizedDescription)
self.handleShowError(
error.localizedDescription,
componentName: action.paymentMethodType
)
return
}

Expand Down Expand Up @@ -212,10 +218,14 @@ import Foundation
presentationDelegate.present(component: presentableComponent)
}

private func handleShowError(_ error: String) {
private func handleShowError(_ errorMessage: String, componentName: String) {
sendThirdPartyErrorEvent(
with: errorMessage,
componentName: componentName
)
let alert = UIAlertController(
title: nil,
message: error,
message: errorMessage,
preferredStyle: .alert
)
alert.addAction(
Expand All @@ -235,6 +245,17 @@ import Foundation
private func cleanup() {
pollingComponent?.didCancel()
}

private func sendThirdPartyErrorEvent(with message: String?, componentName: String) {
var errorEvent = AnalyticsEventError(
component: componentName,
type: .thirdParty
)
errorEvent.code = AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
errorEvent.message = message

context.analyticsProvider?.add(error: errorEvent)
}
}

@_spi(AdyenInternal)
Expand Down
33 changes: 26 additions & 7 deletions AdyenCashAppPay/CashAppPayComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ public final class CashAppPayComponent: PaymentComponent,
static let storeDetailsItem = "storeDetailsItem"
static let cashAppButtonItem = "cashAppButtonItem"
}

private enum ErrorMessage {
static let unexpectedError = "CashApp unexpected error"
static let apiError = "CashApp api error"
static let integrationError = "CashApp integration error"
}

/// The context object for this component.
@_spi(AdyenInternal)
Expand Down Expand Up @@ -209,7 +215,7 @@ public final class CashAppPayComponent: PaymentComponent,
storePaymentMethod: storePayment
))
} catch {
fail(with: error)
fail(with: error, message: error.localizedDescription)
}
}

Expand All @@ -225,24 +231,37 @@ extension CashAppPayComponent: CashAppPayObserver {
case let .approved(request, grants):
submitApprovedRequest(with: grants, profile: request.customerProfile)
case let .apiError(error):
fail(with: error)
fail(with: error, message: ErrorMessage.apiError)
case let .networkError(error):
fail(with: error)
fail(with: error, message: error.localizedDescription)
case let .unexpectedError(error):
fail(with: error)
fail(with: error, message: ErrorMessage.unexpectedError)
case let .integrationError(error):
fail(with: error)
fail(with: error, message: ErrorMessage.integrationError)
case .declined:
fail(with: Error.declined)
let error = Error.declined
fail(with: error, message: error.localizedDescription)
default:
break
}
}

private func fail(with error: Swift.Error) {
private func fail(with error: Swift.Error, message: String? = nil) {
stopLoading()
sendThirdPartyErrorEvent(with: message)
delegate?.didFail(with: error, from: self)
}

private func sendThirdPartyErrorEvent(with message: String?) {
var errorEvent = AnalyticsEventError(
component: paymentMethod.type.rawValue,
type: .thirdParty
)
errorEvent.code = AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
errorEvent.message = message

context.analyticsProvider?.add(error: errorEvent)
}
}

@available(iOS 13.0, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import XCTest
static func actionComponent(
with twintSpy: TwintSpy,
configuration: TwintSDKActionComponent.Configuration = .dummy,
context: AdyenContext = Dummy.context,
presentationDelegate: PresentationDelegate?,
delegate: ActionComponentDelegate?,
shouldFailPolling: Bool = false
Expand All @@ -64,7 +65,7 @@ import XCTest
}

let component = TwintSDKActionComponent(
context: Dummy.context,
context: context,
configuration: configuration,
twint: twintSpy,
pollingComponentBuilder: pollingBuilder
Expand Down Expand Up @@ -114,7 +115,7 @@ import XCTest
return actionComponentDelegateMock
}

/// ActionComponentDelegateMock that fails when `onDidFail` is called
/// ActionComponentDelegateMock that fails when `onDidProvide` is called
static func failureFlowActionComponentDelegateMock(
onDidFail: @escaping (Error) -> Void
) -> ActionComponentDelegateMock {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,25 @@ import XCTest
return false
}

let analyticsProviderMock = AnalyticsProviderMock()
let presentationDelegate = PresentationDelegateMock()

presentationDelegate.doPresent = { component in
let alertController = try XCTUnwrap(component.viewController as? UIAlertController)
XCTAssertEqual(alertController.message, expectedAlertMessage)
let errorEvent = analyticsProviderMock.errors[0]
XCTAssertEqual(errorEvent.component, "paymentMethodType")
XCTAssertEqual(errorEvent.errorType, .thirdParty)
XCTAssertEqual(
errorEvent.code,
AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
)
alertExpectation.fulfill()
}

let twintActionComponent = Self.actionComponent(
with: twintSpy,
context: Dummy.context(with: analyticsProviderMock),
presentationDelegate: presentationDelegate,
delegate: nil
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import XCTest

final class CashAppPayComponentTests: XCTestCase {

private enum ErrorOption {
case apiError(PayKit.APIError)
case integrationError(PayKit.IntegrationError)
case networkError(PayKit.NetworkError)
}

var paymentMethodString = """
{
"configuration" : {
Expand All @@ -25,10 +31,24 @@ import XCTest
"type" : "cashapp"
}
"""

lazy var paymentMethod: CashAppPayPaymentMethod = {
try! JSONDecoder().decode(CashAppPayPaymentMethod.self, from: paymentMethodString.data(using: .utf8)!)
}()

private static var integrationError: PayKit.IntegrationError = .init(
category: .MERCHANT_ERROR,
code: .BRAND_NOT_FOUND,
detail: "integrationError",
field: "error"
)

private static var apiError: PayKit.APIError = .init(
category: .API_ERROR,
code: .GATEWAY_TIMEOUT,
detail: "apiError",
field: nil
)

var context: AdyenContext!

Expand Down Expand Up @@ -280,6 +300,157 @@ import XCTest
XCTAssertEqual(paymentDelegateMock.didSubmitCallsCount, 1)
}

func testSubmitFailure() throws {
let analyticsProviderMock = AnalyticsProviderMock()
let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!)
let sut = CashAppPayComponent(
paymentMethod: paymentMethod,
context: Dummy.context(with: analyticsProviderMock),
configuration: config
)

setupRootViewController(sut.viewController)

let paymentDelegateMock = PaymentComponentDelegateMock()
sut.delegate = paymentDelegateMock

let failureExpectation = expectation(description: "didFail must be called when submitting fails.")
paymentDelegateMock.onDidFail = { _, _ in
let errorEvent = analyticsProviderMock.errors[0]
XCTAssertEqual(errorEvent.component, "cashapp")
XCTAssertEqual(errorEvent.errorType, .thirdParty)
XCTAssertEqual(
errorEvent.code,
AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
)
XCTAssertEqual(errorEvent.message, "There was no grant object in the customer request.")
failureExpectation.fulfill()
}

sut.submitApprovedRequest(with: [], profile: .init(id: "test", cashtag: "test"))
wait(for: [failureExpectation], timeout: 5)
}

func testIntegrationError() throws {
let analyticsProviderMock = AnalyticsProviderMock()
let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!)
let sut = CashAppPayComponent(
paymentMethod: paymentMethod,
context: Dummy.context(with: analyticsProviderMock),
configuration: config
)

let paymentDelegateMock = PaymentComponentDelegateMock()
sut.delegate = paymentDelegateMock

let errorExpectation = expectation(description: "should fail with integration error")

paymentDelegateMock.onDidFail = { _, _ in
let errorEvent = analyticsProviderMock.errors[0]
XCTAssertEqual(errorEvent.component, "cashapp")
XCTAssertEqual(errorEvent.errorType, .thirdParty)
XCTAssertEqual(
errorEvent.code,
AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
)
XCTAssertEqual(errorEvent.message, "CashApp integration error")
errorExpectation.fulfill()
}

sut.stateDidChange(to: .integrationError(Self.integrationError))
wait(for: [errorExpectation], timeout: 5)
}

func testApiError() throws {
let analyticsProviderMock = AnalyticsProviderMock()
let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!)
let sut = CashAppPayComponent(
paymentMethod: paymentMethod,
context: Dummy.context(with: analyticsProviderMock),
configuration: config
)

let paymentDelegateMock = PaymentComponentDelegateMock()
sut.delegate = paymentDelegateMock

let errorExpectation = expectation(description: "should fail with integration error")

paymentDelegateMock.onDidFail = { _, _ in
let errorEvent = analyticsProviderMock.errors[0]
XCTAssertEqual(errorEvent.component, "cashapp")
XCTAssertEqual(errorEvent.errorType, .thirdParty)
XCTAssertEqual(
errorEvent.code,
AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
)
XCTAssertEqual(errorEvent.message, "CashApp api error")
errorExpectation.fulfill()
}

sut.stateDidChange(to: .apiError(Self.apiError))
wait(for: [errorExpectation], timeout: 5)
}

func testUnexpectedError() throws {
let analyticsProviderMock = AnalyticsProviderMock()
let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!)
let sut = CashAppPayComponent(
paymentMethod: paymentMethod,
context: Dummy.context(with: analyticsProviderMock),
configuration: config
)

let paymentDelegateMock = PaymentComponentDelegateMock()
sut.delegate = paymentDelegateMock

let errorExpectation = expectation(description: "should fail with integration error")

paymentDelegateMock.onDidFail = { _, _ in
let errorEvent = analyticsProviderMock.errors[0]
XCTAssertEqual(errorEvent.component, "cashapp")
XCTAssertEqual(errorEvent.errorType, .thirdParty)
XCTAssertEqual(
errorEvent.code,
AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
)
XCTAssertEqual(errorEvent.message, "CashApp unexpected error")
errorExpectation.fulfill()
}

sut.stateDidChange(to: .unexpectedError(.emptyErrorArray))
wait(for: [errorExpectation], timeout: 5)
}

func testNetworkError() throws {
let analyticsProviderMock = AnalyticsProviderMock()
let config = CashAppPayConfiguration(redirectURL: URL(string: "test")!)
let sut = CashAppPayComponent(
paymentMethod: paymentMethod,
context: Dummy.context(with: analyticsProviderMock),
configuration: config
)

let paymentDelegateMock = PaymentComponentDelegateMock()
sut.delegate = paymentDelegateMock

let errorExpectation = expectation(description: "should fail with integration error")

paymentDelegateMock.onDidFail = { _, _ in
let errorEvent = analyticsProviderMock.errors[0]
XCTAssertEqual(errorEvent.component, "cashapp")
XCTAssertEqual(errorEvent.errorType, .thirdParty)
XCTAssertEqual(
errorEvent.code,
AnalyticsConstants.ErrorCode.thirdPartyError.stringValue
)
XCTAssertNotNil(errorEvent.message)
errorExpectation.fulfill()
}

sut.stateDidChange(to: .networkError(.noResponse))
wait(for: [errorExpectation], timeout: 5)
}

func testValidateShouldReturnFormViewControllerValidateResult() throws {
// Given
let configuration = CashAppPayConfiguration(redirectURL: URL(string: "test")!, showsSubmitButton: false)
Expand Down

0 comments on commit 860ac8e

Please sign in to comment.