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

Cashapp/Twint error events #1931

Merged
merged 21 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fcb411a
add new level field to initial analytics
erenbesel Nov 11, 2024
fbb8ccb
Merge branch 'develop' into COIOS-817_level_field
erenbesel Nov 18, 2024
31dc37b
add level value to tests
erenbesel Nov 18, 2024
a1e3083
remove constant which was not needed yet
erenbesel Nov 18, 2024
d47f1fa
disable buggy swiftformat rule
erenbesel Nov 18, 2024
dbad355
send redirect error events
erenbesel Dec 4, 2024
a815adc
send encryption error events
erenbesel Dec 4, 2024
71d10eb
3ds2 related error events
erenbesel Dec 12, 2024
47f1299
Merge branch 'develop' into COIOS-841_error_events_3ds2
erenbesel Dec 12, 2024
9d5f4a5
Merge branch 'develop' into COIOS-841_error_events_3ds2
erenbesel Dec 17, 2024
7b84586
add redirect action name
erenbesel Dec 17, 2024
346dd04
Merge branch 'COIOS-841_error_events_3ds2' of https://github.com/Adye…
erenbesel Dec 17, 2024
a07d1bf
Merge branch 'develop' into COIOS-841_error_events_3ds2
goergisn Dec 18, 2024
b858ef7
Update AdyenActions/Actions/RedirectAction.swift
erenbesel Dec 18, 2024
974a19b
a few more tests
erenbesel Dec 19, 2024
228267e
twint/cashapp error events
erenbesel Dec 31, 2024
7a0f63b
Merge branch 'develop' into COIOS-845_error_events_3rdparty
erenbesel Dec 31, 2024
deeaf26
add errordesc back on cashapp error
erenbesel Jan 3, 2025
a8f9395
revert localzied desc addition
erenbesel Jan 3, 2025
d11f441
Merge branch 'develop' into COIOS-845_thirdParty_errors
erenbesel Jan 3, 2025
1dfa16d
Merge branch 'develop' into COIOS-845_thirdParty_errors
erenbesel Jan 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
erenbesel marked this conversation as resolved.
Show resolved Hide resolved
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) {
erenbesel marked this conversation as resolved.
Show resolved Hide resolved
goergisn marked this conversation as resolved.
Show resolved Hide resolved
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"
erenbesel marked this conversation as resolved.
Show resolved Hide resolved
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)
goergisn marked this conversation as resolved.
Show resolved Hide resolved
delegate?.didFail(with: error, from: self)
}

private func sendThirdPartyErrorEvent(with message: String?) {
erenbesel marked this conversation as resolved.
Show resolved Hide resolved
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
Loading