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

Refactor ADYServiceAdaptor to ThreeDSServicable #2023

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
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
Loading