Skip to content
4 changes: 4 additions & 0 deletions Adyen.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@
A09C704327032870004C01AA /* FormCardLogosItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A09C704227032870004C01AA /* FormCardLogosItemView.swift */; };
A09FB2EB2B8399B700270D51 /* TrackableComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A09FB2EA2B8399B700270D51 /* TrackableComponent.swift */; };
A09FB2ED2B86051000270D51 /* AnalyticsEventDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = A09FB2EC2B86051000270D51 /* AnalyticsEventDataSource.swift */; };
A0A0B37E2E93F87200E8567D /* SDKData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A0B37D2E93F86F00E8567D /* SDKData.swift */; };
A0A0B37A2E8FEBC800E8567D /* ObservableThreadSafetyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0A0B3792E8FEBC800E8567D /* ObservableThreadSafetyTests.swift */; };
A0B180312A2DE445003C608E /* MealVoucherDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B180302A2DE445003C608E /* MealVoucherDetails.swift */; };
A0B4B9B12BC027DE00C99926 /* CardComponentEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A0B4B9B02BC027DE00C99926 /* CardComponentEventTests.swift */; };
Expand Down Expand Up @@ -1947,6 +1948,7 @@
A09C704227032870004C01AA /* FormCardLogosItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormCardLogosItemView.swift; sourceTree = "<group>"; };
A09FB2EA2B8399B700270D51 /* TrackableComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackableComponent.swift; sourceTree = "<group>"; };
A09FB2EC2B86051000270D51 /* AnalyticsEventDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnalyticsEventDataSource.swift; sourceTree = "<group>"; };
A0A0B37D2E93F86F00E8567D /* SDKData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKData.swift; sourceTree = "<group>"; };
A0A0B3792E8FEBC800E8567D /* ObservableThreadSafetyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableThreadSafetyTests.swift; sourceTree = "<group>"; };
A0B180302A2DE445003C608E /* MealVoucherDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MealVoucherDetails.swift; sourceTree = "<group>"; };
A0B4B9B02BC027DE00C99926 /* CardComponentEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardComponentEventTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4835,6 +4837,7 @@
E788A4D82657FF5D00089448 /* Model */ = {
isa = PBXGroup;
children = (
A0A0B37D2E93F86F00E8567D /* SDKData.swift */,
F91664EF23E41D7300C10738 /* AnyEncodable.swift */,
F9FE25222625ADD0001874BB /* PartialPaymentOrder.swift */,
F9838D382631AD1900963483 /* Balance.swift */,
Expand Down Expand Up @@ -7632,6 +7635,7 @@
00BC8F552BF349FC00EBE0EB /* SelectableFormItemStyle.swift in Sources */,
F90FB7C02448587C005BFE0E /* TextField.swift in Sources */,
E224088D22B0FD220058923E /* StoredPayPalPaymentMethod.swift in Sources */,
A0A0B37E2E93F87200E8567D /* SDKData.swift in Sources */,
F99D2F0A266136A700BB5B2F /* AppleWalletPassResponse.swift in Sources */,
F9639B3524DD97A30073F38A /* PaymentStatusResponse.swift in Sources */,
A0DB486E2AFD0BFC00348C83 /* AnalyticsEventError.swift in Sources */,
Expand Down
32 changes: 23 additions & 9 deletions Adyen/Core/Core Protocols/PaymentComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,32 @@ extension PaymentComponent {
sendSubmitEvent()

let component = component ?? self
let checkoutAttemptId = component.context.analyticsProvider?.checkoutAttemptId ?? AnalyticsConstants.fetchCheckoutAttemptIdFailed
let updatedData = data.replacing(checkoutAttemptId: checkoutAttemptId)

guard updatedData.browserInfo == nil else {
delegate?.didSubmit(updatedData, from: component)
return
}
updatedData.dataByAddingBrowserInfo { [weak self] in
self?.delegate?.didSubmit($0, from: component)

prepareSubmitData(from: data) { [weak self] updatedData in
guard let self else { return }
self.delegate?.didSubmit(updatedData, from: component)
}
}

public var checkoutAttemptId: String {
context.analyticsProvider?.checkoutAttemptId ?? AnalyticsConstants.fetchCheckoutAttemptIdFailed
}

/// Adds SDK related info to payment data object and returns the final data in the completion.
public func prepareSubmitData(from data: PaymentComponentData, completion: @escaping (PaymentComponentData) -> Void) {

let sdkData = SDKData(
checkoutAttemptId: checkoutAttemptId,
authenticationProvider: data.paymentMethod as? SDKDataAuthenticationProvider
)

let updatedData = data
.replacing(checkoutAttemptId: checkoutAttemptId)
.replacing(sdkData: sdkData)

updatedData.dataByAddingBrowserInfo(completion: completion)
}

private func sendSubmitEvent() {
let logEvent = AnalyticsEventLog(component: paymentMethod.type.rawValue, type: .submit)
context.analyticsProvider?.add(log: logEvent)
Expand Down
37 changes: 31 additions & 6 deletions Adyen/Model/PaymentComponentData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Foundation

/**
The data supplied by a payment component upon completion.

- SeeAlso:
[API Reference](https://docs.adyen.com/api-explorer/#/CheckoutService/latest/post/payments__example_payments-klarna)
*/
Expand Down Expand Up @@ -36,6 +36,7 @@ public struct PaymentComponentData {
public let installments: Installments?

/// Indicates whether the current SDK version suports native redirect without glue pages.
@available(*, deprecated, message: "This property is deprecated. Use the new sdkData property instead.")
public let supportNativeRedirect: Bool = true

/// Shopper name.
Expand All @@ -60,6 +61,7 @@ public struct PaymentComponentData {
public let browserInfo: BrowserInfo?

/// A unique identifier for a checkout attempt.
@available(*, deprecated, message: "This property is deprecated. Use the new sdkData property instead.")
public var checkoutAttemptId: String? {
paymentMethod.checkoutAttemptId
}
Expand Down Expand Up @@ -87,6 +89,10 @@ public struct PaymentComponentData {
return paymentMethod.delegatedAuthenticationData
}

/// An encoded string containing important SDK-specific data.
/// It is recommended to pass this field to your server to ensure maximum performance and reliability.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please disregard if it makes no sense, but I decided to bring this to your attention. Can we make this statement less vague and explain a bit more?
And the second part, a bit more bold just to stretch us - can we make it non-optional?

public var sdkData: String?

/// Initializes the payment component data.
///
///
Expand All @@ -98,21 +104,36 @@ public struct PaymentComponentData {
/// - browserInfo: The device default browser info.
/// - checkoutAttemptId: The checkoutAttempt identifier.
/// - installments: Installments selection if specified.
/// - sdkData: The encoded SDK data if specified.
@_spi(AdyenInternal)
public init(
paymentMethodDetails: some PaymentMethodDetails,
amount: Amount?,
order: PartialPaymentOrder?,
storePaymentMethod: Bool? = nil,
browserInfo: BrowserInfo? = nil,
installments: Installments? = nil
installments: Installments? = nil,
sdkData: String? = nil
) {
self.amount = amount
self.paymentMethod = paymentMethodDetails
self.order = order
self.storePaymentMethod = storePaymentMethod
self.browserInfo = browserInfo
self.installments = installments
self.sdkData = sdkData
}

internal func replacing(sdkData: SDKData) -> PaymentComponentData {
PaymentComponentData(
paymentMethodDetails: paymentMethod,
amount: amount,
order: order,
storePaymentMethod: storePaymentMethod,
browserInfo: browserInfo,
installments: installments,
sdkData: sdkData.encodedValue
)
}

@_spi(AdyenInternal)
Expand All @@ -123,7 +144,8 @@ public struct PaymentComponentData {
order: order,
storePaymentMethod: storePaymentMethod,
browserInfo: browserInfo,
installments: installments
installments: installments,
sdkData: sdkData
)
}

Expand All @@ -135,7 +157,8 @@ public struct PaymentComponentData {
order: order,
storePaymentMethod: storePaymentMethod,
browserInfo: browserInfo,
installments: installments
installments: installments,
sdkData: sdkData
)
}

Expand All @@ -150,7 +173,8 @@ public struct PaymentComponentData {
order: order,
storePaymentMethod: storePaymentMethod,
browserInfo: browserInfo,
installments: installments
installments: installments,
sdkData: sdkData
)
}

Expand All @@ -168,7 +192,8 @@ public struct PaymentComponentData {
order: order,
storePaymentMethod: storePaymentMethod,
browserInfo: $0,
installments: installments
installments: installments,
sdkData: sdkData
))
}
}
Expand Down
58 changes: 58 additions & 0 deletions Adyen/Model/SDKData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// 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)
public struct SDKData: Codable {

internal struct Analytics: Codable {
internal let checkoutAttemptId: String
}

public struct Authentication: Codable {
internal let threeDS2SdkVersion: String

public init(threeDS2SdkVersion: String) {
self.threeDS2SdkVersion = threeDS2SdkVersion
}
}

internal let analytics: Analytics
internal private(set) var authentication: Authentication?
internal let schemaVersion: Int = SchemaVersions.v1
private let supportNativeRedirect: Bool = true
private let timestamp = Int(Date().timeIntervalSince1970 * 1000)

internal var encodedValue: String? {
try? AdyenCoder.encodeBase64(self)
}

internal init(
checkoutAttemptId: String,
authenticationProvider: SDKDataAuthenticationProvider? = nil
) {
self.analytics = .init(checkoutAttemptId: checkoutAttemptId)
self.authentication = authenticationProvider?.authentication
}

private enum CodingKeys: String, CodingKey {
case analytics
case authentication
case supportNativeRedirect
case schemaVersion
case timestamp = "createdAt"
}

private enum SchemaVersions {
internal static let v1 = 1
}
}

@_spi(AdyenInternal)
public protocol SDKDataAuthenticationProvider {
var authentication: SDKData.Authentication { get }
}
8 changes: 8 additions & 0 deletions AdyenCard/Components/Card/CardDetails.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public struct CardDetails: PaymentMethodDetails, ShopperInformation {
public let socialSecurityNumber: String?

/// The 3DS2 SDK version.
@available(*, deprecated, message: "This property is deprecated. Use the new sdkData property of the PaymentComponentData object instead.")
public let threeDS2SDKVersion: String = threeDS2SdkVersion

/// Brand of the card.
Expand Down Expand Up @@ -157,3 +158,10 @@ public struct CardDetails: PaymentMethodDetails, ShopperInformation {

@_spi(AdyenInternal)
extension CardDetails: DelegatedAuthenticationAware {}

@_spi(AdyenInternal)
extension CardDetails: SDKDataAuthenticationProvider {
public var authentication: SDKData.Authentication {
.init(threeDS2SdkVersion: threeDS2SdkVersion)
}
}
11 changes: 2 additions & 9 deletions AdyenDropIn/DropInComponentExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,17 +67,10 @@ extension DropInComponent: PaymentComponentDelegate {
public func didSubmit(_ data: PaymentComponentData, from component: PaymentComponent) {
paymentInProgress = true

let updatedData = data.replacing(checkoutAttemptId: component.context.analyticsProvider?.checkoutAttemptId)

guard updatedData.browserInfo == nil else {
self.delegate?.didSubmit(updatedData, from: component, in: self)
return
}
updatedData.dataByAddingBrowserInfo { [weak self] in
component.prepareSubmitData(from: data) { [weak self] updatedData in
guard let self else { return }
self.delegate?.didSubmit($0, from: component, in: self)
self.delegate?.didSubmit(updatedData, from: component, in: self)
}

}

public func didFail(with error: Error, from component: PaymentComponent) {
Expand Down
7 changes: 2 additions & 5 deletions AdyenSession/API/Payments/PaymentsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ internal struct PaymentsRequest: APIRequest {

try container.encode(data.paymentMethod.encodable, forKey: .paymentMethod)
try container.encode(sessionData, forKey: .sessionData)
try container.encode(data.supportNativeRedirect, forKey: .supportNativeRedirect)
try container.encode(data.storePaymentMethod, forKey: .storePaymentMethod)
try container.encodeIfPresent(
data.delegatedAuthenticationData,
Expand All @@ -57,25 +56,24 @@ internal struct PaymentsRequest: APIRequest {
try container.encodeIfPresent(data.deliveryAddress, forKey: .deliveryAddress)
try container.encodeIfPresent(data.socialSecurityNumber, forKey: .socialSecurityNumber)
try container.encodeIfPresent(data.browserInfo, forKey: .browserInfo)
try container.encodeIfPresent(data.checkoutAttemptId, forKey: .checkoutAttemptId)
try container.encodeIfPresent(data.order?.compactOrder, forKey: .order)
try container.encodeIfPresent(data.sdkData, forKey: .sdkData)
}

private enum CodingKeys: String, CodingKey {
case sessionData
case paymentMethod
case supportNativeRedirect
case storePaymentMethod
case shopperEmail
case browserInfo
case checkoutAttemptId
case shopperName
case telephoneNumber
case billingAddress
case deliveryAddress
case socialSecurityNumber
case order
case delegatedAuthenticationData
case sdkData
}
}

Expand All @@ -102,7 +100,6 @@ internal struct PaymentsResponse: SessionResponse, SessionPaymentResultAware {

internal extension PaymentsResponse {

// swiftlint:disable:next explicit_acl
enum ResultCode: String, Decodable {
case authenticationFinished = "AuthenticationFinished"
case authenticationNotRequired = "AuthenticationNotRequired"
Expand Down
6 changes: 2 additions & 4 deletions Demo/Common/Networking/PaymentsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ internal struct PaymentsRequest: APIRequest {
}

try container.encode(data.paymentMethod.encodable, forKey: .details)
try container.encode(data.supportNativeRedirect, forKey: .supportNativeRedirect)
try container.encode(data.storePaymentMethod, forKey: .storePaymentMethod)
try container.encodeIfPresent(
data.delegatedAuthenticationData,
Expand Down Expand Up @@ -69,7 +68,7 @@ internal struct PaymentsRequest: APIRequest {
try container.encodeIfPresent(data.installments, forKey: .installments)
try container.encode(ConfigurationConstants.lineItems, forKey: .lineItems)
try container.encode(ConfigurationConstants.recurringProcessingModel, forKey: .recurringProcessingModel)
try container.encodeIfPresent(data.checkoutAttemptId, forKey: .checkoutAttemptId)
try container.encodeIfPresent(data.sdkData, forKey: .sdkData)
try container.encode(ConfigurationConstants.mandate, forKey: .mandate)
}

Expand All @@ -96,7 +95,6 @@ internal struct PaymentsRequest: APIRequest {

private enum CodingKeys: String, CodingKey {
case details = "paymentMethod"
case supportNativeRedirect
case storePaymentMethod
case amount
case reference
Expand All @@ -108,7 +106,6 @@ internal struct PaymentsRequest: APIRequest {
case additionalData
case merchantAccount
case browserInfo
case checkoutAttemptId
case shopperName
case telephoneNumber
case shopperLocale
Expand All @@ -121,6 +118,7 @@ internal struct PaymentsRequest: APIRequest {
case delegatedAuthenticationData
case recurringProcessingModel
case mandate
case sdkData
}

}
Expand Down
Loading
Loading