From 37758ef8a2f4a1fd52d4ecd9e0870d05ae7a9e04 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 20:51:38 +0200 Subject: [PATCH 01/16] Revove BACSConfirmation --- .../BACSDirectDebitComponent.swift | 118 +++++------------- .../Factories/BACSItemsFactory.swift | 39 ++---- .../BACSConfirmationPresenter.swift | 110 ---------------- .../BACSConfirmationViewController.swift | 52 -------- ...troller.swift => BACSViewController.swift} | 13 +- ...putPresenter.swift => BACSViewModel.swift} | 110 +++++++--------- .../BACSDirectDebitComponentTests.swift | 2 +- .../Mocks/BACSInputFormViewProtocolMock.swift | 2 +- .../BACSInputPresenterProtocolMock.swift | 2 +- .../BACSInputFormViewControllerTests.swift | 6 +- .../Input/BACSInputPresenterTests.swift | 20 +-- 11 files changed, 105 insertions(+), 369 deletions(-) delete mode 100644 AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationPresenter.swift delete mode 100644 AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationViewController.swift rename AdyenComponents/BACS Direct Debit/Scenes/Input/{BACSInputFormViewController.swift => BACSViewController.swift} (67%) rename AdyenComponents/BACS Direct Debit/Scenes/Input/{BACSInputPresenter.swift => BACSViewModel.swift} (64%) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index acdd35756d..2c64a0893e 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -13,11 +13,6 @@ import Adyen #endif import UIKit -internal protocol BACSDirectDebitRouterProtocol: AnyObject { - func presentConfirmation(with data: BACSDirectDebitData) - func confirmPayment(with data: BACSDirectDebitData) -} - /// A component that provides a form for BACS Direct Debit payments. @MainActor package final class BACSDirectDebitComponent: PaymentComponent, PresentableComponent { @@ -49,13 +44,9 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo // MARK: - Properties internal let bacsPaymentMethod: BACSDirectDebitPaymentMethod - - internal var confirmationPresenter: BACSConfirmationPresenterProtocol? - private var confirmationViewPresented = false - - internal let inputFormViewController: BACSInputFormViewController - - internal private(set) var inputPresenter: BACSInputPresenterProtocol? + + internal let bacsViewController: BACSViewController + internal private(set) var bacsViewModel: BACSViewModelProtocol? // MARK: - Initializers @@ -72,13 +63,13 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo self.bacsPaymentMethod = paymentMethod self.context = context self.configuration = configuration - self.inputFormViewController = BACSInputFormViewController( + self.bacsViewController = BACSViewController( title: paymentMethod.name, scrollEnabled: configuration.showsSubmitButton, styleProvider: configuration.style ) self.viewController = SecuredViewController( - child: inputFormViewController, + child: bacsViewController, style: configuration.style ) @@ -92,70 +83,32 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo localizationParameters: configuration.localizationParameters, scope: String(describing: self) ) - self.inputPresenter = BACSInputPresenter( - view: inputFormViewController, - router: self, - tracker: tracker, - itemsFactory: itemsFactory - ) - inputPresenter?.amount = context.amount - inputFormViewController.presenter = inputPresenter - - } -} - -// MARK: - BACSDirectDebitRouterProtocol - -/// :nodoc: -extension BACSDirectDebitComponent: BACSDirectDebitRouterProtocol { - - internal func presentConfirmation(with data: BACSDirectDebitData) { - confirmationViewPresented = true - let confirmationView = assembleConfirmationView(with: data) - - let wrappedComponent = PresentableComponentWrapper( - component: self, - viewController: confirmationView - ) - presentationDelegate?.present(component: wrappedComponent) - } - internal func confirmPayment(with data: BACSDirectDebitData) { - guard let bacsDirectDebitPaymentMethod = paymentMethod as? BACSDirectDebitPaymentMethod else { - return - } - let details = BACSDirectDebitDetails( - paymentMethod: bacsDirectDebitPaymentMethod, - holderName: data.holderName, - bankAccountNumber: data.bankAccountNumber, - bankLocationId: data.bankLocationId + self.bacsViewModel = BACSViewModel( + view: bacsViewController, + tracker: tracker, + itemsFactory: itemsFactory, + amount: context.amount, + onSubmitTap: { [weak self] data in + let details = BACSDirectDebitDetails( + paymentMethod: paymentMethod, + holderName: data.holderName, + bankAccountNumber: data.bankAccountNumber, + bankLocationId: data.bankLocationId + ) + let data = PaymentComponentData( + paymentMethodDetails: details, + amount: context.amount, + order: self?.order + ) + self?.submit(data: data) + } ) - confirmationPresenter?.startLoading() - submit(data: PaymentComponentData(paymentMethodDetails: details, amount: context.amount, order: order)) + bacsViewController.viewModel = bacsViewModel } - // MARK: - Private - - private func assembleConfirmationView(with data: BACSDirectDebitData) -> UIViewController { - let confirmationViewController = BACSConfirmationViewController( - title: paymentMethod.name, - scrollEnabled: configuration.showsSubmitButton, - styleProvider: configuration.style, - localizationParameters: configuration.localizationParameters - ) - let itemsFactory = BACSItemsFactory( - styleProvider: configuration.style, - localizationParameters: configuration.localizationParameters, - scope: String(describing: self) - ) - confirmationPresenter = BACSConfirmationPresenter( - data: data, - view: confirmationViewController, - router: self, - itemsFactory: itemsFactory - ) - confirmationViewController.presenter = confirmationPresenter - return SecuredViewController(child: confirmationViewController, style: configuration.style) + package func submit() { + bacsViewModel?.onSubmitButtonTap() } } @@ -166,21 +119,6 @@ extension BACSDirectDebitComponent: LoadingComponent { /// Stops any processing animation that the component is running. package func stopLoading() { - confirmationPresenter?.stopLoading() - } -} - -// MARK: - Cancellable - -/// :nodoc: -extension BACSDirectDebitComponent: Cancellable { - - /// Called when the user cancels the component. - package func didCancel() { - if confirmationViewPresented == false { - inputPresenter?.resetForm() - } else { - confirmationViewPresented = false - } + bacsViewModel?.stopLoading() } } diff --git a/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift b/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift index cd8d839820..60e7fb28bb 100644 --- a/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift +++ b/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift @@ -18,10 +18,8 @@ internal protocol BACSItemsFactoryProtocol { func createBankAccountNumberItem() -> FormTextInputItem func createSortCodeItem() -> FormTextInputItem func createEmailItem() -> FormTextInputItem - func createContinueButton() -> FormButtonItem func createPaymentButton() -> FormButtonItem func createAmountConsentToggle(amount: Amount?) -> FormToggleItem - func createConsentText(with amount: Amount?) -> String func createLegalConsentToggle() -> FormToggleItem } @@ -32,7 +30,6 @@ internal struct BACSItemsFactory: BACSItemsFactoryProtocol { static let bankAccountNumberItem = "bankAccountNumberItem" static let sortCodeItem = "sortCodeItem" static let emailItem = "emailItem" - static let continueButtonItem = "continueButtonItem" static let paymentButtonItem = "paymentButtonItem" static let amountTermsToggleItem = "amountConsentToggleItem" static let legalTermsToggleItem = "legalConsentToggleItem" @@ -151,20 +148,6 @@ internal struct BACSItemsFactory: BACSItemsFactoryProtocol { return textItem } - internal func createContinueButton() -> FormButtonItem { - let buttonItem = FormButtonItem(style: styleProvider.mainButtonItem) - - let localizedTitle = localizedString(.continueTitle, localizationParameters) - buttonItem.title = localizedTitle - - let identifier = ViewIdentifierBuilder.build( - scopeInstance: scope, - postfix: ViewIdentifier.continueButtonItem - ) - buttonItem.identifier = identifier - return buttonItem - } - internal func createPaymentButton() -> FormButtonItem { let buttonItem = FormButtonItem(style: styleProvider.mainButtonItem) @@ -192,16 +175,6 @@ internal struct BACSItemsFactory: BACSItemsFactoryProtocol { toggleItem.identifier = identifier return toggleItem } - - internal func createConsentText(with amount: Amount?) -> String { - let localizedTitle: String - if let amount { - localizedTitle = localizedString(.bacsSpecifiedAmountConsentToggleTitle, localizationParameters, amount.formatted) - } else { - localizedTitle = localizedString(.bacsAmountConsentToggleTitle, localizationParameters) - } - return localizedTitle - } internal func createLegalConsentToggle() -> FormToggleItem { let toggleItem = FormToggleItem(style: styleProvider.toggle) @@ -217,4 +190,16 @@ internal struct BACSItemsFactory: BACSItemsFactoryProtocol { toggleItem.identifier = identifier return toggleItem } + + // MARK: - Private + + private func createConsentText(with amount: Amount?) -> String { + let localizedTitle: String + if let amount { + localizedTitle = localizedString(.bacsSpecifiedAmountConsentToggleTitle, localizationParameters, amount.formatted) + } else { + localizedTitle = localizedString(.bacsAmountConsentToggleTitle, localizationParameters) + } + return localizedTitle + } } diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationPresenter.swift b/AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationPresenter.swift deleted file mode 100644 index 274b7c63e6..0000000000 --- a/AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationPresenter.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// Copyright (c) 2021 Adyen N.V. -// -// This file is open source and available under the MIT license. See the LICENSE file for more info. -// - -import Adyen -#if canImport(AdyenUI) - import AdyenUI - @_spi(AdyenInternal) import class AdyenUI.FormTextInputItem -#endif -import Foundation - -internal protocol BACSConfirmationPresenterProtocol: AnyObject { - func viewDidLoad() - func startLoading() - func stopLoading() -} - -internal class BACSConfirmationPresenter: BACSConfirmationPresenterProtocol { - - // MARK: - Properties - - private let data: BACSDirectDebitData - private let view: BACSConfirmationViewProtocol - private weak var router: BACSDirectDebitRouterProtocol? - private let itemsFactory: BACSItemsFactoryProtocol - - // MARK: - Items - - internal var holderNameItem: FormTextInputItem? - internal var bankAccountNumberItem: FormTextInputItem? - internal var sortCodeItem: FormTextInputItem? - internal var emailItem: FormTextInputItem? - internal var paymentButtonItem: FormButtonItem? - - // MARK: - Initializers - - internal init( - data: BACSDirectDebitData, - view: BACSConfirmationViewProtocol, - router: BACSDirectDebitRouterProtocol, - itemsFactory: BACSItemsFactoryProtocol - ) { - self.data = data - self.router = router - self.view = view - self.itemsFactory = itemsFactory - } - - // MARK: - BACSDirectDebitConfirmationPresenterProtocol - - internal func viewDidLoad() { - createItems() - setupView() - } - - internal func startLoading() { - paymentButtonItem?.showsActivityIndicator = true - view.setUserInteraction(enabled: false) - } - - internal func stopLoading() { - paymentButtonItem?.showsActivityIndicator = false - view.setUserInteraction(enabled: true) - } - - // MARK: - Private - - private func createItems() { - holderNameItem = itemsFactory.createHolderNameItem() - bankAccountNumberItem = itemsFactory.createBankAccountNumberItem() - sortCodeItem = itemsFactory.createSortCodeItem() - emailItem = itemsFactory.createEmailItem() - paymentButtonItem = itemsFactory.createPaymentButton() - paymentButtonItem?.buttonSelectionHandler = handlePayment - - fillItems() - } - - private func setupView() { - view.add(item: holderNameItem) - view.add(item: bankAccountNumberItem) - view.add(item: sortCodeItem) - view.add(item: emailItem) - view.add(item: FormSpacerItem(numberOfSpaces: 2)) - view.add(item: paymentButtonItem) - view.add(item: FormSpacerItem(numberOfSpaces: 1)) - - setupItems() - } - - private func setupItems() { - holderNameItem?.isEnabled = false - bankAccountNumberItem?.isEnabled = false - sortCodeItem?.isEnabled = false - emailItem?.isEnabled = false - } - - private func fillItems() { - holderNameItem?.value = data.holderName - bankAccountNumberItem?.value = data.bankAccountNumber - sortCodeItem?.value = data.bankLocationId - emailItem?.value = data.shopperEmail - } - - private func handlePayment() { - router?.confirmPayment(with: data) - } -} diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationViewController.swift b/AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationViewController.swift deleted file mode 100644 index e8c1a44acd..0000000000 --- a/AdyenComponents/BACS Direct Debit/Scenes/Confirmation/BACSConfirmationViewController.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// Copyright (c) 2021 Adyen N.V. -// -// This file is open source and available under the MIT license. See the LICENSE file for more info. -// - -import Adyen -#if canImport(AdyenUI) - import AdyenUI - @_spi(AdyenInternal) import class AdyenUI.FormViewController -#endif -import UIKit - -internal protocol BACSConfirmationViewProtocol: FormViewProtocol { - func setUserInteraction(enabled: Bool) -} - -internal class BACSConfirmationViewController: FormViewController, BACSConfirmationViewProtocol { - - // MARK: - Properties - - internal weak var presenter: BACSConfirmationPresenterProtocol? - - // MARK: - Initializers - - internal init( - title: String, - scrollEnabled: Bool, - styleProvider: FormComponentStyle, - localizationParameters: LocalizationParameters? = nil - ) { - super.init( - scrollEnabled: scrollEnabled, - style: styleProvider, - localizationParameters: localizationParameters - ) - self.title = title - } - - // MARK: - View life cycle - - override internal func viewDidLoad() { - super.viewDidLoad() - presenter?.viewDidLoad() - } - - // MARK: - BACSConfirmationViewProtocol - - internal func setUserInteraction(enabled: Bool) { - view.isUserInteractionEnabled = enabled - } -} diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSInputFormViewController.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift similarity index 67% rename from AdyenComponents/BACS Direct Debit/Scenes/Input/BACSInputFormViewController.swift rename to AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift index 43bc533878..1e2c3302ae 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSInputFormViewController.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift @@ -11,13 +11,13 @@ import Adyen #endif import UIKit -internal protocol BACSInputFormViewProtocol: FormViewProtocol {} +internal protocol BACSView: FormViewProtocol {} -internal class BACSInputFormViewController: FormViewController, BACSInputFormViewProtocol { +internal class BACSViewController: FormViewController, BACSView { // MARK: - Properties - internal weak var presenter: BACSInputPresenterProtocol? + internal weak var viewModel: BACSViewModelProtocol? // MARK: - Initializers @@ -39,11 +39,6 @@ internal class BACSInputFormViewController: FormViewController, BACSInputFormVie override internal func viewDidLoad() { super.viewDidLoad() - presenter?.viewDidLoad() - } - - override internal func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - presenter?.viewWillAppear() + viewModel?.viewDidLoad() } } diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSInputPresenter.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift similarity index 64% rename from AdyenComponents/BACS Direct Debit/Scenes/Input/BACSInputPresenter.swift rename to AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift index 1cb3653b3b..6968827534 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSInputPresenter.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift @@ -11,21 +11,21 @@ import Adyen #endif import Foundation -internal protocol BACSInputPresenterProtocol: AnyObject { - var amount: Amount? { get set } +internal protocol BACSViewModelProtocol: AnyObject { func viewDidLoad() - func viewWillAppear() - func resetForm() + func stopLoading() + func onSubmitButtonTap() } -internal class BACSInputPresenter: BACSInputPresenterProtocol { +internal class BACSViewModel: BACSViewModelProtocol { // MARK: - Properties - private let view: BACSInputFormViewProtocol + private let view: BACSView private let tracker: BACSDirectDebitComponentTrackerProtocol - private weak var router: BACSDirectDebitRouterProtocol? - private var data: BACSDirectDebitData? + internal let itemsFactory: BACSItemsFactoryProtocol + private let amount: Amount? + private let onSubmitTap: (_ data: BACSDirectDebitData) -> Void // MARK: - Items @@ -35,28 +35,22 @@ internal class BACSInputPresenter: BACSInputPresenterProtocol { internal var emailItem: FormTextInputItem? internal var amountConsentToggleItem: FormToggleItem? internal var legalConsentToggleItem: FormToggleItem? - internal var continueButtonItem: FormButtonItem? - - internal let itemsFactory: BACSItemsFactoryProtocol - - internal var amount: Amount? { - didSet { - amountConsentToggleItem?.title = itemsFactory.createConsentText(with: amount) - } - } + internal var submitButtonItem: FormButtonItem? // MARK: - Initializers internal init( - view: BACSInputFormViewProtocol, - router: BACSDirectDebitRouterProtocol, + view: BACSView, tracker: BACSDirectDebitComponentTrackerProtocol, - itemsFactory: BACSItemsFactoryProtocol + itemsFactory: BACSItemsFactoryProtocol, + amount: Amount?, + onSubmitTap: @escaping (_ data: BACSDirectDebitData) -> Void ) { self.view = view - self.router = router self.tracker = tracker self.itemsFactory = itemsFactory + self.amount = amount + self.onSubmitTap = onSubmitTap } // MARK: - BACSInputPresenterProtocol @@ -68,23 +62,40 @@ internal class BACSInputPresenter: BACSInputPresenterProtocol { setupView() } - internal func viewWillAppear() { - restoreFields() + internal func stopLoading() { + submitButtonItem?.showsActivityIndicator = false } - internal func resetForm() { - holderNameItem?.value = "" - bankAccountNumberItem?.value = "" - sortCodeItem?.value = "" - emailItem?.value = "" + internal func onSubmitButtonTap() { + startLoading() - amountConsentToggleItem?.value = false - legalConsentToggleItem?.value = false - data = nil + guard validateForm() else { + stopLoading() + return + } + + guard let holderName = holderNameItem?.value, + let bankAccountNumber = bankAccountNumberItem?.value, + let sortCode = sortCodeItem?.value, + let shopperEmail = emailItem?.value else { + return + } + + let bacsDirectDebitData = BACSDirectDebitData( + holderName: holderName, + bankAccountNumber: bankAccountNumber, + bankLocationId: sortCode, + shopperEmail: shopperEmail + ) + onSubmitTap(bacsDirectDebitData) } // MARK: - Private + private func startLoading() { + submitButtonItem?.showsActivityIndicator = true + } + private func createItems() { holderNameItem = itemsFactory.createHolderNameItem() bankAccountNumberItem = itemsFactory.createBankAccountNumberItem() @@ -93,8 +104,8 @@ internal class BACSInputPresenter: BACSInputPresenterProtocol { amountConsentToggleItem = itemsFactory.createAmountConsentToggle(amount: amount) legalConsentToggleItem = itemsFactory.createLegalConsentToggle() - continueButtonItem = itemsFactory.createContinueButton() - continueButtonItem?.buttonSelectionHandler = continuePayment + submitButtonItem = itemsFactory.createPaymentButton() + submitButtonItem?.buttonSelectionHandler = onSubmitButtonTap } private func setupView() { @@ -107,7 +118,7 @@ internal class BACSInputPresenter: BACSInputPresenterProtocol { view.add(item: FormSpacerItem(numberOfSpaces: 1)) view.add(item: legalConsentToggleItem) view.add(item: FormSpacerItem(numberOfSpaces: 2)) - view.add(item: continueButtonItem) + view.add(item: submitButtonItem) view.add(item: FormSpacerItem(numberOfSpaces: 1)) } @@ -128,35 +139,4 @@ internal class BACSInputPresenter: BACSInputPresenterProtocol { ].compactMap { $0 } .allSatisfy { $0.isValid() } } - - private func restoreFields() { - guard let data else { return } - holderNameItem?.value = data.holderName - bankAccountNumberItem?.value = data.bankAccountNumber - sortCodeItem?.value = data.bankLocationId - emailItem?.value = data.shopperEmail - - amountConsentToggleItem?.value = true - legalConsentToggleItem?.value = true - } - - private func continuePayment() { - guard validateForm() else { return } - - guard let holderName = holderNameItem?.value, - let bankAccountNumber = bankAccountNumberItem?.value, - let sortCode = sortCodeItem?.value, - let shopperEmail = emailItem?.value else { - return - } - - let bacsDirectDebitData = BACSDirectDebitData( - holderName: holderName, - bankAccountNumber: bankAccountNumber, - bankLocationId: sortCode, - shopperEmail: shopperEmail - ) - self.data = bacsDirectDebitData - router?.presentConfirmation(with: bacsDirectDebitData) - } } diff --git a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/BACSDirectDebitComponentTests.swift b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/BACSDirectDebitComponentTests.swift index eccaecb2ee..78ac455eb9 100644 --- a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/BACSDirectDebitComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/BACSDirectDebitComponentTests.swift @@ -67,7 +67,7 @@ class BACSDirectDebitComponentTests: XCTestCase { configuration: .init() ) - let presenter: BACSInputPresenter = try XCTUnwrap(sut.inputPresenter as? BACSInputPresenter) + let presenter: BACSViewModel = try XCTUnwrap(sut.inputPresenter as? BACSViewModel) let expectedConsentTitle1 = presenter.itemsFactory.createConsentText(with: amount) setupRootViewController(sut.viewController) wait(for: .milliseconds(200)) diff --git a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputFormViewProtocolMock.swift b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputFormViewProtocolMock.swift index 91695ca287..b1763189a4 100644 --- a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputFormViewProtocolMock.swift +++ b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputFormViewProtocolMock.swift @@ -8,7 +8,7 @@ @testable import AdyenComponents @_spi(AdyenInternal) @testable import AdyenUI -class BACSInputFormViewProtocolMock: BACSInputFormViewProtocol { +class BACSInputFormViewProtocolMock: BACSView { // MARK: - setupNavigationBar diff --git a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputPresenterProtocolMock.swift b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputPresenterProtocolMock.swift index 891e3e2825..5de20aea4d 100644 --- a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputPresenterProtocolMock.swift +++ b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Mocks/BACSInputPresenterProtocolMock.swift @@ -7,7 +7,7 @@ @_spi(AdyenInternal) @testable import Adyen @testable import AdyenComponents -class BACSInputPresenterProtocolMock: BACSInputPresenterProtocol { +class BACSInputPresenterProtocolMock: BACSViewModelProtocol { // MARK: - viewDidLoad diff --git a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputFormViewControllerTests.swift b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputFormViewControllerTests.swift index eeffe013fa..2ab03ee858 100644 --- a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputFormViewControllerTests.swift +++ b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputFormViewControllerTests.swift @@ -11,7 +11,7 @@ import XCTest class BACSInputFormViewControllerTests: XCTestCase { - var sut: BACSInputFormViewController! + var sut: BACSViewController! var presenter: BACSInputPresenterProtocolMock! override func setUpWithError() throws { @@ -19,12 +19,12 @@ class BACSInputFormViewControllerTests: XCTestCase { presenter = BACSInputPresenterProtocolMock() let styleProvider = FormComponentStyle() - sut = BACSInputFormViewController( + sut = BACSViewController( title: "BACS Direct Debit", scrollEnabled: true, styleProvider: styleProvider ) - sut.presenter = presenter + sut.viewModel = presenter } override func tearDownWithError() throws { diff --git a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputPresenterTests.swift b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputPresenterTests.swift index 2c36e6976a..544a92f730 100644 --- a/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputPresenterTests.swift +++ b/Tests/IntegrationTests/Components Tests/BACS Direct Debit/Scenes/Input/BACSInputPresenterTests.swift @@ -15,7 +15,7 @@ class BACSInputPresenterTests: XCTestCase { var router: BACSRouterProtocolMock! var tracker: BACSDirectDebitComponentTrackerProtocolMock! var itemsFactory: BACSItemsFactoryProtocolMock! - var sut: BACSInputPresenter! + var sut: BACSViewModel! override func setUpWithError() throws { try super.setUpWithError() @@ -26,7 +26,7 @@ class BACSInputPresenterTests: XCTestCase { itemsFactory = itemsFactoryMock let amount = Amount(value: 105.7, currencyCode: "USD", localeIdentifier: nil) - sut = BACSInputPresenter( + sut = BACSViewModel( view: view, router: router, tracker: tracker, @@ -77,7 +77,7 @@ class BACSInputPresenterTests: XCTestCase { func testContinuePaymentWhenButtonTappedShouldDisplayValidationOnView() { // When sut.viewDidLoad() - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then XCTAssertEqual(view.displayValidationCallsCount, 1) @@ -95,7 +95,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = "mail" // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then XCTAssertEqual(router.presentConfirmationWithDataCallsCount, 0) @@ -113,7 +113,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = bacsDataMock.shopperEmail // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then XCTAssertEqual(router.presentConfirmationWithDataCallsCount, 0) @@ -131,7 +131,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = bacsDataMock.shopperEmail // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then XCTAssertEqual(router.presentConfirmationWithDataCallsCount, 0) @@ -149,7 +149,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = bacsDataMock.shopperEmail // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then XCTAssertEqual(router.presentConfirmationWithDataCallsCount, 0) @@ -167,7 +167,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = bacsDataMock.shopperEmail // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then XCTAssertEqual(router.presentConfirmationWithDataCallsCount, 1) @@ -187,7 +187,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = expectedBacsData.shopperEmail // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() // Then let receivedBacsData = router.presentConfirmationWithDataReceivedData @@ -209,7 +209,7 @@ class BACSInputPresenterTests: XCTestCase { sut.emailItem?.value = expectedBacsData.shopperEmail // When - sut.continueButtonItem?.buttonSelectionHandler?() + sut.submitButtonItem?.buttonSelectionHandler?() sut.viewWillAppear() // Then From d63a6995c3227743f42b17565db2808cbd567850 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 21:45:10 +0200 Subject: [PATCH 02/16] Implement BACSViewModel --- .../BACSDirectDebitComponent.swift | 52 +++++++-------- .../Scenes/Input/BACSViewController.swift | 31 +++++++-- .../Scenes/Input/BACSViewModel.swift | 66 ++++++++++--------- .../FormViewController+ViewProtocol.swift | 5 ++ AdyenUI/UI/Form/FormViewController.swift | 28 ++++++++ AdyenUI/UI/Form/FormViewItemManager.swift | 18 +++++ 6 files changed, 135 insertions(+), 65 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index 2c64a0893e..539759099c 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -22,7 +22,10 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo // MARK: - PresentableComponent - package let viewController: UIViewController + package lazy var viewController: UIViewController = { + let bacsViewController = resolveBACSViewController() + return SecuredViewController(child: bacsViewController, style: configuration.style) + }() /// The object that acts as the delegate of the component. package weak var delegate: PaymentComponentDelegate? @@ -45,7 +48,6 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo internal let bacsPaymentMethod: BACSDirectDebitPaymentMethod - internal let bacsViewController: BACSViewController internal private(set) var bacsViewModel: BACSViewModelProtocol? // MARK: - Initializers @@ -63,16 +65,15 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo self.bacsPaymentMethod = paymentMethod self.context = context self.configuration = configuration - self.bacsViewController = BACSViewController( - title: paymentMethod.name, - scrollEnabled: configuration.showsSubmitButton, - styleProvider: configuration.style - ) - self.viewController = SecuredViewController( - child: bacsViewController, - style: configuration.style - ) - + } + + package func submit() { + bacsViewModel?.onSubmitButtonTap() + } + + // MARK: - Private + + private func resolveBACSViewController() -> UIViewController { let tracker = BACSDirectDebitComponentTracker( paymentMethod: bacsPaymentMethod, context: context, @@ -84,31 +85,28 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo scope: String(describing: self) ) - self.bacsViewModel = BACSViewModel( - view: bacsViewController, + let bacsViewModel = BACSViewModel( + paymentMethod: bacsPaymentMethod, + amount: context.amount, tracker: tracker, itemsFactory: itemsFactory, - amount: context.amount, - onSubmitTap: { [weak self] data in - let details = BACSDirectDebitDetails( - paymentMethod: paymentMethod, - holderName: data.holderName, - bankAccountNumber: data.bankAccountNumber, - bankLocationId: data.bankLocationId - ) + onSubmit: { [weak self] details in let data = PaymentComponentData( paymentMethodDetails: details, - amount: context.amount, + amount: self?.context.amount, order: self?.order ) self?.submit(data: data) } ) - bacsViewController.viewModel = bacsViewModel - } + self.bacsViewModel = bacsViewModel - package func submit() { - bacsViewModel?.onSubmitButtonTap() + return BACSViewController( + title: paymentMethod.name, + scrollEnabled: configuration.showsSubmitButton, + styleProvider: configuration.style, + viewModel: bacsViewModel + ) } } diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift index 1e2c3302ae..613b4d7914 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift @@ -9,15 +9,15 @@ import Adyen import AdyenUI @_spi(AdyenInternal) import class AdyenUI.FormViewController #endif +import Combine import UIKit -internal protocol BACSView: FormViewProtocol {} - -internal class BACSViewController: FormViewController, BACSView { +internal final class BACSViewController: FormViewController { // MARK: - Properties - internal weak var viewModel: BACSViewModelProtocol? + private let viewModel: BACSViewModel + private var cancellables = Set() // MARK: - Initializers @@ -25,8 +25,10 @@ internal class BACSViewController: FormViewController, BACSView { title: String, scrollEnabled: Bool, styleProvider: FormComponentStyle, - localizationParameters: LocalizationParameters? = nil + localizationParameters: LocalizationParameters? = nil, + viewModel: BACSViewModel ) { + self.viewModel = viewModel super.init( scrollEnabled: scrollEnabled, style: styleProvider, @@ -39,6 +41,23 @@ internal class BACSViewController: FormViewController, BACSView { override internal func viewDidLoad() { super.viewDidLoad() - viewModel?.viewDidLoad() + viewModel.viewDidLoad() + + bindItems() + bindValidation() + } + + // MARK: - Private + + private func bindItems() { + viewModel.$items.sink { items in + items.forEach { self.add(item: $0) } + }.store(in: &cancellables) + } + + private func bindValidation() { + viewModel.$shouldShowValidation.sink { shouldShowValidation in + if shouldShowValidation { self.showValidation() } + }.store(in: &cancellables) } } diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift index 6968827534..d25c58cd30 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift @@ -17,15 +17,18 @@ internal protocol BACSViewModelProtocol: AnyObject { func onSubmitButtonTap() } -internal class BACSViewModel: BACSViewModelProtocol { +internal final class BACSViewModel: BACSViewModelProtocol { // MARK: - Properties - private let view: BACSView - private let tracker: BACSDirectDebitComponentTrackerProtocol - internal let itemsFactory: BACSItemsFactoryProtocol + private let paymentMethod: BACSDirectDebitPaymentMethod private let amount: Amount? - private let onSubmitTap: (_ data: BACSDirectDebitData) -> Void + private let tracker: BACSDirectDebitComponentTrackerProtocol + private let itemsFactory: BACSItemsFactoryProtocol + private let onSubmit: (_ details: BACSDirectDebitDetails) -> Void + + @Published internal private(set) var items: [(any FormItem)?] = [] + @Published internal private(set) var shouldShowValidation = false // MARK: - Items @@ -40,17 +43,17 @@ internal class BACSViewModel: BACSViewModelProtocol { // MARK: - Initializers internal init( - view: BACSView, + paymentMethod: BACSDirectDebitPaymentMethod, + amount: Amount?, tracker: BACSDirectDebitComponentTrackerProtocol, itemsFactory: BACSItemsFactoryProtocol, - amount: Amount?, - onSubmitTap: @escaping (_ data: BACSDirectDebitData) -> Void + onSubmit: @escaping (_ details: BACSDirectDebitDetails) -> Void ) { - self.view = view + self.amount = amount + self.paymentMethod = paymentMethod self.tracker = tracker self.itemsFactory = itemsFactory - self.amount = amount - self.onSubmitTap = onSubmitTap + self.onSubmit = onSubmit } // MARK: - BACSInputPresenterProtocol @@ -58,8 +61,7 @@ internal class BACSViewModel: BACSViewModelProtocol { internal func viewDidLoad() { tracker.sendInitialAnalytics() tracker.sendDidLoadEvent() - createItems() - setupView() + items = createItems() } internal func stopLoading() { @@ -81,13 +83,13 @@ internal class BACSViewModel: BACSViewModelProtocol { return } - let bacsDirectDebitData = BACSDirectDebitData( + let details = BACSDirectDebitDetails( + paymentMethod: paymentMethod, holderName: holderName, bankAccountNumber: bankAccountNumber, - bankLocationId: sortCode, - shopperEmail: shopperEmail + bankLocationId: sortCode ) - onSubmitTap(bacsDirectDebitData) + onSubmit(details) } // MARK: - Private @@ -96,7 +98,7 @@ internal class BACSViewModel: BACSViewModelProtocol { submitButtonItem?.showsActivityIndicator = true } - private func createItems() { + private func createItems() -> [(any FormItem)?] { holderNameItem = itemsFactory.createHolderNameItem() bankAccountNumberItem = itemsFactory.createBankAccountNumberItem() sortCodeItem = itemsFactory.createSortCodeItem() @@ -106,24 +108,24 @@ internal class BACSViewModel: BACSViewModelProtocol { submitButtonItem = itemsFactory.createPaymentButton() submitButtonItem?.buttonSelectionHandler = onSubmitButtonTap - } - private func setupView() { - view.add(item: holderNameItem) - view.add(item: bankAccountNumberItem) - view.add(item: sortCodeItem) - view.add(item: emailItem) - view.add(item: FormSpacerItem(numberOfSpaces: 2)) - view.add(item: amountConsentToggleItem) - view.add(item: FormSpacerItem(numberOfSpaces: 1)) - view.add(item: legalConsentToggleItem) - view.add(item: FormSpacerItem(numberOfSpaces: 2)) - view.add(item: submitButtonItem) - view.add(item: FormSpacerItem(numberOfSpaces: 1)) + return [ + holderNameItem, + bankAccountNumberItem, + sortCodeItem, + emailItem, + FormSpacerItem(numberOfSpaces: 2), + amountConsentToggleItem, + FormSpacerItem(numberOfSpaces: 1), + legalConsentToggleItem, + FormSpacerItem(numberOfSpaces: 2), + submitButtonItem, + FormSpacerItem(numberOfSpaces: 1) + ] } private func validateForm() -> Bool { - view.displayValidation() + shouldShowValidation = true guard let amountTermsAccepted = amountConsentToggleItem?.value, let legalTermsAccepted = legalConsentToggleItem?.value, diff --git a/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift b/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift index d179e7a7c3..22196b0a30 100644 --- a/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift +++ b/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift @@ -20,6 +20,11 @@ extension FormViewController: FormViewProtocol { append(item) } + package func add(item: (any FormItem)?) { + guard let item else { return } + append(item) + } + package func displayValidation() { resignFirstResponder() showValidation() diff --git a/AdyenUI/UI/Form/FormViewController.swift b/AdyenUI/UI/Form/FormViewController.swift index d07418aeee..f82d102507 100644 --- a/AdyenUI/UI/Form/FormViewController.swift +++ b/AdyenUI/UI/Form/FormViewController.swift @@ -177,6 +177,18 @@ open class FormViewController: UIViewController, AdyenObserver { addItemViewIfNeeded(itemView) } + /// Appends an existential item to the form. + /// + /// - Parameters: + /// - item: The item to append. + @_spi(AdyenInternal) + public func append(_ item: any FormItem) { + let itemView = itemManager.append(item) + observerVisibility(of: item, and: itemView) + itemView.applyTextDelegateIfNeeded(delegate: self) + addItemViewIfNeeded(itemView) + } + private func addItemViewIfNeeded(_ itemView: AnyFormItemView) { guard isViewLoaded else { return } formView.appendItemView(itemView) @@ -198,6 +210,22 @@ open class FormViewController: UIViewController, AdyenObserver { } } + private func observerVisibility(of item: any FormItem, and itemView: UIView) { + itemView.adyen.hide( + animationKey: String(describing: itemView), + hidden: item.isHidden.wrappedValue, + animated: false + ) + + observe(item.isHidden) { isHidden in + itemView.adyen.hide( + animationKey: String(describing: itemView), + hidden: isHidden, + animated: true + ) + } + } + // MARK: - Localizable package let localizationParameters: LocalizationParameters? diff --git a/AdyenUI/UI/Form/FormViewItemManager.swift b/AdyenUI/UI/Form/FormViewItemManager.swift index 754a11e057..80a3df9923 100644 --- a/AdyenUI/UI/Form/FormViewItemManager.swift +++ b/AdyenUI/UI/Form/FormViewItemManager.swift @@ -41,6 +41,20 @@ internal final class FormViewItemManager { return itemView } + + /// Appends an existential item to the list of managed items. + /// + /// - Parameters: + /// - item: The item to append. + /// - Returns: The view instance correspondent to a selected item. + @discardableResult internal func append(_ item: any FormItem) -> AnyFormItemView { + topLevelItem.append(item) + + let itemView = newItemView(for: item) + topLevelItemViews.append(itemView) + + return itemView + } // MARK: - Item Views @@ -56,4 +70,8 @@ internal final class FormViewItemManager { private func newItemView(for item: some FormItem) -> AnyFormItemView { item.build(with: FormItemViewBuilder(theme: theme)) } + + private func newItemView(for item: any FormItem) -> AnyFormItemView { + item.build(with: FormItemViewBuilder(theme: theme)) + } } From c090a461423169bb092cfd9e41872c94aaf48064 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 21:57:13 +0200 Subject: [PATCH 03/16] Remove BACSViewModelProtocol --- .../BACSDirectDebitComponent.swift | 4 ++-- .../Factories/BACSItemsFactory.swift | 5 +++-- .../Scenes/Input/BACSViewController.swift | 3 +-- .../Scenes/Input/BACSViewModel.swift | 15 +++++---------- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index 539759099c..ced202b6ac 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -48,7 +48,7 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo internal let bacsPaymentMethod: BACSDirectDebitPaymentMethod - internal private(set) var bacsViewModel: BACSViewModelProtocol? + internal private(set) var bacsViewModel: BACSViewModel? // MARK: - Initializers @@ -68,7 +68,7 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo } package func submit() { - bacsViewModel?.onSubmitButtonTap() + bacsViewModel?.submit() } // MARK: - Private diff --git a/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift b/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift index 60e7fb28bb..4a143f53da 100644 --- a/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift +++ b/AdyenComponents/BACS Direct Debit/Factories/BACSItemsFactory.swift @@ -18,7 +18,7 @@ internal protocol BACSItemsFactoryProtocol { func createBankAccountNumberItem() -> FormTextInputItem func createSortCodeItem() -> FormTextInputItem func createEmailItem() -> FormTextInputItem - func createPaymentButton() -> FormButtonItem + func createPaymentButton(_ onSubmit: @escaping () -> Void) -> FormButtonItem func createAmountConsentToggle(amount: Amount?) -> FormToggleItem func createLegalConsentToggle() -> FormToggleItem } @@ -148,8 +148,9 @@ internal struct BACSItemsFactory: BACSItemsFactoryProtocol { return textItem } - internal func createPaymentButton() -> FormButtonItem { + internal func createPaymentButton(_ onSubmit: @escaping () -> Void) -> FormButtonItem { let buttonItem = FormButtonItem(style: styleProvider.mainButtonItem) + buttonItem.buttonSelectionHandler = onSubmit let localizedTitle = localizedString(.bacsPaymentButtonTitle, localizationParameters) buttonItem.title = localizedTitle diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift index 613b4d7914..81ebfd4b21 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift @@ -41,10 +41,9 @@ internal final class BACSViewController: FormViewController { override internal func viewDidLoad() { super.viewDidLoad() - viewModel.viewDidLoad() - bindItems() bindValidation() + viewModel.viewDidLoad() } // MARK: - Private diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift index d25c58cd30..3a93794b42 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift @@ -11,13 +11,7 @@ import Adyen #endif import Foundation -internal protocol BACSViewModelProtocol: AnyObject { - func viewDidLoad() - func stopLoading() - func onSubmitButtonTap() -} - -internal final class BACSViewModel: BACSViewModelProtocol { +internal final class BACSViewModel { // MARK: - Properties @@ -68,7 +62,7 @@ internal final class BACSViewModel: BACSViewModelProtocol { submitButtonItem?.showsActivityIndicator = false } - internal func onSubmitButtonTap() { + internal func submit() { startLoading() guard validateForm() else { @@ -106,8 +100,9 @@ internal final class BACSViewModel: BACSViewModelProtocol { amountConsentToggleItem = itemsFactory.createAmountConsentToggle(amount: amount) legalConsentToggleItem = itemsFactory.createLegalConsentToggle() - submitButtonItem = itemsFactory.createPaymentButton() - submitButtonItem?.buttonSelectionHandler = onSubmitButtonTap + submitButtonItem = itemsFactory.createPaymentButton { [weak self] in + self?.submit() + } return [ holderNameItem, From e2648a4d11cef2b1bf15cf79601a18da4f8f613c Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 21:59:43 +0200 Subject: [PATCH 04/16] Inject configuration into ViewModel --- .../BACSDirectDebitComponent.swift | 1 + .../Models/BACSDirectDebitData.swift | 14 -------------- .../Scenes/Input/BACSViewModel.swift | 3 +++ 3 files changed, 4 insertions(+), 14 deletions(-) delete mode 100644 AdyenComponents/BACS Direct Debit/Models/BACSDirectDebitData.swift diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index ced202b6ac..89dfe35284 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -88,6 +88,7 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo let bacsViewModel = BACSViewModel( paymentMethod: bacsPaymentMethod, amount: context.amount, + configuration: configuration, tracker: tracker, itemsFactory: itemsFactory, onSubmit: { [weak self] details in diff --git a/AdyenComponents/BACS Direct Debit/Models/BACSDirectDebitData.swift b/AdyenComponents/BACS Direct Debit/Models/BACSDirectDebitData.swift deleted file mode 100644 index 2a91129539..0000000000 --- a/AdyenComponents/BACS Direct Debit/Models/BACSDirectDebitData.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// Copyright (c) 2021 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 BACSDirectDebitData: Equatable { - internal let holderName: String - internal let bankAccountNumber: String - internal let bankLocationId: String - internal let shopperEmail: String -} diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift index 3a93794b42..4f7ab30026 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift @@ -17,6 +17,7 @@ internal final class BACSViewModel { private let paymentMethod: BACSDirectDebitPaymentMethod private let amount: Amount? + private let configuration: BACSDirectDebitComponent.Configuration private let tracker: BACSDirectDebitComponentTrackerProtocol private let itemsFactory: BACSItemsFactoryProtocol private let onSubmit: (_ details: BACSDirectDebitDetails) -> Void @@ -39,12 +40,14 @@ internal final class BACSViewModel { internal init( paymentMethod: BACSDirectDebitPaymentMethod, amount: Amount?, + configuration: BACSDirectDebitComponent.Configuration, tracker: BACSDirectDebitComponentTrackerProtocol, itemsFactory: BACSItemsFactoryProtocol, onSubmit: @escaping (_ details: BACSDirectDebitDetails) -> Void ) { self.amount = amount self.paymentMethod = paymentMethod + self.configuration = configuration self.tracker = tracker self.itemsFactory = itemsFactory self.onSubmit = onSubmit From b75183fb79e619a288dccdeb83190dc88dd4eb39 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 22:21:25 +0200 Subject: [PATCH 05/16] Address retain cycles --- .../Scenes/Input/BACSViewController.swift | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift index 81ebfd4b21..83b889bdcc 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift @@ -23,16 +23,13 @@ internal final class BACSViewController: FormViewController { internal init( title: String, - scrollEnabled: Bool, - styleProvider: FormComponentStyle, - localizationParameters: LocalizationParameters? = nil, viewModel: BACSViewModel ) { self.viewModel = viewModel super.init( - scrollEnabled: scrollEnabled, - style: styleProvider, - localizationParameters: localizationParameters + scrollEnabled: viewModel.configuration.showsSubmitButton, + style: viewModel.configuration.style, + localizationParameters: viewModel.configuration.localizationParameters ) self.title = title } @@ -49,14 +46,14 @@ internal final class BACSViewController: FormViewController { // MARK: - Private private func bindItems() { - viewModel.$items.sink { items in - items.forEach { self.add(item: $0) } + viewModel.$items.sink { [weak self] items in + items.forEach { self?.add(item: $0) } }.store(in: &cancellables) } private func bindValidation() { - viewModel.$shouldShowValidation.sink { shouldShowValidation in - if shouldShowValidation { self.showValidation() } + viewModel.$shouldShowValidation.sink { [weak self] shouldShowValidation in + if shouldShowValidation { self?.showValidation() } }.store(in: &cancellables) } } From b03b02afadb67744183a7993c0a54adebb527665 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 22:21:48 +0200 Subject: [PATCH 06/16] Add shopper email to details --- .../BACSDirectDebitComponent.swift | 24 +++++++++---------- .../BACSDirectDebitDetails.swift | 11 +++++++-- .../Scenes/Input/BACSViewModel.swift | 14 +++++++---- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index 89dfe35284..0a60c2bd2d 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -23,7 +23,7 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo // MARK: - PresentableComponent package lazy var viewController: UIViewController = { - let bacsViewController = resolveBACSViewController() + let bacsViewController = createViewController() return SecuredViewController(child: bacsViewController, style: configuration.style) }() @@ -44,12 +44,18 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo /// Component's configuration package var configuration: Configuration + // MARK: - PaymentComponent + + package func submit() { + bacsViewModel?.submit() + } + // MARK: - Properties internal let bacsPaymentMethod: BACSDirectDebitPaymentMethod internal private(set) var bacsViewModel: BACSViewModel? - + // MARK: - Initializers /// Creates and returns a BACS Direct Debit component. @@ -67,13 +73,9 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo self.configuration = configuration } - package func submit() { - bacsViewModel?.submit() - } - // MARK: - Private - private func resolveBACSViewController() -> UIViewController { + private func createViewController() -> UIViewController { let tracker = BACSDirectDebitComponentTracker( paymentMethod: bacsPaymentMethod, context: context, @@ -85,7 +87,7 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo scope: String(describing: self) ) - let bacsViewModel = BACSViewModel( + let viewModel = BACSViewModel( paymentMethod: bacsPaymentMethod, amount: context.amount, configuration: configuration, @@ -100,13 +102,11 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo self?.submit(data: data) } ) - self.bacsViewModel = bacsViewModel + self.bacsViewModel = viewModel return BACSViewController( title: paymentMethod.name, - scrollEnabled: configuration.showsSubmitButton, - styleProvider: configuration.style, - viewModel: bacsViewModel + viewModel: viewModel ) } } diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift index 7e5d7529b2..9209524fa9 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift @@ -11,19 +11,22 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { @_spi(AdyenInternal) public var checkoutAttemptId: String? - + /// The payment method type. public let type: PaymentMethodType /// The BACS account's holder name. public let holderName: String + /// The BACS account's email. + public let shopperEmail: String + /// The BACS account's number. public let bankAccountNumber: String /// The BACS location's ID. public let bankLocationId: String - + /// An encoded string containing important SDK-specific data. /// It is recommended to pass this field to your server to ensure maximum performance and reliability. public var sdkData: String? @@ -32,16 +35,19 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { /// - Parameters: /// - paymentMethod: The BACS Direct Debit payment method. /// - holderName: The BACS account's holder name. + /// - shopperEmail: The BACS account's email. /// - bankAccountNumber: The BACS account's number. /// - bankLocationId: The BACS location's ID. public init( paymentMethod: BACSDirectDebitPaymentMethod, holderName: String, + shopperEmail: String, bankAccountNumber: String, bankLocationId: String ) { self.type = paymentMethod.type self.holderName = holderName + self.shopperEmail = shopperEmail self.bankAccountNumber = bankAccountNumber self.bankLocationId = bankLocationId } @@ -51,6 +57,7 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { private enum CodingKeys: String, CodingKey { case type case holderName + case shopperEmail case bankAccountNumber case bankLocationId case sdkData diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift index 4f7ab30026..7de3839990 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift @@ -17,11 +17,13 @@ internal final class BACSViewModel { private let paymentMethod: BACSDirectDebitPaymentMethod private let amount: Amount? - private let configuration: BACSDirectDebitComponent.Configuration + internal let configuration: BACSDirectDebitComponent.Configuration private let tracker: BACSDirectDebitComponentTrackerProtocol private let itemsFactory: BACSItemsFactoryProtocol private let onSubmit: (_ details: BACSDirectDebitDetails) -> Void + // MARK: - State + @Published internal private(set) var items: [(any FormItem)?] = [] @Published internal private(set) var shouldShowValidation = false @@ -53,7 +55,7 @@ internal final class BACSViewModel { self.onSubmit = onSubmit } - // MARK: - BACSInputPresenterProtocol + // MARK: - Internal internal func viewDidLoad() { tracker.sendInitialAnalytics() @@ -77,12 +79,14 @@ internal final class BACSViewModel { let bankAccountNumber = bankAccountNumberItem?.value, let sortCode = sortCodeItem?.value, let shopperEmail = emailItem?.value else { + stopLoading() return } let details = BACSDirectDebitDetails( paymentMethod: paymentMethod, holderName: holderName, + shopperEmail: shopperEmail, bankAccountNumber: bankAccountNumber, bankLocationId: sortCode ) @@ -103,8 +107,10 @@ internal final class BACSViewModel { amountConsentToggleItem = itemsFactory.createAmountConsentToggle(amount: amount) legalConsentToggleItem = itemsFactory.createLegalConsentToggle() - submitButtonItem = itemsFactory.createPaymentButton { [weak self] in - self?.submit() + if configuration.showsSubmitButton { + submitButtonItem = itemsFactory.createPaymentButton { [weak self] in + self?.submit() + } } return [ From c47aa677fe4880831ebb798fb1585b27b3cc724a Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 22:27:36 +0200 Subject: [PATCH 07/16] Remove shopperEmail from details --- .../BACS Direct Debit/BACSDirectDebitDetails.swift | 6 ------ .../{Scenes/Input => }/BACSViewController.swift | 0 .../{Scenes/Input => }/BACSViewModel.swift | 4 +--- 3 files changed, 1 insertion(+), 9 deletions(-) rename AdyenComponents/BACS Direct Debit/{Scenes/Input => }/BACSViewController.swift (100%) rename AdyenComponents/BACS Direct Debit/{Scenes/Input => }/BACSViewModel.swift (96%) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift index 9209524fa9..2ab05a0611 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift @@ -18,9 +18,6 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { /// The BACS account's holder name. public let holderName: String - /// The BACS account's email. - public let shopperEmail: String - /// The BACS account's number. public let bankAccountNumber: String @@ -41,13 +38,11 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { public init( paymentMethod: BACSDirectDebitPaymentMethod, holderName: String, - shopperEmail: String, bankAccountNumber: String, bankLocationId: String ) { self.type = paymentMethod.type self.holderName = holderName - self.shopperEmail = shopperEmail self.bankAccountNumber = bankAccountNumber self.bankLocationId = bankLocationId } @@ -57,7 +52,6 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { private enum CodingKeys: String, CodingKey { case type case holderName - case shopperEmail case bankAccountNumber case bankLocationId case sdkData diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/BACSViewController.swift similarity index 100% rename from AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewController.swift rename to AdyenComponents/BACS Direct Debit/BACSViewController.swift diff --git a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift similarity index 96% rename from AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift rename to AdyenComponents/BACS Direct Debit/BACSViewModel.swift index 7de3839990..bd61f837e8 100644 --- a/AdyenComponents/BACS Direct Debit/Scenes/Input/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift @@ -77,8 +77,7 @@ internal final class BACSViewModel { guard let holderName = holderNameItem?.value, let bankAccountNumber = bankAccountNumberItem?.value, - let sortCode = sortCodeItem?.value, - let shopperEmail = emailItem?.value else { + let sortCode = sortCodeItem?.value else { stopLoading() return } @@ -86,7 +85,6 @@ internal final class BACSViewModel { let details = BACSDirectDebitDetails( paymentMethod: paymentMethod, holderName: holderName, - shopperEmail: shopperEmail, bankAccountNumber: bankAccountNumber, bankLocationId: sortCode ) From 3daa169acca375f2138aa7e5a6fad63449926fae Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 23:21:26 +0200 Subject: [PATCH 08/16] Remove presentationDelegate --- .../BACS Direct Debit/BACSDirectDebitComponent.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index 0a60c2bd2d..9c85906a05 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -38,9 +38,6 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo /// The context object for this component. package let context: AdyenContext - /// The object that acts as the presentation delegate of the component. - package weak var presentationDelegate: PresentationDelegate? - /// Component's configuration package var configuration: Configuration From 0c8358e8d12c22bbe8b865256c155723e0f8524b Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 9 Jun 2026 23:24:08 +0200 Subject: [PATCH 09/16] Remove presentationDelegate in ComponentManager --- AdyenDropIn/DropInComponent.swift | 3 +-- AdyenDropIn/Modules/Root/DropInAssembler.swift | 3 +-- .../ComponentManager+PaymentComponentBuilder.swift | 4 +--- AdyenDropIn/Utilities/ComponentManager.swift | 7 ++----- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/AdyenDropIn/DropInComponent.swift b/AdyenDropIn/DropInComponent.swift index 4bcf891a7d..80bceb1fbc 100644 --- a/AdyenDropIn/DropInComponent.swift +++ b/AdyenDropIn/DropInComponent.swift @@ -214,8 +214,7 @@ package final class DropInComponent: NSObject, configuration: configuration, partialPaymentEnabled: partialPaymentDelegate != nil, order: order, - supportsEditingStoredPaymentMethods: storedPaymentMethodsDelegate != nil, - presentationDelegate: self + supportsEditingStoredPaymentMethods: storedPaymentMethodsDelegate != nil ) } diff --git a/AdyenDropIn/Modules/Root/DropInAssembler.swift b/AdyenDropIn/Modules/Root/DropInAssembler.swift index 32ff972211..27feca615e 100644 --- a/AdyenDropIn/Modules/Root/DropInAssembler.swift +++ b/AdyenDropIn/Modules/Root/DropInAssembler.swift @@ -47,8 +47,7 @@ internal struct DropInAssembler { configuration: configuration, partialPaymentEnabled: false, // TODO: - Set partial payment flow order: nil, - supportsEditingStoredPaymentMethods: false, // TODO: - Support editing stored PMs - presentationDelegate: nil + supportsEditingStoredPaymentMethods: false // TODO: - Support editing stored PMs ) } diff --git a/AdyenDropIn/Utilities/ComponentManager+PaymentComponentBuilder.swift b/AdyenDropIn/Utilities/ComponentManager+PaymentComponentBuilder.swift index 6f34a6b358..f4d6042c04 100644 --- a/AdyenDropIn/Utilities/ComponentManager+PaymentComponentBuilder.swift +++ b/AdyenDropIn/Utilities/ComponentManager+PaymentComponentBuilder.swift @@ -376,13 +376,11 @@ private extension ComponentManager { style: configuration.style.formComponent, localizationParameters: configuration.localizationParameters ) - let bacsDirectDebitComponent = BACSDirectDebitComponent( + return BACSDirectDebitComponent( paymentMethod: paymentMethod, context: context, configuration: bacsConfiguration ) - bacsDirectDebitComponent.presentationDelegate = presentationDelegate - return bacsDirectDebitComponent } func createACHDirectDebitComponent(_ paymentMethod: ACHDirectDebitPaymentMethod) -> ACHDirectDebitComponent { diff --git a/AdyenDropIn/Utilities/ComponentManager.swift b/AdyenDropIn/Utilities/ComponentManager.swift index 1424155c16..a4f3e382b9 100644 --- a/AdyenDropIn/Utilities/ComponentManager.swift +++ b/AdyenDropIn/Utilities/ComponentManager.swift @@ -38,8 +38,7 @@ internal final class ComponentManager: ComponentManaging { internal let context: AdyenContext internal let order: PartialPaymentOrder? internal let partialPaymentEnabled: Bool - internal weak var presentationDelegate: PresentationDelegate? - + private let supportsEditingStoredPaymentMethods: Bool private var localizationParameters: LocalizationParameters? { @@ -58,8 +57,7 @@ internal final class ComponentManager: ComponentManaging { configuration: DropInComponent.Configuration, partialPaymentEnabled: Bool = true, order: PartialPaymentOrder?, - supportsEditingStoredPaymentMethods: Bool = false, - presentationDelegate: PresentationDelegate? + supportsEditingStoredPaymentMethods: Bool = false ) { self.paymentMethods = paymentMethods self.context = context @@ -67,7 +65,6 @@ internal final class ComponentManager: ComponentManaging { self.partialPaymentEnabled = partialPaymentEnabled self.order = order self.supportsEditingStoredPaymentMethods = supportsEditingStoredPaymentMethods - self.presentationDelegate = presentationDelegate updateContextAmountIfNeeded() } From d682f556ceac83c6eb8b5c238eb4d376197cd621 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 12:50:06 +0200 Subject: [PATCH 10/16] Rename submit to performSubmit --- .../BACS Direct Debit/BACSDirectDebitComponent.swift | 5 ++--- AdyenComponents/BACS Direct Debit/BACSViewModel.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift index 9c85906a05..59cb16844e 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitComponent.swift @@ -43,8 +43,8 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo // MARK: - PaymentComponent - package func submit() { - bacsViewModel?.submit() + package func performSubmit() { + bacsViewModel?.performSubmit() } // MARK: - Properties @@ -93,7 +93,6 @@ package final class BACSDirectDebitComponent: PaymentComponent, PresentableCompo onSubmit: { [weak self] details in let data = PaymentComponentData( paymentMethodDetails: details, - amount: self?.context.amount, order: self?.order ) self?.submit(data: data) diff --git a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift index bd61f837e8..4636bc9a1b 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift @@ -67,7 +67,7 @@ internal final class BACSViewModel { submitButtonItem?.showsActivityIndicator = false } - internal func submit() { + internal func performSubmit() { startLoading() guard validateForm() else { @@ -107,7 +107,7 @@ internal final class BACSViewModel { if configuration.showsSubmitButton { submitButtonItem = itemsFactory.createPaymentButton { [weak self] in - self?.submit() + self?.performSubmit() } } From b4bc0c877272234b6d82e20d988d46830970a4cd Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 13:23:15 +0200 Subject: [PATCH 11/16] Remove duplicated methods --- .../BACSDirectDebitDetails.swift | 1 - AdyenUI/UI/Form/FormViewController.swift | 16 ---------------- AdyenUI/UI/Form/FormViewItemManager.swift | 4 ---- 3 files changed, 21 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift index 2ab05a0611..22a65ebef7 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift @@ -32,7 +32,6 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { /// - Parameters: /// - paymentMethod: The BACS Direct Debit payment method. /// - holderName: The BACS account's holder name. - /// - shopperEmail: The BACS account's email. /// - bankAccountNumber: The BACS account's number. /// - bankLocationId: The BACS location's ID. public init( diff --git a/AdyenUI/UI/Form/FormViewController.swift b/AdyenUI/UI/Form/FormViewController.swift index f82d102507..53471358f9 100644 --- a/AdyenUI/UI/Form/FormViewController.swift +++ b/AdyenUI/UI/Form/FormViewController.swift @@ -194,22 +194,6 @@ open class FormViewController: UIViewController, AdyenObserver { formView.appendItemView(itemView) } - private func observerVisibility(of item: some FormItem, and itemView: UIView) { - itemView.adyen.hide( - animationKey: String(describing: itemView), - hidden: item.isHidden.wrappedValue, - animated: false - ) - - observe(item.isHidden) { isHidden in - itemView.adyen.hide( - animationKey: String(describing: itemView), - hidden: isHidden, - animated: true - ) - } - } - private func observerVisibility(of item: any FormItem, and itemView: UIView) { itemView.adyen.hide( animationKey: String(describing: itemView), diff --git a/AdyenUI/UI/Form/FormViewItemManager.swift b/AdyenUI/UI/Form/FormViewItemManager.swift index 80a3df9923..db52289671 100644 --- a/AdyenUI/UI/Form/FormViewItemManager.swift +++ b/AdyenUI/UI/Form/FormViewItemManager.swift @@ -67,10 +67,6 @@ internal final class FormViewItemManager { topLevelItemViews.flatMap(\.flatSubitemViews) } - private func newItemView(for item: some FormItem) -> AnyFormItemView { - item.build(with: FormItemViewBuilder(theme: theme)) - } - private func newItemView(for item: any FormItem) -> AnyFormItemView { item.build(with: FormItemViewBuilder(theme: theme)) } From 2e69f78a44d823acce0685f86d03ee098c623a85 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 13:30:42 +0200 Subject: [PATCH 12/16] Remove published items property --- .../BACS Direct Debit/BACSViewController.swift | 10 ++++------ AdyenComponents/BACS Direct Debit/BACSViewModel.swift | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/BACSViewController.swift index 83b889bdcc..ab8db173ba 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewController.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewController.swift @@ -38,17 +38,15 @@ internal final class BACSViewController: FormViewController { override internal func viewDidLoad() { super.viewDidLoad() - bindItems() - bindValidation() viewModel.viewDidLoad() + bindValidation() + setupItems() } // MARK: - Private - private func bindItems() { - viewModel.$items.sink { [weak self] items in - items.forEach { self?.add(item: $0) } - }.store(in: &cancellables) + private func setupItems() { + viewModel.items.forEach { add(item: $0) } } private func bindValidation() { diff --git a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift index 4636bc9a1b..1a970f4617 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift @@ -24,7 +24,7 @@ internal final class BACSViewModel { // MARK: - State - @Published internal private(set) var items: [(any FormItem)?] = [] + internal private(set) var items: [(any FormItem)?] = [] @Published internal private(set) var shouldShowValidation = false // MARK: - Items From 0161acd9e0e6c47e0fc24b201e493a6ec82c17c3 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 14:19:15 +0200 Subject: [PATCH 13/16] Add items inline instead of function --- AdyenComponents/BACS Direct Debit/BACSViewController.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/BACSViewController.swift index ab8db173ba..9f7a123c1b 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewController.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewController.swift @@ -40,15 +40,11 @@ internal final class BACSViewController: FormViewController { super.viewDidLoad() viewModel.viewDidLoad() bindValidation() - setupItems() + viewModel.items.forEach { add(item: $0) } } // MARK: - Private - private func setupItems() { - viewModel.items.forEach { add(item: $0) } - } - private func bindValidation() { viewModel.$shouldShowValidation.sink { [weak self] shouldShowValidation in if shouldShowValidation { self?.showValidation() } From 76d07239ebf3abcbb792404d5f53ac7a8cffeb16 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 14:55:01 +0200 Subject: [PATCH 14/16] Make items not optional --- AdyenComponents/BACS Direct Debit/BACSViewController.swift | 2 +- AdyenComponents/BACS Direct Debit/BACSViewModel.swift | 7 ++++--- AdyenUI/UI/Form/FormViewController+ViewProtocol.swift | 5 ----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSViewController.swift b/AdyenComponents/BACS Direct Debit/BACSViewController.swift index 9f7a123c1b..b7e50d5e5a 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewController.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewController.swift @@ -40,7 +40,7 @@ internal final class BACSViewController: FormViewController { super.viewDidLoad() viewModel.viewDidLoad() bindValidation() - viewModel.items.forEach { add(item: $0) } + viewModel.items.forEach { append($0) } } // MARK: - Private diff --git a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift index 1a970f4617..e4587b96fd 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift @@ -24,7 +24,7 @@ internal final class BACSViewModel { // MARK: - State - internal private(set) var items: [(any FormItem)?] = [] + internal private(set) var items: [any FormItem] = [] @Published internal private(set) var shouldShowValidation = false // MARK: - Items @@ -97,7 +97,7 @@ internal final class BACSViewModel { submitButtonItem?.showsActivityIndicator = true } - private func createItems() -> [(any FormItem)?] { + private func createItems() -> [any FormItem] { holderNameItem = itemsFactory.createHolderNameItem() bankAccountNumberItem = itemsFactory.createBankAccountNumberItem() sortCodeItem = itemsFactory.createSortCodeItem() @@ -111,7 +111,7 @@ internal final class BACSViewModel { } } - return [ + let allItems: [(any FormItem)?] = [ holderNameItem, bankAccountNumberItem, sortCodeItem, @@ -124,6 +124,7 @@ internal final class BACSViewModel { submitButtonItem, FormSpacerItem(numberOfSpaces: 1) ] + return allItems.compactMap { $0 } } private func validateForm() -> Bool { diff --git a/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift b/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift index 22196b0a30..d179e7a7c3 100644 --- a/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift +++ b/AdyenUI/UI/Form/FormViewController+ViewProtocol.swift @@ -20,11 +20,6 @@ extension FormViewController: FormViewProtocol { append(item) } - package func add(item: (any FormItem)?) { - guard let item else { return } - append(item) - } - package func displayValidation() { resignFirstResponder() showValidation() From 735df2cca94c706a2b98a0cd44171dbb4abfd116 Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 15:52:48 +0200 Subject: [PATCH 15/16] Add shopperEmail in BACS details by making it ShopperInformation --- .../BACSDirectDebitDetails.swift | 16 ++++++++++++++-- .../BACS Direct Debit/BACSViewModel.swift | 3 ++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift index 22a65ebef7..bd82404247 100644 --- a/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift +++ b/AdyenComponents/BACS Direct Debit/BACSDirectDebitDetails.swift @@ -7,7 +7,7 @@ import Adyen /// Contains the details supplied by the BACS Direct Debit component. -public struct BACSDirectDebitDetails: PaymentMethodDetails { +public struct BACSDirectDebitDetails: PaymentMethodDetails, ShopperInformation { @_spi(AdyenInternal) public var checkoutAttemptId: String? @@ -24,6 +24,9 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { /// The BACS location's ID. public let bankLocationId: String + /// The shopper's email address. + public let shopperEmail: String? + /// An encoded string containing important SDK-specific data. /// It is recommended to pass this field to your server to ensure maximum performance and reliability. public var sdkData: String? @@ -34,16 +37,25 @@ public struct BACSDirectDebitDetails: PaymentMethodDetails { /// - holderName: The BACS account's holder name. /// - bankAccountNumber: The BACS account's number. /// - bankLocationId: The BACS location's ID. + /// - shopperEmail: The shopper's email address. public init( paymentMethod: BACSDirectDebitPaymentMethod, holderName: String, bankAccountNumber: String, - bankLocationId: String + bankLocationId: String, + shopperEmail: String? = nil ) { self.type = paymentMethod.type self.holderName = holderName self.bankAccountNumber = bankAccountNumber self.bankLocationId = bankLocationId + self.shopperEmail = shopperEmail + } + + // MARK: - ShopperInformation + + package var emailAddress: String? { + shopperEmail } // MARK: - Private diff --git a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift index e4587b96fd..991aec4538 100644 --- a/AdyenComponents/BACS Direct Debit/BACSViewModel.swift +++ b/AdyenComponents/BACS Direct Debit/BACSViewModel.swift @@ -86,7 +86,8 @@ internal final class BACSViewModel { paymentMethod: paymentMethod, holderName: holderName, bankAccountNumber: bankAccountNumber, - bankLocationId: sortCode + bankLocationId: sortCode, + shopperEmail: emailItem?.value ) onSubmit(details) } From 8f01ff7d9398f41d4e35fb118427ccf52629e10a Mon Sep 17 00:00:00 2001 From: Naufal Aros Date: Tue, 16 Jun 2026 16:14:54 +0200 Subject: [PATCH 16/16] Discard changes in FormViewController --- AdyenUI/UI/Form/FormViewController.swift | 14 +------------- AdyenUI/UI/Form/FormViewItemManager.swift | 16 +--------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/AdyenUI/UI/Form/FormViewController.swift b/AdyenUI/UI/Form/FormViewController.swift index 53471358f9..d07418aeee 100644 --- a/AdyenUI/UI/Form/FormViewController.swift +++ b/AdyenUI/UI/Form/FormViewController.swift @@ -177,24 +177,12 @@ open class FormViewController: UIViewController, AdyenObserver { addItemViewIfNeeded(itemView) } - /// Appends an existential item to the form. - /// - /// - Parameters: - /// - item: The item to append. - @_spi(AdyenInternal) - public func append(_ item: any FormItem) { - let itemView = itemManager.append(item) - observerVisibility(of: item, and: itemView) - itemView.applyTextDelegateIfNeeded(delegate: self) - addItemViewIfNeeded(itemView) - } - private func addItemViewIfNeeded(_ itemView: AnyFormItemView) { guard isViewLoaded else { return } formView.appendItemView(itemView) } - private func observerVisibility(of item: any FormItem, and itemView: UIView) { + private func observerVisibility(of item: some FormItem, and itemView: UIView) { itemView.adyen.hide( animationKey: String(describing: itemView), hidden: item.isHidden.wrappedValue, diff --git a/AdyenUI/UI/Form/FormViewItemManager.swift b/AdyenUI/UI/Form/FormViewItemManager.swift index db52289671..754a11e057 100644 --- a/AdyenUI/UI/Form/FormViewItemManager.swift +++ b/AdyenUI/UI/Form/FormViewItemManager.swift @@ -41,20 +41,6 @@ internal final class FormViewItemManager { return itemView } - - /// Appends an existential item to the list of managed items. - /// - /// - Parameters: - /// - item: The item to append. - /// - Returns: The view instance correspondent to a selected item. - @discardableResult internal func append(_ item: any FormItem) -> AnyFormItemView { - topLevelItem.append(item) - - let itemView = newItemView(for: item) - topLevelItemViews.append(itemView) - - return itemView - } // MARK: - Item Views @@ -67,7 +53,7 @@ internal final class FormViewItemManager { topLevelItemViews.flatMap(\.flatSubitemViews) } - private func newItemView(for item: any FormItem) -> AnyFormItemView { + private func newItemView(for item: some FormItem) -> AnyFormItemView { item.build(with: FormItemViewBuilder(theme: theme)) } }