Skip to content

Commit

Permalink
Refactor ADYServiceAdaptor to ThreeDSServicable (#2023)
Browse files Browse the repository at this point in the history
# Summary

A refactor for the ADYServiceAdaptor to make it easily swappable if
necessary.

# Ticket

<ticket>
DSP-1866
</ticket>
  • Loading branch information
robertdalmeida authored Mar 5, 2025
2 parents ac14353 + c74d0f1 commit a08c8bb
Show file tree
Hide file tree
Showing 25 changed files with 827 additions and 367 deletions.
60 changes: 52 additions & 8 deletions Adyen.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Adyen3DS2
import Foundation

internal protocol AnyAuthenticationRequestParameters {
Expand All @@ -21,5 +20,3 @@ internal protocol AnyAuthenticationRequestParameters {

var messageVersion: String { get }
}

extension ADYAuthenticationRequestParameters: AnyAuthenticationRequestParameters {}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Adyen3DS2
import Foundation

internal protocol AnyChallengeResult {

var transactionStatus: String { get }
}

extension ADYChallengeResult: AnyChallengeResult {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

internal struct ChallengeParameters {
internal let challengeToken: ThreeDS2Component.ChallengeToken
internal let threeDSRequestorAppURL: URL?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import class Adyen3DS2.ADYAppearanceConfiguration
import Foundation

internal struct FingerprintServiceParameters {
internal let directoryServerIdentifier: String
internal let directoryServerPublicKey: String
internal let directoryServerRootCertificates: String
internal let deviceExcludedParameters: [String: Any]?
internal let appearanceConfiguration: Adyen3DS2.ADYAppearanceConfiguration
internal let threeDSMessageVersion: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ internal final class ADYServiceAdapter: AnyADYService {

internal func transaction(withMessageVersion: String) throws -> AnyADYTransaction {
guard let service else {
throw UnknownError(errorDescription: "ADYService is nil.")
throw UnknownError.serviceIsNil
}
return try service.transaction(withMessageVersion: withMessageVersion)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Adyen3DS2
import Foundation
@_spi(AdyenInternal) import Adyen

/// This is a wrapper class for the Objective C 3ds2 sdk.
/// This translates simple non specific sdk bound types such as
/// `FingerprintServiceParameters` & `ChallengeParameters` to sdk specific types and performs actions.
internal final class ThreeDSServiceLegacy: ThreeDSServiceable {
private var service: AnyADYService
private var transaction: AnyADYTransaction?

internal init(
service: AnyADYService = ADYServiceAdapter()
) {
self.service = service
}

internal func performFingerprint(
parameters: FingerprintServiceParameters,
completionHandler: @escaping (Result<AnyAuthenticationRequestParameters, ThreeDSServiceFingerprintError>) -> Void
) {
let serviceParameters = ADYServiceParameters(
directoryServerIdentifier: parameters.directoryServerIdentifier,
directoryServerPublicKey: parameters.directoryServerPublicKey,
directoryServerRootCertificates: parameters.directoryServerRootCertificates
)
service.service(
with: serviceParameters,
appearanceConfiguration: parameters.appearanceConfiguration
) { [weak self] service in
guard let self else { return }
do {
let transaction = try service.transaction(withMessageVersion: parameters.threeDSMessageVersion)
self.transaction = transaction
completionHandler(.success(transaction.authenticationParameters))

} catch {
completionHandler(
.failure(.fingerprintingError(
errorPayload: self.opaqueErrorObject(error: error)
))
)
}
}
}

internal func performChallenge(
with parameters: ChallengeParameters,
completionHandler: @escaping (Result<AnyChallengeResult, ThreeDSServiceChallengeError>) -> Void
) {
let challengeParameters = ADYChallengeParameters(
serverTransactionIdentifier: parameters.challengeToken.serverTransactionIdentifier,
threeDSRequestorAppURL: parameters.threeDSRequestorAppURL,
acsTransactionIdentifier: parameters.challengeToken.acsTransactionIdentifier,
acsReferenceNumber: parameters.challengeToken.acsReferenceNumber,
acsSignedContent: parameters.challengeToken.acsSignedContent
)

guard let transaction else {
return completionHandler(.failure(.transactionNotInitialized(
errorPayload: opaqueErrorObject(error: ThreeDSServiceChallengeError.transactionNotInitialized(errorPayload: ""))
)))
}

transaction.performChallenge(
with: challengeParameters
) { [weak self] challengeResult, error in
guard let self else { return }

guard let result = challengeResult else {
guard let error else {
completionHandler(.failure(.errorAndResultAreNil(
errorPayload: opaqueErrorObject(error: UnknownError.resultAndErrorAreNil)
)))
return
}

if isCancelled(error: error) {
return completionHandler(.failure(.cancelled(
errorPayload: opaqueErrorObject(error: error)
)))
} else {
return completionHandler(.failure(.challengeError(
errorPayload: opaqueErrorObject(error: error)
)))
}
}
completionHandler(.success(result))
}
}

private func isCancelled(error: Error) -> Bool {
let error: NSError = error as NSError
return (error.code == Int(ADYRuntimeErrorCode.challengeCancelled.rawValue)) && (error.domain == ADYRuntimeErrorDomain)
}

internal func opaqueErrorObject(error: any Error) -> String {
(error as NSError).base64Representation()
}

internal func resetTransaction() {
self.transaction = nil
}
}

extension Adyen3DS2.ADYAuthenticationRequestParameters: AnyAuthenticationRequestParameters {}
extension Adyen3DS2.ADYChallengeResult: AnyChallengeResult {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

/// Errors that could happen during a challenge
internal enum ThreeDSServiceChallengeError: Error {
/// The transaction was not initialized during fingerprinting.
case transactionNotInitialized(errorPayload: String)
/// Edge case which should never occur.
case errorAndResultAreNil(errorPayload: String)
/// The challenge has been cancelled.
case cancelled(errorPayload: String)
/// The sdk faced an error performing the challenge.
case challengeError(errorPayload: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation

/// Errors that could happen during fingerprinting
internal enum ThreeDSServiceFingerprintError: Error {
/// The sdk faced an error performing fingerprinting.
case fingerprintingError(errorPayload: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

import Foundation
@_spi(AdyenInternal) import Adyen

/// This protocol abstracts the SDK logic away.
/// The 3DS2 SDK performs 2 tasks - fingerprint & challenge.
/// Additionally we provide a way to reset the transaction.
internal protocol ThreeDSServiceable {
func performFingerprint(
parameters: FingerprintServiceParameters,
completionHandler: @escaping (Result<AnyAuthenticationRequestParameters, ThreeDSServiceFingerprintError>) -> Void
)
func performChallenge(
with parameters: ChallengeParameters,
completionHandler: @escaping (Result<AnyChallengeResult, ThreeDSServiceChallengeError>) -> Void
)
func resetTransaction()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Copyright (c) 2025 Adyen N.V.
//
// This file is open source and available under the MIT license. See the LICENSE file for more info.
//

@_spi(AdyenInternal) import Adyen

extension UnknownError {
internal static let transactionNotInitialized = UnknownError(errorDescription: "Transaction not initialized")
internal static let serviceIsNil = UnknownError(errorDescription: "ADYService is nil.")
internal static let resultAndErrorAreNil = UnknownError(errorDescription: "Both error and result are nil, this should never happen.")
}
Loading

0 comments on commit a08c8bb

Please sign in to comment.