Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
56ee998
Added UI elements for BSB
neelSharma12 Feb 19, 2025
2b2b295
Logic to decide the UI
erenbesel Feb 21, 2025
2d981d0
fix typo
erenbesel Feb 21, 2025
5fb1c0e
add padding explanation
erenbesel Feb 21, 2025
2dc3160
fix spacing
erenbesel Feb 21, 2025
885ba03
update label fonts
erenbesel Feb 24, 2025
5437ce5
reduce component responsibility a bit
erenbesel Feb 26, 2025
2853a9d
Merge branch 'feature/payto-base' into feature/payto-itemsProvider
erenbesel Feb 26, 2025
6839113
adding validation
erenbesel Feb 27, 2025
f90802b
Merge branch 'feature/payto-base' into feature/payto-validation
erenbesel Mar 3, 2025
ae1b28d
validation part for payto component items
erenbesel Mar 3, 2025
99c9d4e
remove extra line
erenbesel Mar 3, 2025
1033445
review updates
erenbesel Mar 3, 2025
6d7ac11
working flow for payto
erenbesel Mar 4, 2025
38086b7
add tests for payto
erenbesel Mar 6, 2025
fc3aa4f
Merge branch 'feature/payto-base' into feature/payto-full-flow
erenbesel Mar 6, 2025
083f759
fix payto await action enum name
erenbesel Mar 7, 2025
443d37b
add storedPayToPaymentMethod
erenbesel Mar 10, 2025
95d094e
tests for stored payto
erenbesel Mar 11, 2025
db0821a
Merge branch 'feature/payto-base' into feature/payto-stored
erenbesel Mar 11, 2025
713c204
fix payTo name
erenbesel Mar 11, 2025
1fb570c
more tests for stored payTo
erenbesel Mar 12, 2025
4162754
more tests for payTo
erenbesel Mar 12, 2025
3da6dff
Merge branch 'feature/payto-base' into feature/payto-stored
erenbesel Mar 12, 2025
a66e682
review updates
erenbesel Mar 12, 2025
37d2932
Merge branch 'feature/payto-stored' of https://github.com/Adyen/adyen…
erenbesel Mar 12, 2025
d88bf87
fix indentation
erenbesel Mar 12, 2025
0afd843
Merge branch 'feature/payto-base' into feature/payto-stored
erenbesel Mar 12, 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
3 changes: 3 additions & 0 deletions Adyen/Core/Core Protocols/PaymentComponentBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ public protocol PaymentComponentBuilder: AdyenContextAware {

/// Builds a certain `PaymentComponent` based on a `PayToPaymentMethod`.
func build(paymentMethod: PayToPaymentMethod) -> PaymentComponent?

/// Builds a certain `PaymentComponent` based on a `StoredPayToPaymentMethod`.
func build(paymentMethod: StoredPayToPaymentMethod) -> PaymentComponent?

/// Builds a certain `PaymentComponent` based on a `CashAppPayPaymentMethod`.
func build(paymentMethod: CashAppPayPaymentMethod) -> PaymentComponent?
Expand Down
2 changes: 2 additions & 0 deletions Adyen/Core/Payment Methods/Abstract/AnyPaymentMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal enum AnyPaymentMethod: Codable {
case storedAchDirectDebit(StoredACHDirectDebitPaymentMethod)
case storedCashAppPay(StoredCashAppPayPaymentMethod)
case storedTwint(StoredTwintPaymentMethod)
case storedPayTo(StoredPayToPaymentMethod)

case instant(PaymentMethod)
case card(AnyCardPaymentMethod)
Expand Down Expand Up @@ -91,6 +92,7 @@ internal enum AnyPaymentMethod: Codable {
case let .storedPayByBankUS(paymentMethod): return paymentMethod
case let .payByBankUS(paymentMethod): return paymentMethod
case let .payTo(paymentMethod): return paymentMethod
case let .storedPayTo(paymentMethod): return paymentMethod
case .none: return nil
}
}
Expand Down
15 changes: 13 additions & 2 deletions Adyen/Core/Payment Methods/Abstract/AnyPaymentMethodDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -543,11 +543,22 @@ private struct TwintPaymentMethodDecoder: PaymentMethodDecoder {

private struct PayToPaymentMethodDecoder: PaymentMethodDecoder {
func decode(from decoder: Decoder, isStored: Bool) throws -> AnyPaymentMethod {
try .payTo(PayToPaymentMethod(from: decoder))
if isStored {
return try .storedPayTo(StoredPayToPaymentMethod(from: decoder))
} else {
return try .payTo(PayToPaymentMethod(from: decoder))
}
}

func anyPaymentMethod(from paymentMethod: any PaymentMethod) -> AnyPaymentMethod? {
(paymentMethod as? PayToPaymentMethod).map { .payTo($0) }
switch paymentMethod {
case let regular as PayToPaymentMethod:
.payTo(regular)
case let stored as StoredPayToPaymentMethod:
.storedPayTo(stored)
default:
nil
}
}
}

Expand Down
43 changes: 43 additions & 0 deletions Adyen/Core/Payment Methods/PayToPaymentMethod.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,46 @@ public struct PayToPaymentMethod: PaymentMethod {
}

}

/// A stored PayTo payment method.
public struct StoredPayToPaymentMethod: StoredPaymentMethod {

public let type: PaymentMethodType

public let name: String

public let identifier: String

public let label: String

public let supportedShopperInteractions: [ShopperInteraction]

public var merchantProvidedDisplayInformation: MerchantCustomDisplayInformation?

@_spi(AdyenInternal)
public func buildComponent(using builder: PaymentComponentBuilder) -> PaymentComponent? {
builder.build(paymentMethod: self)
}

public func defaultDisplayInformation(using parameters: LocalizationParameters?) -> DisplayInformation {
let accessibilityLabel = [
name,
label
].joined(separator: ", ")

return DisplayInformation(
title: label,
subtitle: name,
logoName: type.rawValue,
accessibilityLabel: accessibilityLabel
)
}

private enum CodingKeys: String, CodingKey {
case type
case name
case identifier = "id"
case label
case supportedShopperInteractions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,14 +219,6 @@ extension ComponentManager: PaymentComponentBuilder {
)
}

internal func build(paymentMethod: PayToPaymentMethod) -> PaymentComponent? {
PayToComponent(
paymentMethod: paymentMethod,
context: context,
configuration: .init(style: configuration.style.formComponent)
)
}

internal func build(paymentMethod: UPIPaymentMethod) -> PaymentComponent? {
UPIComponent(
paymentMethod: paymentMethod,
Expand Down Expand Up @@ -288,6 +280,22 @@ extension ComponentManager: PaymentComponentBuilder {
configuration: configuration
)
}

internal func build(paymentMethod: PayToPaymentMethod) -> PaymentComponent? {
PayToComponent(
paymentMethod: paymentMethod,
context: context,
configuration: .init(style: configuration.style.formComponent)
)
}

internal func build(paymentMethod: StoredPayToPaymentMethod) -> (any PaymentComponent)? {
StoredPaymentMethodComponent(
paymentMethod: paymentMethod,
context: context,
configuration: .init(localizationParameters: configuration.localizationParameters)
)
}
}

// MARK: - Privates
Expand Down
2 changes: 2 additions & 0 deletions Demo/Common/Networking/PaymentsRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ internal struct PaymentsRequest: APIRequest {
try container.encode(ConfigurationConstants.lineItems, forKey: .lineItems)
try container.encode(ConfigurationConstants.recurringProcessingModel, forKey: .recurringProcessingModel)
try container.encodeIfPresent(data.checkoutAttemptId, forKey: .checkoutAttemptId)
try container.encode(ConfigurationConstants.mandate, forKey: .mandate)
}

private enum CodingKeys: String, CodingKey {
Expand Down Expand Up @@ -94,6 +95,7 @@ internal struct PaymentsRequest: APIRequest {
case lineItems
case delegatedAuthenticationData
case recurringProcessingModel
case mandate
Comment thread
atmamont marked this conversation as resolved.
}

}
Expand Down
6 changes: 4 additions & 2 deletions Demo/Common/Networking/SessionRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal struct SessionRequest: APIRequest {
try container.encode("iOS", forKey: .channel)
try container.encode(ConfigurationConstants.additionalData, forKey: .additionalData)
try container.encode(ConfigurationConstants.lineItems, forKey: .lineItems)
try container.encode(ConfigurationConstants.mandate, forKey: .mandate)

if ConfigurationConstants.current.cardSettings.enableInstallments {
let installmentOptions = [
Expand All @@ -58,8 +59,8 @@ internal struct SessionRequest: APIRequest {
message: "API version should be v70 or above to apply card component's store payment method field",
condition: ConfigurationConstants.current.apiVersion < 70
)
try container.encode("askForConsent", forKey: .storePaymentMethodMode)
Comment thread
atmamont marked this conversation as resolved.
try container.encode("CardOnFile", forKey: .recurringProcessingModel)
try container.encode("enabled", forKey: .storePaymentMethodMode)
try container.encode(ConfigurationConstants.recurringProcessingModel, forKey: .recurringProcessingModel)
}

if ConfigurationConstants.current.dropInSettings.allowDisablingStoredPaymentMethods {
Expand All @@ -85,6 +86,7 @@ internal struct SessionRequest: APIRequest {
case recurringProcessingModel
case showInstallmentAmount
case showRemovePaymentMethodButton
case mandate
}

}
Expand Down
16 changes: 7 additions & 9 deletions Demo/Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,13 @@ internal enum ConfigurationConstants {
]]

// sample mandate object (e.g., for PayTo)
static let mandate = [[
"amount": "4001", // [Mandatory] for PayTo - Mandate Amount field
"amountRule": "max", // [Mandatory] for PayTo - Needs to be Localised
"endsAt": "2024-12-31", // [Mandatory] for PayTo - Date format
"frequency": "adhoc", // [Mandatory] for PayTo - Needs to be Localised
"remarks": "testThroughFlow1", // [Mandatory] for PayTo - Needs to be Localised as "Description"
"count": "3", // [Optional] will be returned only if the merchant sends it
"startsAt": "2024-11-13"
]]
static let mandate = [
"amount": "\(current.amount.value)",
"amountRule": "max",
"endsAt": "2027-10-01",
"frequency": "adhoc",
"remarks": "Remark on mandate"
]

static var delegatedAuthenticationConfigurations: ThreeDS2Component.Configuration.DelegatedAuthentication {
.init(relyingPartyIdentifier: "test-authentication-adyen.netlify.app")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,26 @@ class StoredPaymentMethodComponentTests: XCTestCase {
XCTAssertEqual(viewController?.message, paymentMethod.defaultDisplayInformation(using: nil).title)
XCTAssertEqual(viewController?.title, localizedString(.dropInStoredTitle, nil, paymentMethod.name))
}

func test_storedPaymentComponent_matches_payTo() throws {
// Given
let paymentMethod = StoredPayToPaymentMethod(
type: .payTo,
name: "PayTo Account",
identifier: "twint",
label: "***123",
supportedShopperInteractions: [.shopperPresent]
)
let sut = StoredPaymentMethodComponent(paymentMethod: paymentMethod, context: context)

let viewController = sut.viewController as? UIAlertController
XCTAssertNotNil(viewController)
XCTAssertEqual(viewController?.actions.count, 2)
XCTAssertEqual(viewController?.actions.first?.title, localizedString(.cancelButton, nil))
XCTAssertEqual(viewController?.actions.last?.title, localizedSubmitButtonTitle(with: Dummy.payment.amount, style: .immediate, nil))
XCTAssertEqual(viewController?.message, paymentMethod.defaultDisplayInformation(using: nil).title)
XCTAssertEqual(viewController?.title, localizedString(.dropInStoredTitle, nil, paymentMethod.name))
}

func testViewDidLoadShouldSendInitialEvent() throws {
// Given
Expand Down
67 changes: 51 additions & 16 deletions Tests/IntegrationTests/DropIn Tests/ComponentManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class ComponentManagerTests: XCTestCase {
storedPayPalDictionary,
storedBcmcDictionary,
storedACHDictionary,
storedTwintDictionary
storedTwintDictionary,
storedPayToDictionary
],
"paymentMethods": [
creditCardDictionary,
Expand Down Expand Up @@ -68,11 +69,13 @@ class ComponentManagerTests: XCTestCase {
cashAppPay,
giftCard,
mealVoucherSodexo,
twint
twint,
payto
]
]

let numberOfExpectedRegularComponents = 27
let numberOfExpectedRegularComponents = 28
let numberOfExpectedStoredComponent = 7

var presentationDelegate: PresentationDelegateMock!
var context: AdyenContext!
Expand Down Expand Up @@ -102,14 +105,14 @@ class ComponentManagerTests: XCTestCase {
presentationDelegate: presentationDelegate
)

XCTAssertEqual(sut.storedComponents.count, 6)
XCTAssertEqual(sut.storedComponents.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.count, numberOfExpectedRegularComponents)

XCTAssertEqual(sut.storedComponents.filter { $0.context.apiContext.clientKey == Dummy.apiContext.clientKey }.count, 6)
XCTAssertEqual(sut.storedComponents.filter { $0.context.apiContext.clientKey == Dummy.apiContext.clientKey }.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.filter { $0.context.apiContext.clientKey == Dummy.apiContext.clientKey }.count, numberOfExpectedRegularComponents)

XCTAssertEqual(sut.regularComponents.filter { $0 is LoadingComponent }.count, 22)
XCTAssertEqual(sut.regularComponents.filter { $0 is PresentableComponent }.count, 22)
XCTAssertEqual(sut.regularComponents.filter { $0 is LoadingComponent }.count, 23)
XCTAssertEqual(sut.regularComponents.filter { $0 is PresentableComponent }.count, 23)
XCTAssertEqual(sut.regularComponents.filter { $0 is FinalizableComponent }.count, 0)
}

Expand All @@ -123,11 +126,11 @@ class ComponentManagerTests: XCTestCase {
presentationDelegate: presentationDelegate
)

XCTAssertEqual(sut.storedComponents.count, 6)
XCTAssertEqual(sut.storedComponents.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.count, numberOfExpectedRegularComponents + 1)

XCTAssertEqual(sut.regularComponents.filter { $0 is LoadingComponent }.count, 22)
XCTAssertEqual(sut.regularComponents.filter { $0 is PresentableComponent }.count, 23)
XCTAssertEqual(sut.regularComponents.filter { $0 is LoadingComponent }.count, 23)
XCTAssertEqual(sut.regularComponents.filter { $0 is PresentableComponent }.count, 24)
XCTAssertEqual(sut.regularComponents.filter { $0 is FinalizableComponent }.count, 1)
}

Expand Down Expand Up @@ -207,6 +210,38 @@ class ComponentManagerTests: XCTestCase {
let storedTwintComponent = paymentComponent as? StoredPaymentMethodComponent
XCTAssertNotNil(storedTwintComponent)
}

func test_componentManager_contains_payToComponent() throws {
let sut = ComponentManager(
paymentMethods: paymentMethods,
context: context,
configuration: configuration,
order: nil,
presentationDelegate: presentationDelegate
)

// When
let paymentComponent = sut.regularComponents.first { $0.paymentMethod.type.rawValue == "payto" }

XCTAssertNotNil(paymentComponent)
}

func test_componentManager_contains_storedPayToComponent() throws {
// Given
let sut = ComponentManager(
paymentMethods: paymentMethods,
context: context,
configuration: configuration,
order: nil,
presentationDelegate: presentationDelegate
)

// When
let paymentComponent = sut.storedComponents.first { $0.paymentMethod.type.rawValue == "payto" }

// Then
XCTAssertNotNil(paymentComponent as? StoredPaymentMethodComponent)
}

func testLocalizationWithCustomTableName() throws {
configuration.localizationParameters = LocalizationParameters(tableName: "AdyenUIHost", keySeparator: nil)
Expand All @@ -219,10 +254,10 @@ class ComponentManagerTests: XCTestCase {
presentationDelegate: presentationDelegate
)

XCTAssertEqual(sut.storedComponents.count, 6)
XCTAssertEqual(sut.storedComponents.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.count, numberOfExpectedRegularComponents)

XCTAssertEqual(sut.storedComponents.compactMap { ($0 as? StoredPaymentMethodComponent)?.configuration.localizationParameters }.filter { $0.tableName == "AdyenUIHost" }.count, 4)
XCTAssertEqual(sut.storedComponents.compactMap { ($0 as? StoredPaymentMethodComponent)?.configuration.localizationParameters }.filter { $0.tableName == "AdyenUIHost" }.count, 5)
}

func testLocalizationWithCustomKeySeparator() throws {
Expand All @@ -236,10 +271,10 @@ class ComponentManagerTests: XCTestCase {
presentationDelegate: presentationDelegate
)

XCTAssertEqual(sut.storedComponents.count, 6)
XCTAssertEqual(sut.storedComponents.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.count, numberOfExpectedRegularComponents)

XCTAssertEqual(sut.storedComponents.compactMap { ($0 as? StoredPaymentMethodComponent)?.configuration.localizationParameters }.filter { $0.keySeparator == "_" }.count, 4)
XCTAssertEqual(sut.storedComponents.compactMap { ($0 as? StoredPaymentMethodComponent)?.configuration.localizationParameters }.filter { $0.keySeparator == "_" }.count, 5)
}

func testOrderInjection() throws {
Expand Down Expand Up @@ -270,11 +305,11 @@ class ComponentManagerTests: XCTestCase {
)

XCTAssertEqual(sut.paidComponents.count, 2)
XCTAssertEqual(sut.storedComponents.count, 6)
XCTAssertEqual(sut.storedComponents.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.count, numberOfExpectedRegularComponents)

XCTAssertEqual(sut.paidComponents.filter { $0.order == order }.count, 2)
XCTAssertEqual(sut.storedComponents.filter { $0.order == order }.count, 6)
XCTAssertEqual(sut.storedComponents.filter { $0.order == order }.count, numberOfExpectedStoredComponent)
XCTAssertEqual(sut.regularComponents.filter { $0.order == order }.count, numberOfExpectedRegularComponents)
}

Expand Down
Loading