From 4b1dcaebd044a4c97d436bda9013511e0baf7694 Mon Sep 17 00:00:00 2001 From: antoniof Date: Fri, 22 Nov 2024 17:20:33 +0100 Subject: [PATCH 1/9] adds PayTo component shell --- packages/lib/src/components/PayTo/PayTo.tsx | 104 ++++++++++++++++++ .../PayTo/components/PayToInput.tsx | 22 ++++ packages/lib/src/components/PayTo/index.ts | 1 + packages/lib/src/components/components-map.ts | 2 + .../lib/src/components/components-name-map.ts | 1 + packages/lib/src/components/tx-variants.ts | 1 + .../stories/components/ANCV.stories.tsx | 24 ---- .../stories/components/PayTo.stories.tsx | 24 ++++ 8 files changed, 155 insertions(+), 24 deletions(-) create mode 100644 packages/lib/src/components/PayTo/PayTo.tsx create mode 100644 packages/lib/src/components/PayTo/components/PayToInput.tsx create mode 100644 packages/lib/src/components/PayTo/index.ts delete mode 100644 packages/lib/storybook/stories/components/ANCV.stories.tsx create mode 100644 packages/lib/storybook/stories/components/PayTo.stories.tsx diff --git a/packages/lib/src/components/PayTo/PayTo.tsx b/packages/lib/src/components/PayTo/PayTo.tsx new file mode 100644 index 0000000000..8d64fc9a85 --- /dev/null +++ b/packages/lib/src/components/PayTo/PayTo.tsx @@ -0,0 +1,104 @@ +import { h } from 'preact'; +import UIElement from '../internal/UIElement/UIElement'; +import { CoreProvider } from '../../core/Context/CoreProvider'; +import Await from '../../components/internal/Await'; +import SRPanelProvider from '../../core/Errors/SRPanelProvider'; +import PayButton from '../internal/PayButton'; + +/* +Types (previously in their own file) + */ +import { UIElementProps } from '../internal/UIElement/types'; +import { TxVariants } from '../tx-variants'; +import PayToInput from './components/PayToInput'; + +export interface PayToConfiguration extends UIElementProps { + paymentData?: any; + data: PayToData; +} + +export interface PayToData { + shopperAccountIdentifier: string; +} + +/* +Await Config (previously in its own file) + */ +const COUNTDOWN_MINUTES = 15; // min +const THROTTLE_TIME = 60000; // ms +const THROTTLE_INTERVAL = 10000; // ms + +const config = { + COUNTDOWN_MINUTES, + THROTTLE_TIME, + THROTTLE_INTERVAL, + showCountdownTimer: false +}; + +/** + * + */ +export class PayToElement extends UIElement { + public static type = TxVariants.payto; + + /** + * Formats the component data output + */ + formatData() { + return { + paymentMethod: { + type: PayToElement.type, + shopperAccountIdentifier: this.state.data?.shopperAccountIdentifier + } + }; + } + + // Reimplement payButton similar to GiftCard to allow to set onClick + public payButton = props => { + return ; + }; + + get isValid(): boolean { + return !!this.state.isValid; + } + + get displayName(): string { + return this.props.name; + } + + render() { + if (this.props.paymentData) { + return ( + + + { + this.componentRef = ref; + }} + clientKey={this.props.clientKey} + paymentData={this.props.paymentData} + onError={this.props.onError} + onComplete={this.onComplete} + brandLogo={this.icon} + type={this.constructor['type']} + messageText={this.props.i18n.get('ancv.confirmPayment')} + awaitText={this.props.i18n.get('await.waitForConfirmation')} + showCountdownTimer={config.showCountdownTimer} + throttleTime={config.THROTTLE_TIME} + throttleInterval={config.THROTTLE_INTERVAL} + onActionHandled={this.onActionHandled} + /> + + + ); + } + + return ( + + + + ); + } +} + +export default PayToElement; diff --git a/packages/lib/src/components/PayTo/components/PayToInput.tsx b/packages/lib/src/components/PayTo/components/PayToInput.tsx new file mode 100644 index 0000000000..09b78f073a --- /dev/null +++ b/packages/lib/src/components/PayTo/components/PayToInput.tsx @@ -0,0 +1,22 @@ +import { h } from 'preact'; +import LoadingWrapper from '../../internal/LoadingWrapper'; + +export default function PayToInput() { + // const { i18n } = useCoreContext(); + + // TODO type this + // const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ + // schema: ['beneficiaryId'] + // }); + // + // const [status, setStatus] = useState('ready'); + + // this.setStatus = setStatus; + // this.showValidation = triggerValidation; + + return ( + +
Input goes here
+
+ ); +} diff --git a/packages/lib/src/components/PayTo/index.ts b/packages/lib/src/components/PayTo/index.ts new file mode 100644 index 0000000000..9286060703 --- /dev/null +++ b/packages/lib/src/components/PayTo/index.ts @@ -0,0 +1 @@ +export { default } from './PayTo'; diff --git a/packages/lib/src/components/components-map.ts b/packages/lib/src/components/components-map.ts index 74ff1adf00..25f216b089 100644 --- a/packages/lib/src/components/components-map.ts +++ b/packages/lib/src/components/components-map.ts @@ -61,6 +61,7 @@ import Trustly from './Trustly'; import Riverty from './Riverty'; import PayByBankUS from './PayByBankUS'; import { TxVariants } from './tx-variants'; +import PayTo from './PayTo/PayTo'; /** * Maps each tx variant to a Component element. @@ -203,6 +204,7 @@ export const ComponentsMap = { [TxVariants.blik]: Blik, [TxVariants.mbway]: MBWay, [TxVariants.ancv]: ANCV, + [TxVariants.payto]: PayTo, [TxVariants.upi]: UPI, // also QR [TxVariants.upi_qr]: UPI, // also QR [TxVariants.upi_collect]: UPI, // also QR diff --git a/packages/lib/src/components/components-name-map.ts b/packages/lib/src/components/components-name-map.ts index 7473e245d5..12fe90de4b 100644 --- a/packages/lib/src/components/components-name-map.ts +++ b/packages/lib/src/components/components-name-map.ts @@ -140,6 +140,7 @@ const ComponentsNameMap = { [TxVariants.blik]: 'Blik', [TxVariants.mbway]: 'MBWay', [TxVariants.ancv]: 'ANCV', + [TxVariants.payto]: 'PayTo', [TxVariants.upi]: 'UPI', // also QR [TxVariants.upi_qr]: 'UPI', // also QR [TxVariants.upi_collect]: 'UPI', // also QR diff --git a/packages/lib/src/components/tx-variants.ts b/packages/lib/src/components/tx-variants.ts index 549a9cb81e..501028b810 100644 --- a/packages/lib/src/components/tx-variants.ts +++ b/packages/lib/src/components/tx-variants.ts @@ -141,6 +141,7 @@ export enum TxVariants { blik = 'blik', mbway = 'mbway', ancv = 'ancv', + payto = 'payto', upi = 'upi', // also QR upi_qr = 'upi_qr', // also QR upi_collect = 'upi_collect', // also QR diff --git a/packages/lib/storybook/stories/components/ANCV.stories.tsx b/packages/lib/storybook/stories/components/ANCV.stories.tsx deleted file mode 100644 index 3fa1718726..0000000000 --- a/packages/lib/storybook/stories/components/ANCV.stories.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Meta, StoryObj } from '@storybook/preact'; -import { PaymentMethodStoryProps } from '../types'; -import { ComponentContainer } from '../ComponentContainer'; -import { ANCVConfiguration } from '../../../src/components/ANCV/types'; -import ANCV from '../../../src/components/ANCV/ANCV'; -import { Checkout } from '../Checkout'; - -type ANCVStory = StoryObj>; - -const meta: Meta> = { - title: 'Components/ANCV' -}; - -export const Default: ANCVStory = { - render: ({ componentConfiguration, ...checkoutConfig }) => ( - {checkout => } - ), - args: { - countryCode: 'NL', - amount: 2000, - useSessions: false - } -}; -export default meta; diff --git a/packages/lib/storybook/stories/components/PayTo.stories.tsx b/packages/lib/storybook/stories/components/PayTo.stories.tsx new file mode 100644 index 0000000000..c5806e779a --- /dev/null +++ b/packages/lib/storybook/stories/components/PayTo.stories.tsx @@ -0,0 +1,24 @@ +import { Meta, StoryObj } from '@storybook/preact'; +import { PaymentMethodStoryProps } from '../types'; +import { ComponentContainer } from '../ComponentContainer'; +import { Checkout } from '../Checkout'; +import PayTo, { PayToConfiguration } from '../../../src/components/PayTo/PayTo'; + +type PayToStory = StoryObj>; + +const meta: Meta> = { + title: 'Components/PayTo' +}; + +export const Default: PayToStory = { + render: ({ componentConfiguration, ...checkoutConfig }) => ( + + {checkout => } + + ), + args: { + countryCode: 'AU', + useSessions: false + } +}; +export default meta; From 8d4296b89179f476feb4836608214c358c3907ef Mon Sep 17 00:00:00 2001 From: antoniof Date: Mon, 25 Nov 2024 17:08:20 +0100 Subject: [PATCH 2/9] payto segmented controller --- .../components/PayTo/components/BSBInput.tsx | 17 ++++++++++ .../PayTo/components/PayIDInput.tsx | 17 ++++++++++ .../PayTo/components/PayToInput.tsx | 33 ++++++++++++++++++- .../SegmentedControl/SegmentedControl.tsx | 31 ++++++++++++++++- 4 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 packages/lib/src/components/PayTo/components/BSBInput.tsx create mode 100644 packages/lib/src/components/PayTo/components/PayIDInput.tsx diff --git a/packages/lib/src/components/PayTo/components/BSBInput.tsx b/packages/lib/src/components/PayTo/components/BSBInput.tsx new file mode 100644 index 0000000000..e8257a09b8 --- /dev/null +++ b/packages/lib/src/components/PayTo/components/BSBInput.tsx @@ -0,0 +1,17 @@ +import { h } from 'preact'; + +export default function BSBInput() { + // const { i18n } = useCoreContext(); + + // TODO type this + // const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ + // schema: ['beneficiaryId'] + // }); + // + // const [status, setStatus] = useState('ready'); + + // this.setStatus = setStatus; + // this.showValidation = triggerValidation; + + return

BSBInput.tsx

; +} diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.tsx b/packages/lib/src/components/PayTo/components/PayIDInput.tsx new file mode 100644 index 0000000000..aa68ea5304 --- /dev/null +++ b/packages/lib/src/components/PayTo/components/PayIDInput.tsx @@ -0,0 +1,17 @@ +import { h } from 'preact'; + +export default function PayIDInput() { + // const { i18n } = useCoreContext(); + + // TODO type this + // const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ + // schema: ['beneficiaryId'] + // }); + // + // const [status, setStatus] = useState('ready'); + + // this.setStatus = setStatus; + // this.showValidation = triggerValidation; + + return

PayIDInput.tsx

; +} diff --git a/packages/lib/src/components/PayTo/components/PayToInput.tsx b/packages/lib/src/components/PayTo/components/PayToInput.tsx index 09b78f073a..2c2a601225 100644 --- a/packages/lib/src/components/PayTo/components/PayToInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayToInput.tsx @@ -1,5 +1,31 @@ import { h } from 'preact'; import LoadingWrapper from '../../internal/LoadingWrapper'; +import SegmentedControl from '../../internal/SegmentedControl'; +import { useState } from 'preact/hooks'; +import { SegmentedControlOptions } from '../../internal/SegmentedControl/SegmentedControl'; +import PayIDInput from './PayIDInput'; +import BSBInput from './BSBInput'; + +const inputOptions: SegmentedControlOptions = [ + { + value: 'payid-option', + label: 'PayID', + htmlProps: { + id: 'payid-option', // TODO move this to i18n + 'aria-controls': 'payid-input', + 'aria-expanded': true // TODO move this logic to segmented controller + } + }, + { + value: 'bsb-option', + label: 'BSB and account number', // TODO move this to i18n + htmlProps: { + id: 'bsb-option', + 'aria-controls': 'bsb-input', + 'aria-expanded': false // TODO move this logic to segmented controller + } + } +]; export default function PayToInput() { // const { i18n } = useCoreContext(); @@ -14,9 +40,14 @@ export default function PayToInput() { // this.setStatus = setStatus; // this.showValidation = triggerValidation; + const defaultOption = inputOptions[0].value; + const [selectedInput, setSelectedInput] = useState(defaultOption); + return ( -
Input goes here
+ + {selectedInput === 'payid-option' && } + {selectedInput === 'bsb-option' && }
); } diff --git a/packages/lib/src/components/internal/SegmentedControl/SegmentedControl.tsx b/packages/lib/src/components/internal/SegmentedControl/SegmentedControl.tsx index 61a509a845..da3213622a 100644 --- a/packages/lib/src/components/internal/SegmentedControl/SegmentedControl.tsx +++ b/packages/lib/src/components/internal/SegmentedControl/SegmentedControl.tsx @@ -2,14 +2,43 @@ import { h } from 'preact'; import cx from 'classnames'; import './SegmentedControl.scss'; +export interface SegmentedControlOption { + label: string; + value: T; + htmlProps: { + id: string; + 'aria-expanded': boolean; + 'aria-controls': string; + }; +} +export type SegmentedControlOptions = Array>; + export interface SegmentedControlProps { classNameModifiers?: string[]; selectedValue: T; disabled?: boolean; - options: Array<{ label: string; value: T; htmlProps?: any }>; + options: SegmentedControlOptions; onChange(value: T, event: MouseEvent): void; } +/** + * + * example: + * + * + * @param classNameModifiers + * @param selectedValue + * @param disabled + * @param options + * @param onChange + * @constructor + */ function SegmentedControl({ classNameModifiers = [], selectedValue, disabled = false, options, onChange }: SegmentedControlProps) { if (!options || options.length === 0) { return null; From 16c9a118e33bf230ef19c83a3fb84f31e4a0aeb4 Mon Sep 17 00:00:00 2001 From: antoniof Date: Wed, 27 Nov 2024 17:10:15 +0100 Subject: [PATCH 3/9] add support to second description --- .../components/PayTo/components/PayIDInput.scss | 3 +++ .../components/PayTo/components/PayIDInput.tsx | 8 +++++++- .../internal/FormFields/Fieldset/Fieldset.scss | 15 ++++++++++++++- .../internal/FormFields/Fieldset/Fieldset.tsx | 13 +++++++++++-- packages/server/translations/en-US.json | 4 +++- 5 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 packages/lib/src/components/PayTo/components/PayIDInput.scss diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.scss b/packages/lib/src/components/PayTo/components/PayIDInput.scss new file mode 100644 index 0000000000..4eac4f7ab2 --- /dev/null +++ b/packages/lib/src/components/PayTo/components/PayIDInput.scss @@ -0,0 +1,3 @@ +/* +This linter is annoying + */ \ No newline at end of file diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.tsx b/packages/lib/src/components/PayTo/components/PayIDInput.tsx index aa68ea5304..7e4be7b466 100644 --- a/packages/lib/src/components/PayTo/components/PayIDInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayIDInput.tsx @@ -1,4 +1,6 @@ import { h } from 'preact'; +//import { useCoreContext } from '../../../core/Context/CoreProvider'; +import Fieldset from '../../internal/FormFields/Fieldset'; export default function PayIDInput() { // const { i18n } = useCoreContext(); @@ -13,5 +15,9 @@ export default function PayIDInput() { // this.setStatus = setStatus; // this.showValidation = triggerValidation; - return

PayIDInput.tsx

; + return ( +
+

+
+ ); } diff --git a/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.scss b/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.scss index a38162985d..60935c080c 100644 --- a/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.scss +++ b/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.scss @@ -13,6 +13,15 @@ padding-block-end: 0; padding-inline-start: 0; padding-inline-end: 0; + + &__description { + font-weight: token(text-body-font-weight); + list-style-type: disc; + color: token(color-label-secondary); + margin: 0; + font-size: token(text-body-font-size); + line-height: 1.5; + } } .adyen-checkout__fieldset:last-of-type { @@ -31,6 +40,10 @@ display: block; margin: 0; padding: 0 0 token(spacer-060); + + &:has(+ .adyen-checkout__fieldset__description) { + padding: 0; + } } .adyen-checkout__fieldset__fields, @@ -60,4 +73,4 @@ font-size: token(text-body-font-size); line-height: token(text-caption-line-height); margin: 0; -} +} \ No newline at end of file diff --git a/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.tsx b/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.tsx index b2b8c6a90d..913fb6fec7 100644 --- a/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.tsx +++ b/packages/lib/src/components/internal/FormFields/Fieldset/Fieldset.tsx @@ -2,17 +2,21 @@ import { h, ComponentChildren } from 'preact'; import cx from 'classnames'; import { useCoreContext } from '../../../../core/Context/CoreProvider'; import './Fieldset.scss'; +import { getUniqueId } from '../../../../utils/idGenerator'; interface FieldsetProps { children: ComponentChildren; classNameModifiers: string[]; label?: string; + description?: string; readonly?: boolean; } -export default function Fieldset({ children, classNameModifiers = [], label, readonly = false }: FieldsetProps) { +export default function Fieldset({ children, classNameModifiers = [], label, readonly = false, description }: FieldsetProps) { const { i18n } = useCoreContext(); + const describedById = getUniqueId('payid-input-description'); + return (
`adyen-checkout__fieldset--${m}`), { 'adyen-checkout__fieldset--readonly': readonly } ])} + aria-describedby={description ? describedById : null} > {label && {i18n.get(label)}} - + {description && ( +

+ {i18n.get(description)} +

+ )}
{children}
); diff --git a/packages/server/translations/en-US.json b/packages/server/translations/en-US.json index d61909117a..4b2fbbb284 100644 --- a/packages/server/translations/en-US.json +++ b/packages/server/translations/en-US.json @@ -324,5 +324,7 @@ "paynow.mobileViewInstruction.step2": "Open the PayNow bank or payment app.", "paynow.mobileViewInstruction.step3": "Select the option to scan a QR code.", "paynow.mobileViewInstruction.step4": "Choose the option to upload a QR and select the screenshot.", - "paynow.mobileViewInstruction.step5": "Complete the transaction." + "paynow.mobileViewInstruction.step5": "Complete the transaction.", + "payto.payid.header": "PayID", + "payto.payid.description": "Enter the PayID and account details that are connected to your Payto account." } \ No newline at end of file From 1a61933215a07f4e68552b8319aa9aa73bf00242 Mon Sep 17 00:00:00 2001 From: antoniof Date: Fri, 29 Nov 2024 17:53:51 +0100 Subject: [PATCH 4/9] adds email and refactor identifier to enum --- .../PayTo/components/IdentifierSelector.tsx | 79 +++++++++++++++++++ .../PayTo/components/PayIDInput.tsx | 51 +++++++++--- .../PayTo/components/PayToPhone.tsx | 21 +++++ .../internal/PhoneInput/PhoneInput.tsx | 5 ++ packages/lib/src/core/utils.ts | 11 +++ packages/lib/src/utils/useForm/types.ts | 4 +- packages/server/translations/en-US.json | 7 +- 7 files changed, 167 insertions(+), 11 deletions(-) create mode 100644 packages/lib/src/components/PayTo/components/IdentifierSelector.tsx create mode 100644 packages/lib/src/components/PayTo/components/PayToPhone.tsx diff --git a/packages/lib/src/components/PayTo/components/IdentifierSelector.tsx b/packages/lib/src/components/PayTo/components/IdentifierSelector.tsx new file mode 100644 index 0000000000..dbc8577b20 --- /dev/null +++ b/packages/lib/src/components/PayTo/components/IdentifierSelector.tsx @@ -0,0 +1,79 @@ +import { h } from 'preact'; +import Select from '../../internal/FormFields/Select'; +import { SelectTargetObject } from '../../internal/FormFields/Select/types'; +import Field from '../../internal/FormFields/Field'; +import { useCoreContext } from '../../../core/Context/CoreProvider'; +import Language from '../../../language'; +import { createEnumChecker } from '../../../core/utils'; + +export enum PayToIdentifierEnum { + phone = 'phone', + email = 'email', + abn = 'abn', + orgid = 'orgid' +} + +const payToIdentifierEnumCheck = createEnumChecker(PayToIdentifierEnum); + +export type PayToPayIDInputIdentifierValues = keyof typeof PayToIdentifierEnum; + +type PayIdOptionsType = { id: PayToPayIDInputIdentifierValues; nameKey: string }[]; + +export const PAYID_IDENTIFIER_OPTIONS: PayIdOptionsType = [ + { + id: PayToIdentifierEnum.phone, + nameKey: 'payto.payid.option.phone' + }, + { + id: PayToIdentifierEnum.email, + nameKey: 'payto.payid.option.email' + }, + { + id: PayToIdentifierEnum.abn, + nameKey: 'payto.payid.option.abn' + }, + { + id: PayToIdentifierEnum.orgid, + nameKey: 'payto.payid.option.orgid' + } +]; + +interface IdentifierSelectorProps { + selectedIdentifier: PayToPayIDInputIdentifierValues; + onSelectedIdentifier: (value: PayToPayIDInputIdentifierValues) => void; +} + +const loadI18nForOptions = (i18n: Language, options: PayIdOptionsType) => + options.map(option => ({ + id: option.id, + name: i18n.get(option.nameKey) + })); + +export default function IdentifierSelector({ selectedIdentifier, onSelectedIdentifier }: IdentifierSelectorProps) { + const { i18n } = useCoreContext(); + + const hydratedOptions = loadI18nForOptions(i18n, PAYID_IDENTIFIER_OPTIONS); + + // TODO this probably can by a bit tidier, clean up some of these types + // maybe make Select type generic + const onChange = (e: { target: SelectTargetObject }) => { + // TODO clean this + const valueStr = e.target.value + ''; + + if (payToIdentifierEnumCheck(valueStr)) { + onSelectedIdentifier(valueStr); + } + }; + + return ( + + diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.tsx b/packages/lib/src/components/PayTo/components/PayIDInput.tsx index c2a7b8f29a..8672beaa1a 100644 --- a/packages/lib/src/components/PayTo/components/PayIDInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayIDInput.tsx @@ -1,56 +1,206 @@ import { h } from 'preact'; import Fieldset from '../../internal/FormFields/Fieldset'; import IdentifierSelector, { PayToIdentifierEnum, PayToPayIDInputIdentifierValues } from './IdentifierSelector'; -import { useState } from 'preact/hooks'; +import { useEffect, useState } from 'preact/hooks'; import useForm from '../../../utils/useForm'; import PayToPhone from './PayToPhone'; import InputEmail from '../../internal/FormFields/InputEmail'; import { getErrorMessage } from '../../../utils/getErrorMessage'; import Field from '../../internal/FormFields/Field'; import { useCoreContext } from '../../../core/Context/CoreProvider'; +import InputText from '../../internal/FormFields/InputText'; +import { ValidatorRule, ValidatorRules } from '../../../utils/Validator/types'; +import { ERROR_FIELD_INVALID, ERROR_FIELD_REQUIRED } from '../../../core/Errors/constants'; +import { isEmpty } from '../../../utils/validator-utils'; +import { validationRules } from '../../../utils/Validator/defaultRules'; + +export interface PayIdFormData { + email: string; + phone: string; + abn: string; + orgid: string; + firstName: string; + lastName: string; +} + +//const emailRegex = /^_`{|}~-]+)@(?:[a-z0-9](?:[a-z0-9-][a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/; + +const abnRegex = /^((\d{9})|(\d{11}))$/; + +const orgidRegex = /`^[!-@[-~][ -@[-~]{0,254}[!-@[-~]$`/; + +// TODO probably move this generic function somewhere else +const createValidationFuncFromRegex = + (regex: RegExp) => + (value: string, validationRule: ValidatorRule): boolean | null => { + if (isEmpty(value)) { + validationRule.errorMessage = ERROR_FIELD_REQUIRED; + return null; + } + validationRule.errorMessage = ERROR_FIELD_INVALID; + return regex.test(value); + }; + +export const payIdValidationRules: ValidatorRules = { + default: { + validate: value => { + return value && value.length > 0; + }, + errorMessage: ERROR_FIELD_REQUIRED, + modes: ['blur'] + }, + email: { + //TODO check this + ...validationRules.emailRule + }, + abn: { + validate: createValidationFuncFromRegex(abnRegex), + errorMessage: 'abn.invalid', + modes: ['blur'] + }, + orgid: { + validate: createValidationFuncFromRegex(orgidRegex), + errorMessage: 'orgid.invalid', + modes: ['blur'] + }, + firstName: { + validate: value => (isEmpty(value) ? null : true), // valid, if there are chars other than spaces, + errorMessage: 'firstName.invalid', + modes: ['blur'] + }, + lastName: { + validate: value => (isEmpty(value) ? null : true), + errorMessage: 'lastName.invalid', + modes: ['blur'] + } +}; export default function PayIDInput(props) { const { i18n } = useCoreContext(); - // TODO type this - const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ - schema: ['beneficiaryId'] + const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ + schema: ['email', 'abn', 'phone', 'orgid'], + rules: payIdValidationRules }); // this.setStatus = setStatus; // this.showValidation = triggerValidation; - const [selectedIdentifier, setSelectedIdentifier] = useState(PayToIdentifierEnum.phone); + // TODO fix all these letters + // TODO double check if what should be default value + const [selectedIdentifier, setSelectedIdentifier] = useState(PayToIdentifierEnum.email); console.log(selectedIdentifier); + useEffect(() => { + console.log(data); + }, [data]); + return (
- + {selectedIdentifier === PayToIdentifierEnum.phone && ( )} {selectedIdentifier === PayToIdentifierEnum.email && ( -
- + - - -
+ value={data.email} + onInput={handleChangeFor('email', 'input')} + onBlur={handleChangeFor('email', 'blur')} + //TODO placeholder={placeholders.shopperEmail} + required={true} + /> + + )} + + {selectedIdentifier === PayToIdentifierEnum.abn && ( + + + )} + + {selectedIdentifier === PayToIdentifierEnum.orgid && ( + + + + )} + + + + + + + +
); } diff --git a/packages/lib/src/components/internal/FormFields/FormFields.scss b/packages/lib/src/components/internal/FormFields/FormFields.scss index 4c880c6d90..34d424f6be 100644 --- a/packages/lib/src/components/internal/FormFields/FormFields.scss +++ b/packages/lib/src/components/internal/FormFields/FormFields.scss @@ -76,9 +76,9 @@ $deduct-width: token(spacer-040); -.adyen-checkout__field--col-70 { +.adyen-checkout__field--col-20 { @include index.screen-s-and-up { - width: calc(70% - $deduct-width); + width: calc(20% - $deduct-width); } } @@ -88,12 +88,37 @@ $deduct-width: token(spacer-040); } } +.adyen-checkout__field--col-40 { + @include index.screen-s-and-up { + width: calc(40% - $deduct-width); + } +} + .adyen-checkout__field--col-50 { @include index.screen-s-and-up { width: calc(50% - $deduct-width); } } +.adyen-checkout__field--col-60 { + @include index.screen-s-and-up { + width: calc(60% - $deduct-width); + } +} + +.adyen-checkout__field--col-70 { + @include index.screen-s-and-up { + width: calc(70% - $deduct-width); + } +} + +.adyen-checkout__field--col-80 { + @include index.screen-s-and-up { + width: calc(80% - $deduct-width); + } +} + + .adyen-checkout__field-wrapper > .adyen-checkout__field:first-child { margin-right: token(spacer-040); From e363822fc79e67251ea71f3b284d18e2a9eba17c Mon Sep 17 00:00:00 2001 From: antoniof Date: Fri, 20 Dec 2024 21:00:13 +0100 Subject: [PATCH 6/9] fix validator --- .../PayTo/components/PayIDInput.tsx | 79 +++---------------- .../PayTo/components/validate.test.ts | 64 +++++++++++++++ .../components/PayTo/components/validate.ts | 61 ++++++++++++++ .../utils/Validator/ValidationRuleResult.ts | 4 +- packages/lib/src/utils/Validator/Validator.ts | 4 + packages/lib/src/utils/Validator/types.ts | 6 +- packages/server/translations/en-US.json | 2 + 7 files changed, 149 insertions(+), 71 deletions(-) create mode 100644 packages/lib/src/components/PayTo/components/validate.test.ts create mode 100644 packages/lib/src/components/PayTo/components/validate.ts diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.tsx b/packages/lib/src/components/PayTo/components/PayIDInput.tsx index 8672beaa1a..e3d6ef4e76 100644 --- a/packages/lib/src/components/PayTo/components/PayIDInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayIDInput.tsx @@ -9,10 +9,7 @@ import { getErrorMessage } from '../../../utils/getErrorMessage'; import Field from '../../internal/FormFields/Field'; import { useCoreContext } from '../../../core/Context/CoreProvider'; import InputText from '../../internal/FormFields/InputText'; -import { ValidatorRule, ValidatorRules } from '../../../utils/Validator/types'; -import { ERROR_FIELD_INVALID, ERROR_FIELD_REQUIRED } from '../../../core/Errors/constants'; -import { isEmpty } from '../../../utils/validator-utils'; -import { validationRules } from '../../../utils/Validator/defaultRules'; +import { payIdValidationRules } from './validate'; export interface PayIdFormData { email: string; @@ -23,70 +20,18 @@ export interface PayIdFormData { lastName: string; } -//const emailRegex = /^_`{|}~-]+)@(?:[a-z0-9](?:[a-z0-9-][a-z0-9])?.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/; - -const abnRegex = /^((\d{9})|(\d{11}))$/; - -const orgidRegex = /`^[!-@[-~][ -@[-~]{0,254}[!-@[-~]$`/; - -// TODO probably move this generic function somewhere else -const createValidationFuncFromRegex = - (regex: RegExp) => - (value: string, validationRule: ValidatorRule): boolean | null => { - if (isEmpty(value)) { - validationRule.errorMessage = ERROR_FIELD_REQUIRED; - return null; - } - validationRule.errorMessage = ERROR_FIELD_INVALID; - return regex.test(value); - }; - -export const payIdValidationRules: ValidatorRules = { - default: { - validate: value => { - return value && value.length > 0; - }, - errorMessage: ERROR_FIELD_REQUIRED, - modes: ['blur'] - }, - email: { - //TODO check this - ...validationRules.emailRule - }, - abn: { - validate: createValidationFuncFromRegex(abnRegex), - errorMessage: 'abn.invalid', - modes: ['blur'] - }, - orgid: { - validate: createValidationFuncFromRegex(orgidRegex), - errorMessage: 'orgid.invalid', - modes: ['blur'] - }, - firstName: { - validate: value => (isEmpty(value) ? null : true), // valid, if there are chars other than spaces, - errorMessage: 'firstName.invalid', - modes: ['blur'] - }, - lastName: { - validate: value => (isEmpty(value) ? null : true), - errorMessage: 'lastName.invalid', - modes: ['blur'] - } -}; - export default function PayIDInput(props) { const { i18n } = useCoreContext(); - const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ + const { handleChangeFor, triggerValidation, data, errors } = useForm({ schema: ['email', 'abn', 'phone', 'orgid'], rules: payIdValidationRules }); - // this.setStatus = setStatus; - // this.showValidation = triggerValidation; + //this.setStatus = setStatus; + this.showValidation = triggerValidation; - // TODO fix all these letters + // TODO fix all these letters - as in this is quite a long class // TODO double check if what should be default value const [selectedIdentifier, setSelectedIdentifier] = useState(PayToIdentifierEnum.email); console.log(selectedIdentifier); @@ -110,7 +55,7 @@ export default function PayIDInput(props) { diff --git a/packages/lib/src/components/PayTo/components/validate.test.ts b/packages/lib/src/components/PayTo/components/validate.test.ts new file mode 100644 index 0000000000..56bdad08ae --- /dev/null +++ b/packages/lib/src/components/PayTo/components/validate.test.ts @@ -0,0 +1,64 @@ +import Validator from '../../../utils/Validator'; +import { payIdValidationRules } from './validate'; + +describe('Test payIdValidationRules', () => { + const validator = new Validator(payIdValidationRules); + + // Email tests + test('Test success email', () => { + expect(validator.validate({ key: 'email', value: 'example@example.com' }).hasError()).toBe(false); + }); + + test('Test sucesss email (empty)', () => { + // Empty validation only should occur on useForm level and not on the field + expect(validator.validate({ key: 'email', value: '' }).hasError()).toBe(false); + }); + + test('Test failure email (invalid format)', () => { + expect(validator.validate({ key: 'email', value: 'invalid-email' }).hasError()).toBe(true); + }); + + test('Test failure email (missing domain)', () => { + expect(validator.validate({ key: 'email', value: 'user@.com' }).hasError()).toBe(true); + }); + + // ABN tests + test('Test success abn (9 digits)', () => { + expect(validator.validate({ key: 'abn', value: '123456789' }).hasError()).toBe(false); + }); + + test('Test success abn (11 digits)', () => { + expect(validator.validate({ key: 'abn', value: '12345678901' }).hasError()).toBe(false); + }); + + test('Test failure abn (empty)', () => { + // Empty validation only should occur on useForm level and not on the field + expect(validator.validate({ key: 'abn', value: '' }).hasError()).toBe(false); + }); + + test('Test failure abn (non-numeric characters)', () => { + expect(validator.validate({ key: 'abn', value: '123abc789' }).hasError()).toBe(true); + }); + + test('Test failure abn (too short)', () => { + expect(validator.validate({ key: 'abn', value: '12345678' }).hasError()).toBe(true); + }); + + test('Test failure abn (too long)', () => { + expect(validator.validate({ key: 'abn', value: '123456789012' }).hasError()).toBe(true); + }); + + test('Test failure orgid (empty)', () => { + // Empty validation only should occur on useForm level and not on the field + expect(validator.validate({ key: 'orgid', value: '' }).hasError()).toBe(false); + }); + + test('Test failure orgid (too long)', () => { + const longOrgID = 'A'.repeat(256); // 256 characters + expect(validator.validate({ key: 'orgid', value: longOrgID }).hasError()).toBe(true); + }); + + test('Test failure orgid (invalid characters)', () => { + expect(validator.validate({ key: 'orgid', value: 'InvalidOrgID<>?' }).hasError()).toBe(true); + }); +}); diff --git a/packages/lib/src/components/PayTo/components/validate.ts b/packages/lib/src/components/PayTo/components/validate.ts new file mode 100644 index 0000000000..3e1aa0481a --- /dev/null +++ b/packages/lib/src/components/PayTo/components/validate.ts @@ -0,0 +1,61 @@ +import { ValidatorRule, ValidatorRules } from '../../../utils/Validator/types'; +import { isEmpty } from '../../../utils/validator-utils'; +import { ERROR_FIELD_INVALID, ERROR_FIELD_REQUIRED } from '../../../core/Errors/constants'; + +const abnRegex = /^((\d{9})|(\d{11}))$/; + +const orgidRegex = /`^[!-@[-~][ -@[-~]{0,254}[!-@[-~]$`/; + +const emailRegex = + /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/; + +export const validationFromRegex = (value: string, regex: RegExp, validationRule: ValidatorRule): boolean | null => { + // TODO investigate why null is the return value for 'empty' validation + if (isEmpty(value)) { + validationRule.errorMessage = ERROR_FIELD_REQUIRED; + return null; + } + validationRule.errorMessage = ERROR_FIELD_INVALID; + return regex.test(value); +}; + +const emailValidatorRule: ValidatorRule = { + validate: value => validationFromRegex(value, emailRegex, emailValidatorRule), + errorMessage: 'abn.invalid', + modes: ['blur'] +}; + +const abnValidatorRule: ValidatorRule = { + validate: value => validationFromRegex(value, abnRegex, abnValidatorRule), + errorMessage: 'abn.invalid', + modes: ['blur'] +}; + +const orgidValidatorRule: ValidatorRule = { + validate: value => validationFromRegex(value, orgidRegex, orgidValidatorRule), + errorMessage: 'orgid.invalid', + modes: ['blur'] +}; + +export const payIdValidationRules: ValidatorRules = { + default: { + validate: value => { + return value && value.length > 0; + }, + errorMessage: ERROR_FIELD_REQUIRED, + modes: ['blur'] + }, + email: emailValidatorRule, + abn: abnValidatorRule, + orgid: orgidValidatorRule, + firstName: { + validate: value => (isEmpty(value) ? null : true), // valid, if there are chars other than spaces, + errorMessage: 'firstName.invalid', + modes: ['blur'] + }, + lastName: { + validate: value => (isEmpty(value) ? null : true), + errorMessage: 'lastName.invalid', + modes: ['blur'] + } +}; diff --git a/packages/lib/src/utils/Validator/ValidationRuleResult.ts b/packages/lib/src/utils/Validator/ValidationRuleResult.ts index 5ed858ed2e..72f72e8f1b 100644 --- a/packages/lib/src/utils/Validator/ValidationRuleResult.ts +++ b/packages/lib/src/utils/Validator/ValidationRuleResult.ts @@ -1,4 +1,4 @@ -import { ErrorMessageObject } from './types'; +import { ErrorMessageObject, ValidatorRule, ValidatorMode } from './types'; /** * Holds the result of a validation @@ -8,7 +8,7 @@ export class ValidationRuleResult { public isValid: boolean; public errorMessage: string | ErrorMessageObject; - constructor(rule, value, mode, context) { + constructor(rule: ValidatorRule, value: string, mode: ValidatorMode, context) { this.shouldValidate = rule.modes.includes(mode); this.isValid = rule.validate(value, context); this.errorMessage = rule.errorMessage; diff --git a/packages/lib/src/utils/Validator/Validator.ts b/packages/lib/src/utils/Validator/Validator.ts index 6b5cac5b9c..314f98819e 100644 --- a/packages/lib/src/utils/Validator/Validator.ts +++ b/packages/lib/src/utils/Validator/Validator.ts @@ -66,6 +66,10 @@ class Validator { */ validate({ key, value, mode = 'blur' }: FieldData, context?: FieldContext) { const fieldRules = this.getRulesFor(key); + // create an ValidationRuleResult, we run the actual validation inside of it + // validate is called in the constructor of ValidationRuleResult + // line rule.validate(value, context); + // const validationRulesResult = fieldRules.map(rule => new ValidationRuleResult(rule, value, mode, context)); return new ValidationResult(validationRulesResult); diff --git a/packages/lib/src/utils/Validator/types.ts b/packages/lib/src/utils/Validator/types.ts index cf9807a653..80152ff8e7 100644 --- a/packages/lib/src/utils/Validator/types.ts +++ b/packages/lib/src/utils/Validator/types.ts @@ -1,7 +1,7 @@ import { ValidationRuleResult } from './ValidationRuleResult'; import { Formatter } from '../useForm/types'; -type ValidatorMode = 'blur' | 'input'; +export type ValidatorMode = 'blur' | 'input'; export type ErrorMessageObject = { translationKey: string; @@ -20,8 +20,10 @@ export type FormatRules = { [field: string]: Formatter }; export type CountryFormatRules = { [country: string]: FormatRules }; +export type ValidateFunction = (value: string, context) => boolean; + export interface ValidatorRule { - validate: (value, context?) => boolean; + validate: ValidateFunction; errorMessage?: string | ErrorMessageObject; modes: ValidatorMode[]; } diff --git a/packages/server/translations/en-US.json b/packages/server/translations/en-US.json index 1e4a95515f..0daa27cb21 100644 --- a/packages/server/translations/en-US.json +++ b/packages/server/translations/en-US.json @@ -328,6 +328,8 @@ "payto.payid.header": "PayID", "payto.payid.description": "Enter the PayID and account details that are connected to your Payto account.", "payto.payid.label.identifier": "Identifier", + "payto.payid.label.abn": "ABN", + "payto.payid.label.orgid": "Organization ID", "payto.payid.option.phone": "Mobile", "payto.payid.option.email": "Email", "payto.payid.option.abn": "ABN", From f32ddec3641bb42324cb7e5e8ca794a111b7cc3b Mon Sep 17 00:00:00 2001 From: antoniof Date: Wed, 8 Jan 2025 16:25:20 +0100 Subject: [PATCH 7/9] splits PhoneInput and passes state to PayTo --- .../Ach/components/AchInput/AchInput.tsx | 2 + .../components/MBWayInput/MBWayInput.tsx | 4 +- packages/lib/src/components/PayTo/PayTo.tsx | 49 ++++++++-- .../PayTo/components/PayIDInput.scss | 13 ++- .../PayTo/components/PayIDInput.tsx | 98 +++++++++++++------ .../PayTo/components/PayToInput.tsx | 31 +++--- .../PayTo/components/PayToPhone.tsx | 21 +++- .../components/PayTo/components/validate.ts | 15 +++ .../CompanyDetails/CompanyDetails.tsx | 5 +- .../PersonalDetails/PersonalDetails.tsx | 3 +- .../{PhoneInput.tsx => PhoneInputFields.tsx} | 78 ++++++--------- ...Input.test.tsx => PhoneInputForm.test.tsx} | 10 +- .../internal/PhoneInput/PhoneInputForm.tsx | 71 ++++++++++++++ .../components/internal/PhoneInput/index.ts | 2 +- .../components/internal/PhoneInput/types.ts | 2 +- .../stories/internals/PhoneInput.stories.tsx | 6 +- 16 files changed, 287 insertions(+), 123 deletions(-) rename packages/lib/src/components/internal/PhoneInput/{PhoneInput.tsx => PhoneInputFields.tsx} (58%) rename packages/lib/src/components/internal/PhoneInput/{PhoneInput.test.tsx => PhoneInputForm.test.tsx} (90%) create mode 100644 packages/lib/src/components/internal/PhoneInput/PhoneInputForm.tsx diff --git a/packages/lib/src/components/Ach/components/AchInput/AchInput.tsx b/packages/lib/src/components/Ach/components/AchInput/AchInput.tsx index 8454915d85..cdf6cfaeb3 100644 --- a/packages/lib/src/components/Ach/components/AchInput/AchInput.tsx +++ b/packages/lib/src/components/Ach/components/AchInput/AchInput.tsx @@ -128,6 +128,8 @@ function AchInput(props: ACHInputProps) { props.onChange({ data, isValid, storePaymentMethod }); }, [data, valid, errors, storePaymentMethod]); + console.log('ach props 2', props); + return (
diff --git a/packages/lib/src/components/MBWay/components/MBWayInput/MBWayInput.tsx b/packages/lib/src/components/MBWay/components/MBWayInput/MBWayInput.tsx index a609ec195a..7df30783fa 100644 --- a/packages/lib/src/components/MBWay/components/MBWayInput/MBWayInput.tsx +++ b/packages/lib/src/components/MBWay/components/MBWayInput/MBWayInput.tsx @@ -3,7 +3,7 @@ import { useState, useRef } from 'preact/hooks'; import { useCoreContext } from '../../../../core/Context/CoreProvider'; import { MBWayInputProps } from './types'; import './MBWayInput.scss'; -import PhoneInput from '../../../internal/PhoneInput'; +import PhoneInputForm from '../../../internal/PhoneInput'; import LoadingWrapper from '../../../internal/LoadingWrapper'; import usePhonePrefixes from '../../../internal/PhoneInput/usePhonePrefixes'; @@ -28,7 +28,7 @@ function MBWayInput(props: MBWayInputProps) { return (
- + {props.showPayButton && props.payButton({ status, label: i18n.get('confirmPurchase') })}
diff --git a/packages/lib/src/components/PayTo/PayTo.tsx b/packages/lib/src/components/PayTo/PayTo.tsx index 8d64fc9a85..334e9b75a8 100644 --- a/packages/lib/src/components/PayTo/PayTo.tsx +++ b/packages/lib/src/components/PayTo/PayTo.tsx @@ -3,7 +3,6 @@ import UIElement from '../internal/UIElement/UIElement'; import { CoreProvider } from '../../core/Context/CoreProvider'; import Await from '../../components/internal/Await'; import SRPanelProvider from '../../core/Errors/SRPanelProvider'; -import PayButton from '../internal/PayButton'; /* Types (previously in their own file) @@ -11,13 +10,16 @@ Types (previously in their own file) import { UIElementProps } from '../internal/UIElement/types'; import { TxVariants } from '../tx-variants'; import PayToInput from './components/PayToInput'; +import { PayIdFormData } from './components/PayIDInput'; +import { PayToIdentifierEnum } from './components/IdentifierSelector'; export interface PayToConfiguration extends UIElementProps { paymentData?: any; data: PayToData; + placeholders: any; //TODO } -export interface PayToData { +export interface PayToData extends PayIdFormData { shopperAccountIdentifier: string; } @@ -35,12 +37,38 @@ const config = { showCountdownTimer: false }; +const getAccountIdentifier = (state: PayToData) => { + switch (state.selectedIdentifier) { + case PayToIdentifierEnum.email: + return state.email; + case PayToIdentifierEnum.abn: + return state.abn; + case PayToIdentifierEnum.orgid: + return state.orgid; + case PayToIdentifierEnum.phone: + return `${state.phonePrefix}-${state.phoneNumber}`; + } +}; /** * */ export class PayToElement extends UIElement { public static type = TxVariants.payto; + protected static defaultProps = { + placeholders: {} + }; + + formatProps(props) { + return { + ...props, + data: { + ...props.data, + phonePrefix: props.data?.phonePrefix || '+61' // use AUS as default value + } + }; + } + /** * Formats the component data output */ @@ -48,16 +76,11 @@ export class PayToElement extends UIElement { return { paymentMethod: { type: PayToElement.type, - shopperAccountIdentifier: this.state.data?.shopperAccountIdentifier + shopperAccountIdentifier: getAccountIdentifier(this.state.data) } }; } - // Reimplement payButton similar to GiftCard to allow to set onClick - public payButton = props => { - return ; - }; - get isValid(): boolean { return !!this.state.isValid; } @@ -95,7 +118,15 @@ export class PayToElement extends UIElement { return ( - + ); } diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.scss b/packages/lib/src/components/PayTo/components/PayIDInput.scss index 4eac4f7ab2..5c2cada5d3 100644 --- a/packages/lib/src/components/PayTo/components/PayIDInput.scss +++ b/packages/lib/src/components/PayTo/components/PayIDInput.scss @@ -1,3 +1,10 @@ -/* -This linter is annoying - */ \ No newline at end of file +@import 'styles/variable-generator'; + +.adyen-checkout__fieldset--payto__payid_input { + margin-top: token(spacer-070); + + .adyen-checkout__fieldset__fields { + margin-top: token(spacer-070); + gap: 0 token(spacer-060); + } +} diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.tsx b/packages/lib/src/components/PayTo/components/PayIDInput.tsx index e3d6ef4e76..e578eb7c8d 100644 --- a/packages/lib/src/components/PayTo/components/PayIDInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayIDInput.tsx @@ -1,7 +1,7 @@ import { h } from 'preact'; import Fieldset from '../../internal/FormFields/Fieldset'; -import IdentifierSelector, { PayToIdentifierEnum, PayToPayIDInputIdentifierValues } from './IdentifierSelector'; -import { useEffect, useState } from 'preact/hooks'; +import IdentifierSelector, { PayToIdentifierEnum } from './IdentifierSelector'; +import { useEffect, useRef } from 'preact/hooks'; import useForm from '../../../utils/useForm'; import PayToPhone from './PayToPhone'; import InputEmail from '../../internal/FormFields/InputEmail'; @@ -10,6 +10,9 @@ import Field from '../../internal/FormFields/Field'; import { useCoreContext } from '../../../core/Context/CoreProvider'; import InputText from '../../internal/FormFields/InputText'; import { payIdValidationRules } from './validate'; +import './PayIDInput.scss'; +import { phoneFormatters } from '../../internal/PhoneInput/validate'; +import { ComponentMethodsRef } from '../../internal/UIElement/types'; export interface PayIdFormData { email: string; @@ -18,43 +21,78 @@ export interface PayIdFormData { orgid: string; firstName: string; lastName: string; + phoneNumber?: string; + phonePrefix?: string; + selectedIdentifier: PayToIdentifierEnum; } -export default function PayIDInput(props) { +export interface PayIDInputProps { + defaultData: PayIdFormData; + placeholders: any; //TODO + onError: () => {}; + onChange: (e) => void; + setComponentRef: (ref: ComponentMethodsRef) => void; +} + +const BASE_SCHEMA = ['selectedIdentifier', 'firstName', 'lastName']; + +const IDENTIFIER_SCHEMA = { + [PayToIdentifierEnum.email]: ['email'], + [PayToIdentifierEnum.phone]: ['phoneNumber', 'phonePrefix'], + [PayToIdentifierEnum.abn]: ['abn'], + [PayToIdentifierEnum.orgid]: ['orgid'] +}; + +export interface KlarnaComponentRef extends ComponentMethodsRef {} + +export default function PayIDInput({ setComponentRef, defaultData, placeholders, onError, onChange }: PayIDInputProps) { const { i18n } = useCoreContext(); - const { handleChangeFor, triggerValidation, data, errors } = useForm({ - schema: ['email', 'abn', 'phone', 'orgid'], - rules: payIdValidationRules + const form = useForm({ + schema: BASE_SCHEMA, + defaultData: { selectedIdentifier: PayToIdentifierEnum.phone, ...defaultData }, + rules: payIdValidationRules, + formatters: phoneFormatters }); + const { handleChangeFor, triggerValidation, data, errors, valid, isValid, setSchema } = form; //this.setStatus = setStatus; - this.showValidation = triggerValidation; + this.triggerValidation = triggerValidation; - // TODO fix all these letters - as in this is quite a long class - // TODO double check if what should be default value - const [selectedIdentifier, setSelectedIdentifier] = useState(PayToIdentifierEnum.email); - console.log(selectedIdentifier); + // handle the changes of identifier, each identifier gets its own schema + useEffect(() => { + // get the correct schema for each identifier and merge it with the base + setSchema([...IDENTIFIER_SCHEMA[data.selectedIdentifier], ...BASE_SCHEMA]); + }, [data.selectedIdentifier]); + + // standard onChange propagate to parent state + useEffect(() => { + onChange({ data, valid, errors, isValid }); + }, [data, valid, errors, isValid]); + + const payToRef = useRef({ + showValidation: triggerValidation + }); useEffect(() => { - console.log(data); - }, [data]); + setComponentRef(payToRef.current); + }, [setComponentRef]); return ( -
+
- {selectedIdentifier === PayToIdentifierEnum.phone && ( - + {data.selectedIdentifier === PayToIdentifierEnum.phone && ( + )} - {selectedIdentifier === PayToIdentifierEnum.email && ( + {data.selectedIdentifier === PayToIdentifierEnum.email && ( )} - {selectedIdentifier === PayToIdentifierEnum.abn && ( + {data.selectedIdentifier === PayToIdentifierEnum.abn && ( )} - {selectedIdentifier === PayToIdentifierEnum.orgid && ( + {data.selectedIdentifier === PayToIdentifierEnum.orgid && ( @@ -122,7 +160,7 @@ export default function PayIDInput(props) { classNameModifiers={['firstName']} onInput={handleChangeFor('firstName', 'input')} onBlur={handleChangeFor('firstName', 'input')} - //placeholder={placeholders.firstName} + placeholder={placeholders?.firstName} spellCheck={false} required={true} /> @@ -141,7 +179,7 @@ export default function PayIDInput(props) { classNameModifiers={['lastName']} onInput={handleChangeFor('lastName', 'input')} onBlur={handleChangeFor('onBlue', 'blur')} - //placeholder={placeholders.lastName} + placeholder={placeholders?.lastName} spellCheck={false} required={true} /> diff --git a/packages/lib/src/components/PayTo/components/PayToInput.tsx b/packages/lib/src/components/PayTo/components/PayToInput.tsx index 2c2a601225..7a0c2839f7 100644 --- a/packages/lib/src/components/PayTo/components/PayToInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayToInput.tsx @@ -5,6 +5,7 @@ import { useState } from 'preact/hooks'; import { SegmentedControlOptions } from '../../internal/SegmentedControl/SegmentedControl'; import PayIDInput from './PayIDInput'; import BSBInput from './BSBInput'; +import { useCoreContext } from '../../../core/Context/CoreProvider'; const inputOptions: SegmentedControlOptions = [ { @@ -27,27 +28,35 @@ const inputOptions: SegmentedControlOptions = [ } ]; -export default function PayToInput() { - // const { i18n } = useCoreContext(); +export default function PayToInput(props) { + const { i18n } = useCoreContext(); - // TODO type this - // const { handleChangeFor, triggerValidation, data, valid, errors } = useForm({ - // schema: ['beneficiaryId'] - // }); - // - // const [status, setStatus] = useState('ready'); + const [status, setStatus] = useState('ready'); - // this.setStatus = setStatus; - // this.showValidation = triggerValidation; + this.setStatus = setStatus; const defaultOption = inputOptions[0].value; const [selectedInput, setSelectedInput] = useState(defaultOption); + const onChange = ({ data, valid, errors, isValid }) => { + props.onChange({ data, valid, errors, isValid }); + }; + return ( - {selectedInput === 'payid-option' && } + {selectedInput === 'payid-option' && ( + + )} {selectedInput === 'bsb-option' && } + + {props.showPayButton && props.payButton({ status, label: i18n.get('confirmPurchase') })} ); } diff --git a/packages/lib/src/components/PayTo/components/PayToPhone.tsx b/packages/lib/src/components/PayTo/components/PayToPhone.tsx index 3421388aad..bb14d2e796 100644 --- a/packages/lib/src/components/PayTo/components/PayToPhone.tsx +++ b/packages/lib/src/components/PayTo/components/PayToPhone.tsx @@ -1,21 +1,32 @@ import { h } from 'preact'; import usePhonePrefixes from '../../internal/PhoneInput/usePhonePrefixes'; -import PhoneInput from '../../internal/PhoneInput'; import { useCoreContext } from '../../../core/Context/CoreProvider'; +import PhoneInputFields from '../../internal/PhoneInput/PhoneInputFields'; +import { Form } from '../../../utils/useForm/types'; +import { PayIdFormData } from './PayIDInput'; +import { getErrorMessage } from '../../../utils/getErrorMessage'; +import { useCallback } from 'preact/hooks'; interface PayToPhoneProps { + form: Form; onChange: (value: string) => void; onError: (error: Error) => void; data: any; // Data } -// TODO change data -export default function PayToPhone({ onChange, onError, data }: PayToPhoneProps) { - const { loadingContext } = useCoreContext(); +export default function PayToPhone({ form, onChange, onError, data }: PayToPhoneProps) { + const { loadingContext, i18n } = useCoreContext(); const allowedCountries = []; const { loadingStatus: prefixLoadingStatus, phonePrefixes } = usePhonePrefixes({ allowedCountries, loadingContext, handleError: onError }); - return ; + // TODO handle the loading status + console.log('TODO', prefixLoadingStatus); + + const getError = useCallback((field: string) => getErrorMessage(i18n, form.errors[field]), [i18n, form]); + + return ( + + ); } diff --git a/packages/lib/src/components/PayTo/components/validate.ts b/packages/lib/src/components/PayTo/components/validate.ts index 3e1aa0481a..6666f96afd 100644 --- a/packages/lib/src/components/PayTo/components/validate.ts +++ b/packages/lib/src/components/PayTo/components/validate.ts @@ -9,6 +9,9 @@ const orgidRegex = /`^[!-@[-~][ -@[-~]{0,254}[!-@[-~]$`/; const emailRegex = /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)$/; +// full phone regex Phone: ^\+[0-9]{1,3}-[1-9]{1,1}[0-9]{1,29}$ +const phoneNumberRegex = /^[1-9]{1,1}[0-9]{1,29}$/; + export const validationFromRegex = (value: string, regex: RegExp, validationRule: ValidatorRule): boolean | null => { // TODO investigate why null is the return value for 'empty' validation if (isEmpty(value)) { @@ -57,5 +60,17 @@ export const payIdValidationRules: ValidatorRules = { validate: value => (isEmpty(value) ? null : true), errorMessage: 'lastName.invalid', modes: ['blur'] + }, + phoneNumber: { + modes: ['blur'], + validate: value => { + return isEmpty(value) ? null : phoneNumberRegex.test(value); + }, + errorMessage: 'mobileNumber.invalid' + }, + phonePrefix: { + modes: ['blur'], + validate: phonePrefix => !!phonePrefix, + errorMessage: 'mobileNumber.invalid' } }; diff --git a/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx b/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx index 7a09c232c1..52a27a8fe0 100644 --- a/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx +++ b/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx @@ -10,6 +10,7 @@ import { CompanyDetailsSchema, CompanyDetailsProps } from './types'; import useForm from '../../../utils/useForm'; import InputText from '../FormFields/InputText'; import { ComponentMethodsRef } from '../UIElement/types'; +import { HandleChangeForModeType } from '../../../utils/useForm/types'; export const COMPANY_DETAILS_SCHEMA = ['name', 'registrationNumber']; @@ -37,7 +38,7 @@ export default function CompanyDetails(props: CompanyDetailsProps) { const generateFieldName = (name: string): string => `${namePrefix ? `${namePrefix}.` : ''}${name}`; const eventHandler = - (mode: string): h.JSX.FocusEventHandler => + (mode: HandleChangeForModeType): h.JSX.FocusEventHandler => (e): void => { const { name } = e.target as HTMLInputElement; const key = name.split(`${namePrefix}.`).pop(); @@ -46,7 +47,7 @@ export default function CompanyDetails(props: CompanyDetailsProps) { }; const inputEventHandler = - (mode: string): h.JSX.InputEventHandler => + (mode: HandleChangeForModeType): h.JSX.InputEventHandler => (e): void => { const { name } = e.target as HTMLInputElement; const key = name.split(`${namePrefix}.`).pop(); diff --git a/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx b/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx index eef4fa08d4..f91e04e57b 100644 --- a/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx +++ b/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx @@ -18,6 +18,7 @@ import InputEmail from '../FormFields/InputEmail'; import InputTelephone from '../FormFields/InputTelephone'; import { getErrorMessage } from '../../../utils/getErrorMessage'; import { ComponentMethodsRef } from '../UIElement/types'; +import { HandleChangeForModeType } from '../../../utils/useForm/types'; export const PERSONAL_DETAILS_SCHEMA = ['firstName', 'lastName', 'gender', 'dateOfBirth', 'shopperEmail', 'telephoneNumber']; @@ -47,7 +48,7 @@ export default function PersonalDetails(props: PersonalDetailsProps) { }; const eventHandler = - (mode: string): h.JSX.GenericEventHandler => + (mode: HandleChangeForModeType): h.JSX.GenericEventHandler => (e: Event): void => { const { name } = e.target as HTMLInputElement; const key = name.split(`${namePrefix}.`).pop(); diff --git a/packages/lib/src/components/internal/PhoneInput/PhoneInput.tsx b/packages/lib/src/components/internal/PhoneInput/PhoneInputFields.tsx similarity index 58% rename from packages/lib/src/components/internal/PhoneInput/PhoneInput.tsx rename to packages/lib/src/components/internal/PhoneInput/PhoneInputFields.tsx index 48189ff9ed..e3b89933fe 100644 --- a/packages/lib/src/components/internal/PhoneInput/PhoneInput.tsx +++ b/packages/lib/src/components/internal/PhoneInput/PhoneInputFields.tsx @@ -1,39 +1,38 @@ -import { h } from 'preact'; -import { useCallback, useEffect } from 'preact/hooks'; +import { Fragment, h } from 'preact'; +import { useEffect } from 'preact/hooks'; import Field from '../FormFields/Field'; -import useForm from '../../../utils/useForm'; import { useCoreContext } from '../../../core/Context/CoreProvider'; import './PhoneInput.scss'; import Select from '../FormFields/Select'; -import { phoneFormatters, phoneValidationRules } from './validate'; -import { PhoneInputProps, PhoneInputSchema } from './types'; +import { PhoneInputSchema } from './types'; import InputText from '../FormFields/InputText'; -import Fieldset from '../FormFields/Fieldset'; +import { DataSet } from '../../../core/Services/data-set'; +import { Form } from '../../../utils/useForm/types'; +export interface PhoneInputFieldProps { + items: DataSet; + requiredFields?: string[]; + data: PhoneInputSchema; + onChange: (obj) => void; + form: Form; + getError: (string) => string | boolean; + phoneNumberKey?: string; + phonePrefixErrorKey?: string; + phoneNumberErrorKey?: string; + placeholders?: PhoneInputSchema; + ref?; + showPrefix?: boolean; + showNumber?: boolean; +} /** * - * @param PhoneInputProps + * @param PhoneInputFormProps * @constructor */ -function PhoneInput(props: PhoneInputProps) { +export default function PhoneInputFields({ getError, showNumber, showPrefix, form, ...props }: PhoneInputFieldProps) { const { i18n } = useCoreContext(); - const schema = props.requiredFields || [...(props?.items?.length ? ['phonePrefix'] : []), 'phoneNumber']; - const showPrefix = schema.includes('phonePrefix') && !!props?.items?.length; - const showNumber = schema.includes('phoneNumber'); - - const { handleChangeFor, data, valid, errors, isValid, triggerValidation, setSchema } = useForm({ - i18n, - ...props, - schema, - defaultData: props.data, - rules: phoneValidationRules, - formatters: phoneFormatters - }); - - useEffect(() => { - setSchema(schema); - }, [schema.toString()]); + const { handleChangeFor, data, valid, triggerValidation } = form; // Force re-validation of the phoneNumber when data.phonePrefix changes (since the validation rules will also change) useEffect((): void => { @@ -42,31 +41,16 @@ function PhoneInput(props: PhoneInputProps) { } }, [data.phonePrefix]); - useEffect(() => { - props.onChange({ data, valid, errors, isValid }); - }, [data, valid, errors, isValid]); - + // TODO why do we need this? this.triggerValidation = triggerValidation; - const getPhoneFieldError = useCallback( - (field: string) => { - if (errors[field]) { - const propsField = field === 'phoneNumber' ? 'phoneNumberErrorKey' : 'phonePrefixErrorKey'; - const key = props[propsField] ? props[propsField] : errors[field].errorMessage; - return i18n.get(key) ?? null; - } - return null; - }, - [errors] - ); - return ( -
+ {showPrefix && ( 0} dir={'ltr'} @@ -108,12 +92,6 @@ function PhoneInput(props: PhoneInputProps) { /> )} -
+ ); } - -PhoneInput.defaultProps = { - phoneLabel: 'telephoneNumber' -}; - -export default PhoneInput; diff --git a/packages/lib/src/components/internal/PhoneInput/PhoneInput.test.tsx b/packages/lib/src/components/internal/PhoneInput/PhoneInputForm.test.tsx similarity index 90% rename from packages/lib/src/components/internal/PhoneInput/PhoneInput.test.tsx rename to packages/lib/src/components/internal/PhoneInput/PhoneInputForm.test.tsx index be3d6df5f9..e1a695ea26 100644 --- a/packages/lib/src/components/internal/PhoneInput/PhoneInput.test.tsx +++ b/packages/lib/src/components/internal/PhoneInput/PhoneInputForm.test.tsx @@ -2,13 +2,13 @@ import { h } from 'preact'; import { fireEvent, render, screen } from '@testing-library/preact'; import { CoreProvider } from '../../../core/Context/CoreProvider'; import userEvent from '@testing-library/user-event'; -import PhoneInput from './PhoneInput'; -import { PhoneInputProps } from './types'; +import PhoneInputForm from './PhoneInputForm'; +import { PhoneInputFormProps } from './types'; const items = [{ id: '+44', name: 'United Kingdom', code: 'GB', selectedOptionName: 'United Kingdom' }]; describe('PhoneInput', () => { - const defaultProps: PhoneInputProps = { + const defaultProps: PhoneInputFormProps = { items, data: { phonePrefix: items[0].id }, onChange: jest.fn(), @@ -16,11 +16,11 @@ describe('PhoneInput', () => { placeholders: {} }; - const renderPhoneInput = (props: PhoneInputProps = defaultProps) => { + const renderPhoneInput = (props: PhoneInputFormProps = defaultProps) => { return render( // @ts-ignore ignore - + ); }; diff --git a/packages/lib/src/components/internal/PhoneInput/PhoneInputForm.tsx b/packages/lib/src/components/internal/PhoneInput/PhoneInputForm.tsx new file mode 100644 index 0000000000..9e95dda7cc --- /dev/null +++ b/packages/lib/src/components/internal/PhoneInput/PhoneInputForm.tsx @@ -0,0 +1,71 @@ +import { h } from 'preact'; +import { useEffect, useCallback } from 'preact/hooks'; +import useForm from '../../../utils/useForm'; +import { useCoreContext } from '../../../core/Context/CoreProvider'; +import './PhoneInput.scss'; +import { phoneFormatters, phoneValidationRules } from './validate'; +import { PhoneInputFormProps, PhoneInputSchema } from './types'; +import Fieldset from '../FormFields/Fieldset'; +import PhoneInputFields from './PhoneInputFields'; + +/** + * + * @param PhoneInputFormProps + * @constructor + */ +function PhoneInputForm(props: PhoneInputFormProps) { + const { i18n } = useCoreContext(); + + const schema = props.requiredFields || [...(props?.items?.length ? ['phonePrefix'] : []), 'phoneNumber']; + const showPrefix = schema.includes('phonePrefix') && !!props?.items?.length; + const showNumber = schema.includes('phoneNumber'); + + const form = useForm({ + i18n, + ...props, + schema, + defaultData: props.data, + rules: phoneValidationRules, + formatters: phoneFormatters + }); + + useEffect(() => { + form.setSchema(schema); + }, [schema.toString()]); + + const { data, valid, errors, isValid, triggerValidation } = form; + + useEffect(() => { + props.onChange({ data, valid, errors, isValid }); + }, [data, valid, errors, isValid]); + + this.showValidation = triggerValidation; + + // This is here for MBWay, prob should be moved up + // MBWay has a weird way of loading its error messages + // They come form the prop phoneNumberErrorKey: 'mobileNumber.invalid' + // Strangely it's defined as invalidPhoneNumber in the validation rules + const getPhoneFieldError = useCallback( + (field: string) => { + if (errors[field]) { + const propsField = field === 'phoneNumber' ? 'phoneNumberErrorKey' : 'phonePrefixErrorKey'; + const key = props[propsField] ? props[propsField] : errors[field].errorMessage; + return i18n.get(key) ?? null; + } + return null; + }, + [errors] + ); + + return ( +
+ +
+ ); +} + +PhoneInputForm.defaultProps = { + phoneLabel: 'telephoneNumber' +}; + +export default PhoneInputForm; diff --git a/packages/lib/src/components/internal/PhoneInput/index.ts b/packages/lib/src/components/internal/PhoneInput/index.ts index 341afad36b..2e5ec13567 100644 --- a/packages/lib/src/components/internal/PhoneInput/index.ts +++ b/packages/lib/src/components/internal/PhoneInput/index.ts @@ -1 +1 @@ -export { default } from './PhoneInput'; +export { default } from './PhoneInputForm'; diff --git a/packages/lib/src/components/internal/PhoneInput/types.ts b/packages/lib/src/components/internal/PhoneInput/types.ts index 707d5b5a5f..57ed4ff2cd 100644 --- a/packages/lib/src/components/internal/PhoneInput/types.ts +++ b/packages/lib/src/components/internal/PhoneInput/types.ts @@ -5,7 +5,7 @@ export interface PhoneInputSchema { phonePrefix?: string; } -export interface PhoneInputProps { +export interface PhoneInputFormProps { items: DataSet; requiredFields?: string[]; data: PhoneInputSchema; diff --git a/packages/lib/storybook/stories/internals/PhoneInput.stories.tsx b/packages/lib/storybook/stories/internals/PhoneInput.stories.tsx index 04a2ac6518..85ad8d5eac 100644 --- a/packages/lib/storybook/stories/internals/PhoneInput.stories.tsx +++ b/packages/lib/storybook/stories/internals/PhoneInput.stories.tsx @@ -1,5 +1,5 @@ import { Meta, StoryObj } from '@storybook/preact'; -import PhoneInput from '../../../src/components/internal/PhoneInput'; +import PhoneInputForm from '../../../src/components/internal/PhoneInput'; import { CoreProvider } from '../../../src/core/Context/CoreProvider'; import Language from '../../../src/language'; @@ -55,14 +55,14 @@ const formatPrefixName = item => { const meta: Meta = { title: 'Internals/PhoneInput', - component: PhoneInput + component: PhoneInputForm }; export const Default: StoryObj = { render: args => { return ( - console.log({ item })} From 05a9a558c804035e07939c301288dad72271d3a3 Mon Sep 17 00:00:00 2001 From: antoniof Date: Fri, 10 Jan 2025 15:58:46 +0100 Subject: [PATCH 8/9] tests and PR comments --- .../lib/src/components/PayTo/PayTo.test.ts | 72 +++++++++++++++++++ packages/lib/src/components/PayTo/PayTo.tsx | 4 +- .../PayTo/components/PayIDInput.tsx | 27 +++---- .../PayTo/components/PayToPhone.tsx | 11 ++- .../stories/components/ANCV.stories.tsx | 24 +++++++ 5 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 packages/lib/src/components/PayTo/PayTo.test.ts create mode 100644 packages/lib/storybook/stories/components/ANCV.stories.tsx diff --git a/packages/lib/src/components/PayTo/PayTo.test.ts b/packages/lib/src/components/PayTo/PayTo.test.ts new file mode 100644 index 0000000000..febb7217a9 --- /dev/null +++ b/packages/lib/src/components/PayTo/PayTo.test.ts @@ -0,0 +1,72 @@ +import { render, screen } from '@testing-library/preact'; +import PayTo from './PayTo'; +import userEvent from '@testing-library/user-event'; +import getDataset from '../../core/Services/get-dataset'; + +jest.mock('../../core/Services/get-dataset'); +(getDataset as jest.Mock).mockImplementation( + jest.fn(() => { + return Promise.resolve([{ id: 'AUS', prefix: '+61' }]); + }) +); + +describe('PayTo', () => { + let onSubmitMock; + let user; + + beforeEach(() => { + onSubmitMock = jest.fn(); + user = userEvent.setup(); + }); + + test('should render payment and show PayID page', async () => { + const payTo = new PayTo(global.core, { + i18n: global.i18n, + loadingContext: 'test', + modules: { resources: global.resources } + }); + + render(payTo.render()); + expect(await screen.findByText(/Enter the PayID and account details that are connected to your Payto account./i)).toBeTruthy(); + expect(await screen.findByLabelText(/Prefix/i)).toBeTruthy(); + expect(await screen.findByLabelText(/Telephone number/i)).toBeTruthy(); // TODO this should be mobile number + expect(await screen.findByLabelText(/First name/i)).toBeTruthy(); + expect(await screen.findByLabelText(/Last name/i)).toBeTruthy(); + }); + + test('should render continue button', async () => { + const payTo = new PayTo(global.core, { + onSubmit: onSubmitMock, + i18n: global.i18n, + loadingContext: 'test', + modules: { resources: global.resources } + }); + + render(payTo.render()); + const button = await screen.findByRole('button', { name: 'Confirm purchase' }); + + // check if button actually triggers submit + await user.click(button); + expect(onSubmitMock).toHaveBeenCalledTimes(0); + + //TODO check validation fails + }); + + test('should change to different identifier when selected', async () => { + const payTo = new PayTo(global.core, { + onSubmit: onSubmitMock, + i18n: global.i18n, + loadingContext: 'test', + modules: { resources: global.resources }, + showPayButton: false + }); + + render(payTo.render()); + + await user.click(screen.queryByRole('button', { name: 'Mobile' })); + await user.click(screen.queryByRole('option', { name: /Email/i })); + + expect(await screen.findByLabelText(/Prefix/i)).toBeFalsy(); + expect(await screen.findByLabelText(/Email/i)).toBeTruthy(); + }); +}); diff --git a/packages/lib/src/components/PayTo/PayTo.tsx b/packages/lib/src/components/PayTo/PayTo.tsx index 334e9b75a8..abcb11e237 100644 --- a/packages/lib/src/components/PayTo/PayTo.tsx +++ b/packages/lib/src/components/PayTo/PayTo.tsx @@ -15,8 +15,8 @@ import { PayToIdentifierEnum } from './components/IdentifierSelector'; export interface PayToConfiguration extends UIElementProps { paymentData?: any; - data: PayToData; - placeholders: any; //TODO + data?: PayToData; + placeholders?: any; //TODO } export interface PayToData extends PayIdFormData { diff --git a/packages/lib/src/components/PayTo/components/PayIDInput.tsx b/packages/lib/src/components/PayTo/components/PayIDInput.tsx index e578eb7c8d..61fb7898cc 100644 --- a/packages/lib/src/components/PayTo/components/PayIDInput.tsx +++ b/packages/lib/src/components/PayTo/components/PayIDInput.tsx @@ -82,13 +82,14 @@ export default function PayIDInput({ setComponentRef, defaultData, placeholders,
{data.selectedIdentifier === PayToIdentifierEnum.phone && ( )} + {/* TODO probably worth refactoring this into either re-usable components or builder */} {data.selectedIdentifier === PayToIdentifierEnum.email && ( - @@ -132,16 +133,16 @@ export default function PayIDInput({ setComponentRef, defaultData, placeholders, - @@ -178,7 +179,7 @@ export default function PayIDInput({ setComponentRef, defaultData, placeholders, value={data.lastName} classNameModifiers={['lastName']} onInput={handleChangeFor('lastName', 'input')} - onBlur={handleChangeFor('onBlue', 'blur')} + onBlur={handleChangeFor('lastName', 'blur')} placeholder={placeholders?.lastName} spellCheck={false} required={true} diff --git a/packages/lib/src/components/PayTo/components/PayToPhone.tsx b/packages/lib/src/components/PayTo/components/PayToPhone.tsx index bb14d2e796..fa1de5a070 100644 --- a/packages/lib/src/components/PayTo/components/PayToPhone.tsx +++ b/packages/lib/src/components/PayTo/components/PayToPhone.tsx @@ -27,6 +27,15 @@ export default function PayToPhone({ form, onChange, onError, data }: PayToPhone const getError = useCallback((field: string) => getErrorMessage(i18n, form.errors[field]), [i18n, form]); return ( - + ); } diff --git a/packages/lib/storybook/stories/components/ANCV.stories.tsx b/packages/lib/storybook/stories/components/ANCV.stories.tsx new file mode 100644 index 0000000000..3fa1718726 --- /dev/null +++ b/packages/lib/storybook/stories/components/ANCV.stories.tsx @@ -0,0 +1,24 @@ +import { Meta, StoryObj } from '@storybook/preact'; +import { PaymentMethodStoryProps } from '../types'; +import { ComponentContainer } from '../ComponentContainer'; +import { ANCVConfiguration } from '../../../src/components/ANCV/types'; +import ANCV from '../../../src/components/ANCV/ANCV'; +import { Checkout } from '../Checkout'; + +type ANCVStory = StoryObj>; + +const meta: Meta> = { + title: 'Components/ANCV' +}; + +export const Default: ANCVStory = { + render: ({ componentConfiguration, ...checkoutConfig }) => ( + {checkout => } + ), + args: { + countryCode: 'NL', + amount: 2000, + useSessions: false + } +}; +export default meta; From 58bb4c8b49dbf033e7133659e859d40e3cdee7ef Mon Sep 17 00:00:00 2001 From: antoniof Date: Fri, 10 Jan 2025 16:32:06 +0100 Subject: [PATCH 9/9] fix tests --- packages/lib/src/components/PayTo/PayTo.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/components/PayTo/PayTo.test.ts b/packages/lib/src/components/PayTo/PayTo.test.ts index febb7217a9..4ab8abdc16 100644 --- a/packages/lib/src/components/PayTo/PayTo.test.ts +++ b/packages/lib/src/components/PayTo/PayTo.test.ts @@ -29,7 +29,7 @@ describe('PayTo', () => { render(payTo.render()); expect(await screen.findByText(/Enter the PayID and account details that are connected to your Payto account./i)).toBeTruthy(); expect(await screen.findByLabelText(/Prefix/i)).toBeTruthy(); - expect(await screen.findByLabelText(/Telephone number/i)).toBeTruthy(); // TODO this should be mobile number + expect(await screen.findByLabelText(/Mobile number/i)).toBeTruthy(); expect(await screen.findByLabelText(/First name/i)).toBeTruthy(); expect(await screen.findByLabelText(/Last name/i)).toBeTruthy(); }); @@ -66,7 +66,7 @@ describe('PayTo', () => { await user.click(screen.queryByRole('button', { name: 'Mobile' })); await user.click(screen.queryByRole('option', { name: /Email/i })); - expect(await screen.findByLabelText(/Prefix/i)).toBeFalsy(); - expect(await screen.findByLabelText(/Email/i)).toBeTruthy(); + expect(screen.queryByLabelText(/Prefix/i)).toBeFalsy(); + expect(screen.getByLabelText(/Email/i)).toBeTruthy(); }); });