diff --git a/Adyen.xcodeproj/project.pbxproj b/Adyen.xcodeproj/project.pbxproj index acbb230b96..ac12e12eba 100644 --- a/Adyen.xcodeproj/project.pbxproj +++ b/Adyen.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 000633D92D5A307E004C3FBF /* FormStringPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000633D82D5A307C004C3FBF /* FormStringPickerItem.swift */; }; 000888742D4CF597009C03E1 /* PayToPaymentMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000888732D4CF58D009C03E1 /* PayToPaymentMethod.swift */; }; 000888782D4CF5FC009C03E1 /* PayToComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 000888772D4CF5F5009C03E1 /* PayToComponent.swift */; }; 0008887C2D4CFA73009C03E1 /* PayToComponentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0008887A2D4CFA67009C03E1 /* PayToComponentTests.swift */; }; @@ -1500,6 +1501,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 000633D82D5A307C004C3FBF /* FormStringPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormStringPickerItem.swift; sourceTree = ""; }; 000888732D4CF58D009C03E1 /* PayToPaymentMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayToPaymentMethod.swift; sourceTree = ""; }; 000888772D4CF5F5009C03E1 /* PayToComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayToComponent.swift; sourceTree = ""; }; 0008887A2D4CFA67009C03E1 /* PayToComponentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayToComponentTests.swift; sourceTree = ""; }; @@ -2693,6 +2695,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 000633D72D5A306A004C3FBF /* Identifier Picker */ = { + isa = PBXGroup; + children = ( + 000633D82D5A307C004C3FBF /* FormStringPickerItem.swift */, + ); + path = "Identifier Picker"; + sourceTree = ""; + }; 000888762D4CF5DB009C03E1 /* PayTo */ = { isa = PBXGroup; children = ( @@ -4196,6 +4206,7 @@ E7085B062628B29600D0153B /* Value Pickers */ = { isa = PBXGroup; children = ( + 000633D72D5A306A004C3FBF /* Identifier Picker */, E7085B082628B29600D0153B /* Abstract */, 00EACBBB2876C7D10082B360 /* Issuer List Picker */, ); @@ -6981,6 +6992,7 @@ E7085B132628B29600D0153B /* BaseFormPickerItemView.swift in Sources */, 81C400642A3B0C58007EC51C /* FormSearchButtonItem.swift in Sources */, F9976BA526D654ED00D2D7CE /* Throttler.swift in Sources */, + 000633D92D5A307E004C3FBF /* FormStringPickerItem.swift in Sources */, A0414C302790429A00DF3FE9 /* PublicKeyConsumer.swift in Sources */, 813EF9E82A5FE03B00C65D15 /* ListItem+Icon.swift in Sources */, E28098C4220DC66E0087928F /* ListItemView.swift in Sources */, diff --git a/Adyen/UI/Form/Items/Value Pickers/Identifier Picker/FormStringPickerItem.swift b/Adyen/UI/Form/Items/Value Pickers/Identifier Picker/FormStringPickerItem.swift new file mode 100644 index 0000000000..2b74ba9c17 --- /dev/null +++ b/Adyen/UI/Form/Items/Value Pickers/Identifier Picker/FormStringPickerItem.swift @@ -0,0 +1,55 @@ +// +// Copyright (c) 2025 Adyen N.V. +// +// This file is open source and available under the MIT license. See the LICENSE file for more info. +// + +/// A wrapper struct to use as item in ``FormStringPickerItem`` +@_spi(AdyenInternal) +public struct FormStringPickerElement: CustomStringConvertible, Equatable { + + public let identifier: String + public let title: String + public var description: String { + title + } + + public init(identifier: String, title: String) { + self.identifier = identifier + self.title = title + } +} + +/// A identifier picker form item +@_spi(AdyenInternal) +public final class FormStringPickerItem: BaseFormPickerItem { + + public init( + preselectedStringValue: FormStringPickerElement, + selectableStringValues: [FormStringPickerElement], + style: FormTextItemStyle + ) { + super.init( + preselectedValue: .init( + identifier: preselectedStringValue.identifier, + element: preselectedStringValue + ), + selectableValues: selectableStringValues + .map { + $0.toBaseFormPickerElement() + }, + style: style + ) + } + + override public func build(with builder: FormItemViewBuilder) -> AnyFormItemView { + builder.build(with: self) + } +} + +private extension FormStringPickerElement { + + func toBaseFormPickerElement() -> BasePickerElement { + .init(identifier: identifier, element: self) + } +} diff --git a/AdyenComponents/PayTo/PayToComponent.swift b/AdyenComponents/PayTo/PayToComponent.swift index 972135ff0d..79d48d0926 100644 --- a/AdyenComponents/PayTo/PayToComponent.swift +++ b/AdyenComponents/PayTo/PayToComponent.swift @@ -16,6 +16,31 @@ public final class PayToComponent: PaymentComponent, static let flowSelectionItem = "flowSelectionSegmentedControl" static let phoneNumberItem = "phoneNumberItem" static let continueButtonItem = "continueButton" + static let identifierPickerItem = "identifierPicker" + static let firstNameInputItem = "firstNameTextfield" + static let lastNameInputItem = "lastNameTextfield" + } + + private enum AccountIdentifiers: String, CustomStringConvertible, CaseIterable { + case phone + case email + case abn + case organizationID + + // TODO: Add translation + public var description: String { + switch self { + case .phone: + return "Phone" + case .email: + return "Email" + case .abn: + return "ABN" + case .organizationID: + return "Organization ID" + } + } + } /// Configuration for PayTo Component. @@ -69,7 +94,7 @@ public final class PayToComponent: PaymentComponent, internal lazy var flowSelectionTitleLabelItem: FormLabelItem = { // TODO: Add translation let item = FormLabelItem( - text: "How would you like to use Payto?", + text: localizedString(LocalizationKey(key: "How would you like to use Payto?"), configuration.localizationParameters), style: configuration.style.footnoteLabel ) item.style.textAlignment = .left @@ -82,6 +107,7 @@ public final class PayToComponent: PaymentComponent, /// The segment control item to choose the payTo flow. internal lazy var flowSelectionItem: FormSegmentedControlItem = { + // TODO: Add translation let item = FormSegmentedControlItem( items: ["PayID", "BSB"], style: configuration.style.segmentedControlStyle, @@ -105,11 +131,13 @@ public final class PayToComponent: PaymentComponent, scopeInstance: self, postfix: ViewIdentifier.phoneNumberItem ) + item.title = localizedString(LocalizationKey(key: "Phone"), configuration.localizationParameters) + item.placeholder = localizedString(LocalizationKey(key: "Mobile number"), configuration.localizationParameters) return item }() /// The continue button item. - internal lazy var continueButton: FormButtonItem = { + internal lazy var continueButtonItem: FormButtonItem = { let item = FormButtonItem(style: configuration.style.mainButtonItem) item.identifier = ViewIdentifierBuilder.build( scopeInstance: self, @@ -122,6 +150,54 @@ public final class PayToComponent: PaymentComponent, return item }() + /// The account holder firstname text input item. + internal lazy var firstNameInputItem: FormTextInputItem = { + let item = FormTextInputItem(style: configuration.style.textField) + // TODO: Add translation + item.title = localizedString(LocalizationKey(key: "Account holder first name"), configuration.localizationParameters) + item.placeholder = localizedString(LocalizationKey(key: "Account holder first name"), configuration.localizationParameters) + item.identifier = ViewIdentifierBuilder.build( + scopeInstance: self, + postfix: ViewIdentifier.firstNameInputItem + ) + return item + }() + + /// The account holder lastname text input item. + internal lazy var lastNameInputItem: FormTextInputItem = { + let item = FormTextInputItem(style: configuration.style.textField) + // TODO: Add translation + item.title = localizedString(LocalizationKey(key: "Account holder last name"), configuration.localizationParameters) + item.placeholder = localizedString(LocalizationKey(key: "Account holder last name"), configuration.localizationParameters) + item.identifier = ViewIdentifierBuilder.build( + scopeInstance: self, + postfix: ViewIdentifier.lastNameInputItem + ) + return item + }() + + /// The identifier picker item. + internal lazy var identifierPickerItem: FormStringPickerItem = { + let selectableValues = AccountIdentifiers.allCases.map { accountIdentifier in + FormStringPickerElement(identifier: accountIdentifier.rawValue, title: accountIdentifier.description) + } + + AdyenAssertion.assert(message: "selectableValues should be greater than 0", condition: selectableValues.count <= 0) + + let item = FormStringPickerItem( + preselectedStringValue: selectableValues[0], + selectableStringValues: selectableValues, + style: configuration.style.textField + ) + // TODO: Add translation + item.title = localizedString(LocalizationKey(key: "Identifier"), configuration.localizationParameters) + item.identifier = ViewIdentifierBuilder.build( + scopeInstance: self, + postfix: ViewIdentifier.identifierPickerItem + ) + return item + }() + private lazy var formViewController: FormViewController = { let formViewController = FormViewController( scrollEnabled: configuration.showsSubmitButton, @@ -132,19 +208,38 @@ public final class PayToComponent: PaymentComponent, formViewController.delegate = self formViewController.append(FormSpacerItem(numberOfSpaces: 1)) + formViewController.append(flowSelectionTitleLabelItem.padding()) formViewController.append(FormSpacerItem(numberOfSpaces: 1)) + formViewController.append(flowSelectionItem.padding()) - + formViewController.append(FormSpacerItem(numberOfSpaces: 1)) + + formViewController.append(identifierPickerItem.padding()) + formViewController.append(FormSpacerItem(numberOfSpaces: 1)) + formViewController.append(phoneNumberItem) + appendItemsTo(formVC: formViewController) + if configuration.showsSubmitButton { formViewController.append(FormSpacerItem(numberOfSpaces: 2)) - formViewController.append(continueButton) + formViewController.append(continueButtonItem) } return formViewController }() + + // MARK: - Private + + private func appendItemsTo(formVC: FormViewController) { + staticContent(formVC) + } + + private func staticContent(_ formVC: FormViewController) { + formVC.append(firstNameInputItem) + formVC.append(lastNameInputItem) + } } @_spi(AdyenInternal) diff --git a/Tests/IntegrationTests/Components Tests/PayTo/PayToComponentTests.swift b/Tests/IntegrationTests/Components Tests/PayTo/PayToComponentTests.swift index 669fd577c2..a25bd6a09b 100644 --- a/Tests/IntegrationTests/Components Tests/PayTo/PayToComponentTests.swift +++ b/Tests/IntegrationTests/Components Tests/PayTo/PayToComponentTests.swift @@ -93,4 +93,52 @@ class PayToComponentTests: XCTestCase { XCTAssertNotNil(continueButton, "ContinueButton should exist") } + func test_identifierPicker_exists() throws { + // Given + let sut = try PayToComponent( + paymentMethod: AdyenCoder.decode(payto), + context: Dummy.context + ) + + sut.viewController.loadViewIfNeeded() + + // Check by accessibility identifier + let identifierPickerItem: BaseFormPickerItemView = try XCTUnwrap(sut.viewController.view.findView(with: "AdyenComponents.PayToComponent.identifierPicker")) + + // Then + XCTAssertNotNil(identifierPickerItem, "identifier picker should exist") + } + + func test_firstname_textfield_exists() throws { + // Given + let sut = try PayToComponent( + paymentMethod: AdyenCoder.decode(payto), + context: Dummy.context + ) + + sut.viewController.loadViewIfNeeded() + + // Check by accessibility identifier + let firstNameInputItem: FormTextInputItemView = try XCTUnwrap(sut.viewController.view.findView(with: "AdyenComponents.PayToComponent.firstNameTextfield")) + + // Then + XCTAssertNotNil(firstNameInputItem, "first name input field should exist") + } + + func test_lastname_textfield_exists() throws { + // Given + let sut = try PayToComponent( + paymentMethod: AdyenCoder.decode(payto), + context: Dummy.context + ) + + sut.viewController.loadViewIfNeeded() + + // Check by accessibility identifier + let lastNameInputItem: FormTextInputItemView = try XCTUnwrap(sut.viewController.view.findView(with: "AdyenComponents.PayToComponent.lastNameTextfield")) + + // Then + XCTAssertNotNil(lastNameInputItem, "last name input field should exist") + } + } diff --git a/spell-check-word-allow-list.yaml b/spell-check-word-allow-list.yaml index 717951d7c7..066d5fb70a 100644 --- a/spell-check-word-allow-list.yaml +++ b/spell-check-word-allow-list.yaml @@ -210,3 +210,5 @@ whiteList: - SDKs - ks - payto + - abn + - firstname