diff --git a/AdyenActions/Components/SDK/TwintSDKActionComponent.swift b/AdyenActions/Components/SDK/TwintSDKActionComponent.swift index 73c87e5d81..3278ac8d42 100644 --- a/AdyenActions/Components/SDK/TwintSDKActionComponent.swift +++ b/AdyenActions/Components/SDK/TwintSDKActionComponent.swift @@ -117,7 +117,10 @@ import Foundation .twintNoAppsInstalledMessage, self.configuration.localizationParameters ) - self.handleShowError(errorMessage) + self.handleShowError( + errorMessage, + componentName: action.paymentMethodType + ) return } @@ -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 } @@ -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( @@ -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) diff --git a/AdyenCashAppPay/CashAppPayComponent.swift b/AdyenCashAppPay/CashAppPayComponent.swift index 2645c1c65b..cddb21e9cb 100644 --- a/AdyenCashAppPay/CashAppPayComponent.swift +++ b/AdyenCashAppPay/CashAppPayComponent.swift @@ -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) @@ -209,7 +215,7 @@ public final class CashAppPayComponent: PaymentComponent, storePaymentMethod: storePayment )) } catch { - fail(with: error) + fail(with: error, message: error.localizedDescription) } } @@ -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, *) diff --git a/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift b/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift index 22dc064cb9..d30db0c664 100644 --- a/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift +++ b/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests+Convenience.swift @@ -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 @@ -64,7 +65,7 @@ import XCTest } let component = TwintSDKActionComponent( - context: Dummy.context, + context: context, configuration: configuration, twint: twintSpy, pollingComponentBuilder: pollingBuilder @@ -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 { diff --git a/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift b/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift index d8f16b236b..51736fcce7 100644 --- a/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift +++ b/Tests/IntegrationTests/Actions Tests/ActionComponent/Twint/TwintSDKActionTests.swift @@ -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 ) diff --git a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift index cd7fdb9303..3408011e80 100644 --- a/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/Cash App Pay/CashAppPayComponentTests.swift @@ -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" : { @@ -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! @@ -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)