From d33226a8148861da3eef501421191e18451a38dc Mon Sep 17 00:00:00 2001 From: sponglord Date: Tue, 21 Mar 2023 15:11:19 +0100 Subject: [PATCH] Feature/OpenInvoice uses SRPanel (#2053) * OpenInvoice comp uses SRPanel * PersonalDetails & Address do not consider an input element to be in error if empty, unless the whole form is being validated * IbanInput fields do not consider an element to be in error if empty, unless the whole form is being validated * layout & countrySpecificLabels are optional props when setting SRMessages * Adding mockSRContext object (needed for next phase) * Reordered OpenInvoice comps to match markup in playground. Added switching mechanism to sho/hide them --- .../Card/components/CardInput/CardInput.tsx | 1 + .../OpenInvoiceContainer.tsx | 19 +- .../components/internal/Address/Address.tsx | 5 +- .../Address/components/FieldContainer.tsx | 7 +- .../src/components/internal/Address/utils.ts | 16 ++ .../CompanyDetails/CompanyDetails.tsx | 6 +- .../internal/CompanyDetails/validate.ts | 16 +- .../internal/IbanInput/IbanInput.tsx | 6 +- .../internal/OpenInvoice/OpenInvoice.tsx | 100 +++++++- .../components/internal/OpenInvoice/types.ts | 13 +- .../components/internal/OpenInvoice/utils.ts | 37 +++ .../PersonalDetails/PersonalDetails.tsx | 4 +- .../internal/PersonalDetails/utils.ts | 17 ++ .../internal/PersonalDetails/validate.ts | 16 +- .../lib/src/core/Errors/SRPanelContext.ts | 9 + .../lib/src/core/Errors/SRPanelProvider.tsx | 2 +- .../src/pages/OpenInvoices/OpenInvoices.html | 17 +- .../src/pages/OpenInvoices/OpenInvoices.js | 228 ++++++++++-------- 18 files changed, 367 insertions(+), 152 deletions(-) create mode 100644 packages/lib/src/components/internal/Address/utils.ts diff --git a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx index 08675375..cb8af9c5 100644 --- a/packages/lib/src/components/Card/components/CardInput/CardInput.tsx +++ b/packages/lib/src/components/Card/components/CardInput/CardInput.tsx @@ -375,6 +375,7 @@ const CardInput: FunctionalComponent = props => { setSortedErrorList(currentErrorsSortedByLayout); switch (srPanelResp?.action) { + // A call to focus the first field in error will always follow the call to validate the whole form case ERROR_ACTION_FOCUS_FIELD: if (shouldMoveFocusSR) setFocusOnFirstField(isValidating.current, sfp, srPanelResp?.fieldToFocus); // Remove 'showValidation' mode - allowing time for collation of all the fields in error whilst it is 'showValidation' mode (some errors come in a second render pass) diff --git a/packages/lib/src/components/helpers/OpenInvoiceContainer/OpenInvoiceContainer.tsx b/packages/lib/src/components/helpers/OpenInvoiceContainer/OpenInvoiceContainer.tsx index 373e20aa..5cfa585f 100644 --- a/packages/lib/src/components/helpers/OpenInvoiceContainer/OpenInvoiceContainer.tsx +++ b/packages/lib/src/components/helpers/OpenInvoiceContainer/OpenInvoiceContainer.tsx @@ -4,6 +4,7 @@ import OpenInvoice from '../../internal/OpenInvoice'; import CoreProvider from '../../../core/Context/CoreProvider'; import { OpenInvoiceProps } from '../../internal/OpenInvoice/types'; import { AddressSpecifications } from '../../internal/Address/types'; +import SRPanelProvider from '../../../core/Errors/SRPanelProvider'; export interface OpenInvoiceContainerProps extends Partial { consentCheckboxLabel?: h.JSX.Element; @@ -86,14 +87,16 @@ export default class OpenInvoiceContainer extends UIElement - + + + ); } diff --git a/packages/lib/src/components/internal/Address/Address.tsx b/packages/lib/src/components/internal/Address/Address.tsx index 58aba35e..14c5711f 100644 --- a/packages/lib/src/components/internal/Address/Address.tsx +++ b/packages/lib/src/components/internal/Address/Address.tsx @@ -33,7 +33,8 @@ export default function Address(props: AddressProps) { const { data, errors, valid, isValid, handleChangeFor, triggerValidation } = useForm({ schema: requiredFieldsSchema, defaultData: props.data, - rules: props.validationRules || getAddressValidationRules(specifications), + // Ensure any passed validation rules are merged with the default ones + rules: { ...getAddressValidationRules(specifications), ...props.validationRules }, formatters: addressFormatters }); @@ -134,7 +135,7 @@ export default function Address(props: AddressProps) { return ( -
+
{addressSchema.map(field => (field instanceof Array ? getWrapper(field) : getComponent(field, {})))}
{/* Needed to easily test when showValidation is called */} diff --git a/packages/lib/src/components/internal/Address/components/FieldContainer.tsx b/packages/lib/src/components/internal/Address/components/FieldContainer.tsx index 69ff9922..426e22d5 100644 --- a/packages/lib/src/components/internal/Address/components/FieldContainer.tsx +++ b/packages/lib/src/components/internal/Address/components/FieldContainer.tsx @@ -22,10 +22,7 @@ function getErrorMessage(errors: AddressStateError, fieldName: string, i18n: Lan * - then you should implement or directly */ function FieldContainer(props: FieldContainerProps) { - const { - i18n, - commonProps: { isCollatingErrors } - } = useCoreContext(); + const { i18n } = useCoreContext(); const { classNameModifiers = [], data, errors, valid, fieldName, onInput, onBlur, trimOnBlur, maxlength, disabled } = props; const value: string = data[fieldName]; @@ -68,7 +65,6 @@ function FieldContainer(props: FieldContainerProps) { errorMessage={errorMessage} isValid={valid[fieldName]} name={fieldName} - isCollatingErrors={isCollatingErrors} i18n={i18n} > {renderFormField('text', { @@ -77,7 +73,6 @@ function FieldContainer(props: FieldContainerProps) { value, onInput, onBlur, - isCollatingErrors, maxlength, trimOnBlur, disabled diff --git a/packages/lib/src/components/internal/Address/utils.ts b/packages/lib/src/components/internal/Address/utils.ts new file mode 100644 index 00000000..b500ed77 --- /dev/null +++ b/packages/lib/src/components/internal/Address/utils.ts @@ -0,0 +1,16 @@ +import Language from '../../../language'; +import { ADDRESS_SCHEMA } from './constants'; +import { AddressField } from '../../../types'; +import { StringObject } from './types'; + +/** + * Used by the SRPanel sorting function to tell it whether we need to prepend the field type to the SR panel message, and, if so, we retrieve the correct translation for the field type. + * (Whether we need to prepend the field type depends on whether we know that the error message correctly reflects the label of the field. Ultimately all error messages should do this + * and this mapping fn will become redundant) + */ +export const mapFieldKey = (key: string, i18n: Language, countrySpecificLabels: StringObject): string => { + if (ADDRESS_SCHEMA.includes(key as AddressField)) { + return countrySpecificLabels?.[key] ? i18n.get(countrySpecificLabels?.[key]) : i18n.get(key); + } + return null; +}; diff --git a/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx b/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx index 0d2e7796..21e013d9 100644 --- a/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx +++ b/packages/lib/src/components/internal/CompanyDetails/CompanyDetails.tsx @@ -11,14 +11,14 @@ import { CompanyDetailsSchema, CompanyDetailsProps } from './types'; import useForm from '../../../utils/useForm'; import { ComponentMethodsRef } from '../../types'; -const companyDetailsSchema = ['name', 'registrationNumber']; +export const COMPANY_DETAILS_SCHEMA = ['name', 'registrationNumber']; export default function CompanyDetails(props: CompanyDetailsProps) { const { label = '', namePrefix, requiredFields, visibility } = props; const { i18n } = useCoreContext(); const { handleChangeFor, triggerValidation, data, valid, errors, isValid } = useForm({ schema: requiredFields, - rules: props.validationRules, + rules: { ...companyDetailsValidationRules, ...props.validationRules }, defaultData: props.data }); @@ -93,6 +93,6 @@ CompanyDetails.defaultProps = { data: {}, onChange: () => {}, visibility: 'editable', - requiredFields: companyDetailsSchema, + requiredFields: COMPANY_DETAILS_SCHEMA, validationRules: companyDetailsValidationRules }; diff --git a/packages/lib/src/components/internal/CompanyDetails/validate.ts b/packages/lib/src/components/internal/CompanyDetails/validate.ts index 1b845f66..f1b1acde 100644 --- a/packages/lib/src/components/internal/CompanyDetails/validate.ts +++ b/packages/lib/src/components/internal/CompanyDetails/validate.ts @@ -1,8 +1,22 @@ -export const companyDetailsValidationRules = { +import { ValidatorRules } from '../../../utils/Validator/types'; +import { isEmpty } from '../../../utils/validator-utils'; + +export const companyDetailsValidationRules: ValidatorRules = { default: { validate: value => { return value && value.length > 0; }, + modes: ['blur'], + errorMessage: 'error.va.gen.01' // = "Incomplete field" + }, + name: { + validate: value => (isEmpty(value) ? null : true), // valid, if there are chars other than spaces + errorMessage: 'companyDetails.name.invalid', + modes: ['blur'] + }, + registrationNumber: { + validate: value => (isEmpty(value) ? null : true), + errorMessage: 'companyDetails.registrationNumber.invalid', modes: ['blur'] } }; diff --git a/packages/lib/src/components/internal/IbanInput/IbanInput.tsx b/packages/lib/src/components/internal/IbanInput/IbanInput.tsx index abfa11fc..be3d7d40 100644 --- a/packages/lib/src/components/internal/IbanInput/IbanInput.tsx +++ b/packages/lib/src/components/internal/IbanInput/IbanInput.tsx @@ -162,6 +162,9 @@ class IbanInput extends Component { if (currentIban.length > 0) { const validationStatus = checkIbanStatus(currentIban).status; this.setError('iban', validationStatus !== 'valid' ? ibanErrorObj : null, this.onChange); + } else { + // Empty field is not in error + this.setError('iban', null, this.onChange); } }; @@ -197,7 +200,8 @@ class IbanInput extends Component { value: data['ownerName'], 'aria-invalid': !!this.state.errors.holder, 'aria-label': i18n.get('sepa.ownerName'), - onInput: e => this.handleHolderInput(e.target.value) + onInput: e => this.handleHolderInput(e.target.value), + onBlur: e => this.handleHolderInput(e.target.value) })} )} diff --git a/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.tsx b/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.tsx index 74fdf50b..8dbe2e8c 100644 --- a/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.tsx +++ b/packages/lib/src/components/internal/OpenInvoice/OpenInvoice.tsx @@ -1,12 +1,12 @@ import { h } from 'preact'; -import { useEffect, useRef, useState } from 'preact/hooks'; +import { useEffect, useRef, useState, useMemo } from 'preact/hooks'; import useCoreContext from '../../../core/Context/useCoreContext'; import CompanyDetails from '../CompanyDetails'; import PersonalDetails from '../PersonalDetails'; import Address from '../Address'; import Checkbox from '../FormFields/Checkbox'; import ConsentCheckbox from '../FormFields/ConsentCheckbox'; -import { getActiveFieldsData, getInitialActiveFieldsets, fieldsetsSchema } from './utils'; +import { getActiveFieldsData, getInitialActiveFieldsets, fieldsetsSchema, mapFieldKey } from './utils'; import { OpenInvoiceActiveFieldsets, OpenInvoiceFieldsetsRefs, @@ -18,6 +18,21 @@ import { import './OpenInvoice.scss'; import IbanInput from '../IbanInput'; import { ComponentMethodsRef } from '../../types'; +import { enhanceErrorObjectKeys } from '../../../core/Errors/utils'; +import { GenericError, SetSRMessagesReturnObject } from '../../../core/Errors/types'; +import useSRPanelContext from '../../../core/Errors/useSRPanelContext'; +import { SetSRMessagesReturnFn } from '../../../core/Errors/SRPanelProvider'; +import Specifications from '../Address/Specifications'; +import { PERSONAL_DETAILS_SCHEMA } from '../PersonalDetails/PersonalDetails'; +import { COMPANY_DETAILS_SCHEMA } from '../CompanyDetails/CompanyDetails'; +import { setFocusOnField } from '../../../utils/setFocus'; +import { ERROR_ACTION_FOCUS_FIELD } from '../../../core/Errors/constants'; + +const consentCBErrorObj: GenericError = { + isValid: false, + errorMessage: 'consent.checkbox.invalid', + error: 'consent.checkbox.invalid' +}; export default function OpenInvoice(props: OpenInvoiceProps) { const { countryCode, visibility } = props; @@ -30,6 +45,19 @@ export default function OpenInvoice(props: OpenInvoiceProps) { props.setComponentRef?.(openInvoiceRef.current); } + const isValidating = useRef(false); + + /** SR stuff */ + const { setSRMessagesFromObjects, shouldMoveFocusSR } = useSRPanelContext(); + + // Generate a setSRMessages function - implemented as a partial, since the initial set of arguments don't change. + const setSRMessages: SetSRMessagesReturnFn = setSRMessagesFromObjects?.({ + fieldTypeMappingFn: mapFieldKey + }); + + const specifications = useMemo(() => new Specifications(), []); + /** end SR stuff */ + const initialActiveFieldsets: OpenInvoiceActiveFieldsets = getInitialActiveFieldsets(visibility, props.data); const [activeFieldsets, setActiveFieldsets] = useState(initialActiveFieldsets); @@ -57,12 +85,13 @@ export default function OpenInvoice(props: OpenInvoiceProps) { // Expose methods expected by parent openInvoiceRef.current.showValidation = () => { + isValidating.current = true; fieldsetsSchema.forEach(fieldset => { if (fieldsetsRefs[fieldset].current) fieldsetsRefs[fieldset].current.showValidation(); }); setErrors({ - ...(hasConsentCheckbox && { consentCheckbox: !data.consentCheckbox }) + ...(hasConsentCheckbox && { consentCheckbox: data.consentCheckbox ? null : consentCBErrorObj }) }); }; @@ -74,6 +103,71 @@ export default function OpenInvoice(props: OpenInvoiceProps) { const isValid: boolean = fieldsetsAreValid && consentCheckboxValid; const newData: OpenInvoiceStateData = getActiveFieldsData(activeFieldsets, data); + const DELIVERY_ADDRESS_PREFIX = 'deliveryAddress:'; + + /** Create messages for SRPanel */ + // Extract nested errors from the various child components... + const { + companyDetails: extractedCompanyDetailsErrors, + personalDetails: extractedPersonalDetailsErrors, + bankAccount: extractedBankAccountErrors, + billingAddress: extractedBillingAddressErrors, + deliveryAddress: extractedDeliveryAddressErrors, + ...remainingErrors + } = errors; + + // (Differentiate between billingAddress and deliveryAddress errors by adding a prefix to the latter) + const enhancedDeliveryAddressErrors = enhanceErrorObjectKeys(extractedDeliveryAddressErrors, DELIVERY_ADDRESS_PREFIX); + + // ...and then collate the errors into a new object so that they all sit at top level + const errorsForPanel = { + ...(typeof extractedCompanyDetailsErrors === 'object' && extractedCompanyDetailsErrors), + ...(typeof extractedPersonalDetailsErrors === 'object' && extractedPersonalDetailsErrors), + ...(typeof extractedBankAccountErrors === 'object' && extractedBankAccountErrors), + ...(typeof extractedBillingAddressErrors === 'object' && extractedBillingAddressErrors), + ...(typeof enhancedDeliveryAddressErrors === 'object' && enhancedDeliveryAddressErrors), + ...remainingErrors + }; + + // Create layout + const companyDetailsLayout: string[] = COMPANY_DETAILS_SCHEMA; + + const personalDetailsReqFields: string[] = props.personalDetailsRequiredFields ?? PERSONAL_DETAILS_SCHEMA; + const personalDetailLayout: string[] = PERSONAL_DETAILS_SCHEMA.filter(x => personalDetailsReqFields?.includes(x)); + + const bankAccountLayout = ['holder', 'iban']; + + const billingAddressLayout = specifications.getAddressSchemaForCountryFlat(data.billingAddress?.country); + + const deliveryAddressLayout = specifications.getAddressSchemaForCountryFlat(data.deliveryAddress?.country); + // In order to sort the deliveryAddress errors the layout entries need to have the same (prefixed) identifier as the errors themselves + const deliveryAddressLayoutEnhanced = deliveryAddressLayout.map(item => `${DELIVERY_ADDRESS_PREFIX}${item}`); + + const fullLayout = companyDetailsLayout.concat(personalDetailLayout, bankAccountLayout, billingAddressLayout, deliveryAddressLayoutEnhanced, [ + 'consentCheckbox' + ]); + + // Country specific address labels + const countrySpecificLabels = specifications.getAddressLabelsForCountry(data.billingAddress?.country ?? data.deliveryAddress?.country); + + // Set messages: Pass dynamic props (errors, layout etc) to SRPanel via partial + const srPanelResp: SetSRMessagesReturnObject = setSRMessages?.({ + errors: errorsForPanel, + isValidating: isValidating.current, + layout: fullLayout, + countrySpecificLabels + }); + + // A call to focus the first field in error will always follow the call to validate the whole form + if (srPanelResp?.action === ERROR_ACTION_FOCUS_FIELD) { + // Focus first field in error, if required + if (shouldMoveFocusSR) setFocusOnField('.adyen-checkout__open-invoice', srPanelResp.fieldToFocus); + // Remove 'showValidation' mode - allowing time for collation of all the fields in error whilst it is 'showValidation' mode (some errors come in a second render pass) + setTimeout(() => { + isValidating.current = false; + }, 300); + } + props.onChange({ data: newData, errors, valid, isValid }); }, [data, activeFieldsets]); diff --git a/packages/lib/src/components/internal/OpenInvoice/types.ts b/packages/lib/src/components/internal/OpenInvoice/types.ts index a770b9ae..d50bf640 100644 --- a/packages/lib/src/components/internal/OpenInvoice/types.ts +++ b/packages/lib/src/components/internal/OpenInvoice/types.ts @@ -3,6 +3,7 @@ import { CompanyDetailsSchema } from '../CompanyDetails/types'; import { AddressSpecifications } from '../Address/types'; import { UIElementProps } from '../../types'; import UIElement from '../../UIElement'; +import { GenericError, ValidationRuleErrorObj } from '../../../core/Errors/types'; export interface OpenInvoiceVisibility { companyDetails?: FieldsetVisibility; @@ -49,12 +50,12 @@ export interface OpenInvoiceStateData { } export interface OpenInvoiceStateError { - consentCheckbox?: boolean; - companyDetails?: boolean; - billingAddress?: boolean; - deliveryAddress?: boolean; - personalDetails?: boolean; - bankAccount?: boolean; + consentCheckbox?: boolean | GenericError; + companyDetails?: boolean | ValidationRuleErrorObj; + billingAddress?: boolean | ValidationRuleErrorObj; + deliveryAddress?: boolean | ValidationRuleErrorObj; + personalDetails?: boolean | ValidationRuleErrorObj; + bankAccount?: boolean | object; } export interface OpenInvoiceStateValid { diff --git a/packages/lib/src/components/internal/OpenInvoice/utils.ts b/packages/lib/src/components/internal/OpenInvoice/utils.ts index 29d6da0f..a640be13 100644 --- a/packages/lib/src/components/internal/OpenInvoice/utils.ts +++ b/packages/lib/src/components/internal/OpenInvoice/utils.ts @@ -1,4 +1,8 @@ import { OpenInvoiceActiveFieldsets, OpenInvoiceStateData, OpenInvoiceVisibility } from './types'; +import Language from '../../../language'; +import { mapFieldKey as mapFieldKeyPD } from '../PersonalDetails/utils'; +import { mapFieldKey as mapFieldKeyAddress } from '../Address/utils'; +import { StringObject } from '../Address/types'; export const fieldsetsSchema: Array = [ 'companyDetails', @@ -29,3 +33,36 @@ export const getInitialActiveFieldsets = (visibility: OpenInvoiceVisibility, dat acc[fieldset] = isVisible && (!isDeliveryAddress || billingAddressIsHidden || isPrefilled(data[fieldset])); return acc; }, {} as OpenInvoiceActiveFieldsets); + +/** + * Used by the SRPanel sorting function to tell it whether we need to prepend the field type to the SR panel message, and, if so, we retrieve the correct translation for the field type. + * (Whether we need to prepend the field type depends on whether we know that the error message correctly reflects the label of the field. Ultimately all error messages should do this + * and this mapping fn will become redundant) + */ +export const mapFieldKey = (key: string, i18n: Language, countrySpecificLabels: StringObject): string => { + let refKey = key; + let label; + + // Differentiate between address types (billing and delivery) + const splitKey = refKey.split(':'); + const hasSplitKey = splitKey.length > 1; + if (hasSplitKey) { + label = splitKey[0]; + refKey = splitKey[1]; + } + + const addressKey = mapFieldKeyAddress(refKey, i18n, countrySpecificLabels); + if (addressKey) return hasSplitKey ? `${i18n.get(label)} ${addressKey}` : addressKey; + + // Personal details related + switch (refKey) { + case 'gender': + case 'dateOfBirth': + return mapFieldKeyPD(refKey, i18n); + default: + break; + } + + // We know that the translated error messages do contain a reference to the field they refer to, so we won't need to map them + return null; +}; diff --git a/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx b/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx index b635700f..8602d9b5 100644 --- a/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx +++ b/packages/lib/src/components/internal/PersonalDetails/PersonalDetails.tsx @@ -14,7 +14,7 @@ import useForm from '../../../utils/useForm'; import './PersonalDetails.scss'; import { ComponentMethodsRef } from '../../types'; -const personalDetailsSchema = ['firstName', 'lastName', 'gender', 'dateOfBirth', 'shopperEmail', 'telephoneNumber']; +export const PERSONAL_DETAILS_SCHEMA = ['firstName', 'lastName', 'gender', 'dateOfBirth', 'shopperEmail', 'telephoneNumber']; export default function PersonalDetails(props: PersonalDetailsProps) { const { label = '', namePrefix, placeholders, requiredFields, visibility } = props; @@ -197,7 +197,7 @@ PersonalDetails.defaultProps = { data: {}, onChange: () => {}, placeholders: {}, - requiredFields: personalDetailsSchema, + requiredFields: PERSONAL_DETAILS_SCHEMA, validationRules: personalDetailsValidationRules, visibility: 'editable' }; diff --git a/packages/lib/src/components/internal/PersonalDetails/utils.ts b/packages/lib/src/components/internal/PersonalDetails/utils.ts index c6ceade7..47e28d06 100644 --- a/packages/lib/src/components/internal/PersonalDetails/utils.ts +++ b/packages/lib/src/components/internal/PersonalDetails/utils.ts @@ -1,4 +1,5 @@ import { unformatDate } from '../FormFields/InputDate/utils'; +import Language from '../../../language'; export const getFormattedData = data => { const { firstName, lastName, gender, dateOfBirth, shopperEmail, telephoneNumber } = data; @@ -16,3 +17,19 @@ export const getFormattedData = data => { ...(telephoneNumber && { telephoneNumber }) }; }; + +/** + * Used by the SRPanel sorting function to tell it whether we need to prepend the field type to the SR panel message, and, if so, we retrieve the correct translation for the field type. + * (Whether we need to prepend the field type depends on whether we know that the error message correctly reflects the label of the field. Ultimately all error messages should do this + * and this mapping fn will become redundant) + */ +export const mapFieldKey = (key: string, i18n: Language): string => { + switch (key) { + case 'gender': + case 'dateOfBirth': + return i18n.get(key); + // We know that the translated error messages do contain a reference to the field they refer to, so we won't need to map them + default: + return null; + } +}; diff --git a/packages/lib/src/components/internal/PersonalDetails/validate.ts b/packages/lib/src/components/internal/PersonalDetails/validate.ts index 1579f3db..1a19f21e 100644 --- a/packages/lib/src/components/internal/PersonalDetails/validate.ts +++ b/packages/lib/src/components/internal/PersonalDetails/validate.ts @@ -1,6 +1,7 @@ import { email, telephoneNumber } from '../../../utils/regex'; import { unformatDate } from '../FormFields/InputDate/utils'; import { ValidatorRules } from '../../../utils/Validator/types'; +import { isEmpty } from '../../../utils/validator-utils'; const isDateOfBirthValid = value => { if (!value) return false; @@ -19,31 +20,28 @@ export const personalDetailsValidationRules: ValidatorRules = { modes: ['blur'] }, firstName: { - validate: value => { - return value && value.length > 0; - }, + validate: value => (isEmpty(value) ? null : true), // valid, if there are chars other than spaces, errorMessage: 'firstName.invalid', modes: ['blur'] }, lastName: { - validate: value => { - return value && value.length > 0; - }, + validate: value => (isEmpty(value) ? null : true), errorMessage: 'lastName.invalid', modes: ['blur'] }, dateOfBirth: { - validate: value => isDateOfBirthValid(value), + validate: value => (isEmpty(value) ? null : isDateOfBirthValid(value)), errorMessage: 'dateOfBirth.invalid', modes: ['blur'] }, telephoneNumber: { - validate: value => telephoneNumber.test(value), + validate: value => (isEmpty(value) ? null : telephoneNumber.test(value)), errorMessage: 'telephoneNumber.invalid', modes: ['blur'] }, shopperEmail: { - validate: value => email.test(value), + // If it's empty it's not in error, else, is it a valid email? + validate: value => (isEmpty(value) ? null : email.test(value)), errorMessage: 'shopperEmail.invalid', modes: ['blur'] } diff --git a/packages/lib/src/core/Errors/SRPanelContext.ts b/packages/lib/src/core/Errors/SRPanelContext.ts index a85f722a..5727981a 100644 --- a/packages/lib/src/core/Errors/SRPanelContext.ts +++ b/packages/lib/src/core/Errors/SRPanelContext.ts @@ -10,6 +10,15 @@ export interface ISRPanelContext { shouldMoveFocusSR: boolean; } +// Will be needed once PersonalDetails & Address have ability to administer their own SRPanel +// export const mockSRContext: ISRPanelContext = { +// srPanel: null, +// setSRMessagesFromObjects: null, +// setSRMessagesFromStrings: null, +// clearSRPanel: null, +// shouldMoveFocusSR: null +// }; + export const SRPanelContext = createContext({ srPanel: null, setSRMessagesFromObjects: null, diff --git a/packages/lib/src/core/Errors/SRPanelProvider.tsx b/packages/lib/src/core/Errors/SRPanelProvider.tsx index e89ba665..2776647a 100644 --- a/packages/lib/src/core/Errors/SRPanelProvider.tsx +++ b/packages/lib/src/core/Errors/SRPanelProvider.tsx @@ -11,7 +11,7 @@ type SRPanelProviderProps = { children: ComponentChildren; }; -export type SetSRMessagesReturnFn = ({ errors, isValidating, layout, countrySpecificLabels }) => SetSRMessagesReturnObject; +export type SetSRMessagesReturnFn = ({ errors, isValidating, layout = null, countrySpecificLabels = null }) => SetSRMessagesReturnObject; const SRPanelProvider = ({ srPanel, children }: SRPanelProviderProps) => { const { i18n } = useCoreContext(); diff --git a/packages/playground/src/pages/OpenInvoices/OpenInvoices.html b/packages/playground/src/pages/OpenInvoices/OpenInvoices.html index fa616efb..b5d436d4 100644 --- a/packages/playground/src/pages/OpenInvoices/OpenInvoices.html +++ b/packages/playground/src/pages/OpenInvoices/OpenInvoices.html @@ -12,6 +12,15 @@
+
+
+

RatePay

+
+
+
+
+
+

RatePay Direct Debit

@@ -48,14 +57,6 @@
-
-
-

RatePay

-
-
-
-
-

Affirm

diff --git a/packages/playground/src/pages/OpenInvoices/OpenInvoices.js b/packages/playground/src/pages/OpenInvoices/OpenInvoices.js index 0828a4f1..9c5fb08c 100644 --- a/packages/playground/src/pages/OpenInvoices/OpenInvoices.js +++ b/packages/playground/src/pages/OpenInvoices/OpenInvoices.js @@ -8,6 +8,16 @@ import '../../style.scss'; window.paymentData = {}; +const showComps = { + ratepay: true, + ratepaydd: true, + afterpay: true, + afterpayb2b: true, + facilypay_3x: true, + affirm: true, + atome: true +}; + getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsData => { window.checkout = await AdyenCheckout({ clientKey: process.env.__CLIENT_KEY__, @@ -21,116 +31,130 @@ getPaymentMethods({ amount, shopperLocale }).then(async paymentMethodsData => { amount // Optional. Used to display the amount in the Pay Button. }); - // AFTERPAY - window.afterpay = checkout - .create('afterpay_default', { - countryCode: 'NL', // 'NL' / 'BE' - visibility: { - personalDetails: 'editable', // editable [default] / readOnly / hidden - billingAddress: 'readOnly', - deliveryAddress: 'hidden' - }, - data: { - billingAddress: { - city: 'Gravenhage', - country: 'NL', - houseNumberOrName: '1', - postalCode: '2521VA', - street: 'Neherkade' + // RATEPAY + if (showComps.ratepay) { + window.ratepay = checkout + .create('ratepay', { + countryCode: 'DE', // 'DE' / 'AT' / 'CH' + visibility: { + personalDetails: 'editable', // editable [default] / readOnly / hidden + billingAddress: 'editable', + deliveryAddress: 'editable' } - } - }) - .mount('.afterpay-field'); + }) + .mount('.ratepay-field'); + } + + // RATEPAY + if (showComps.ratepaydd) { + window.ratepaydd = checkout + .create('ratepay_directdebit', { + //countryCode: 'DE', // 'DE' / 'AT' / 'CH' + visibility: { + personalDetails: 'editable', // editable [default] / readOnly / hidden + billingAddress: 'editable', + deliveryAddress: 'editable' + } + }) + .mount('.ratepay-direct-field'); + } + + // AFTERPAY + if (showComps.afterpay) { + window.afterpay = checkout + .create('afterpay_default', { + countryCode: 'NL', // 'NL' / 'BE' + visibility: { + personalDetails: 'editable', // editable [default] / readOnly / hidden + billingAddress: 'readOnly', + deliveryAddress: 'hidden' + }, + data: { + billingAddress: { + city: 'Gravenhage', + country: 'NL', + houseNumberOrName: '1', + postalCode: '2521VA', + street: 'Neherkade' + } + } + }) + .mount('.afterpay-field'); + } // AFTERPAY B2B - window.afterpayb2b = checkout - .create('afterpay_b2b', { - countryCode: 'NL', // 'NL' / 'BE' - visibility: { - companyDetails: 'editable' // editable [default] / readOnly / hidden - } - }) - .mount('.afterpayb2b-field'); - - // AFFIRM - window.affirm = checkout - .create('affirm', { - countryCode: 'US', // 'US' / 'CA' - visibility: { - personalDetails: 'editable', // editable [default] / readOnly / hidden - billingAddress: 'editable', - deliveryAddress: 'editable' - }, - data: { - personalDetails: { - firstName: 'Jan', - lastName: 'Jansen', - shopperEmail: 'shopper@testemail.com', - telephoneNumber: '+17203977880' - }, - billingAddress: { - city: 'Boulder', - country: 'US', - houseNumberOrName: '242', - postalCode: '80302', - stateOrProvince: 'CO', - street: 'Silver Cloud Lane' + if (showComps.afterpayb2b) { + window.afterpayb2b = checkout + .create('afterpay_b2b', { + countryCode: 'NL', // 'NL' / 'BE' + visibility: { + companyDetails: 'editable' // editable [default] / readOnly / hidden } - } - }) - .mount('.affirm-field'); + }) + .mount('.afterpayb2b-field'); + } // FACILYPAY_3x - window.facilypay_3x = checkout - .create('facilypay_3x', { - countryCode: 'ES', // 'ES' / 'FR' - visibility: { - personalDetails: 'editable', // editable [default] / readOnly / hidden - billingAddress: 'editable', - deliveryAddress: 'editable' - } - }) - .mount('.facilypay_3x-field'); + if (showComps.facilypay_3x) { + window.facilypay_3x = checkout + .create('facilypay_3x', { + countryCode: 'ES', // 'ES' / 'FR' + visibility: { + personalDetails: 'editable', // editable [default] / readOnly / hidden + billingAddress: 'editable', + deliveryAddress: 'editable' + } + }) + .mount('.facilypay_3x-field'); + } - // RATEPAY - window.ratepay = checkout - .create('ratepay', { - countryCode: 'DE', // 'DE' / 'AT' / 'CH' - visibility: { - personalDetails: 'editable', // editable [default] / readOnly / hidden - billingAddress: 'editable', - deliveryAddress: 'editable' - } - }) - .mount('.ratepay-field'); - - // RATEPAY - window.ratepaydd = checkout - .create('ratepay_directdebit', { - //countryCode: 'DE', // 'DE' / 'AT' / 'CH' - visibility: { - personalDetails: 'editable', // editable [default] / readOnly / hidden - billingAddress: 'editable', - deliveryAddress: 'editable' - } - }) - .mount('.ratepay-direct-field'); + // AFFIRM + if (showComps.affirm) { + window.affirm = checkout + .create('affirm', { + countryCode: 'US', // 'US' / 'CA' + visibility: { + personalDetails: 'editable', // editable [default] / readOnly / hidden + billingAddress: 'editable', + deliveryAddress: 'editable' + }, + data: { + personalDetails: { + firstName: 'Jan', + lastName: 'Jansen', + shopperEmail: 'shopper@testemail.com', + telephoneNumber: '+17203977880' + }, + billingAddress: { + city: 'Boulder', + country: 'US', + houseNumberOrName: '242', + postalCode: '80302', + stateOrProvince: 'CO', + street: 'Silver Cloud Lane' + } + } + }) + .mount('.affirm-field'); + } // ATOME - window.atome = checkout - .create('atome', { - countryCode: 'SG', - data: { - personalDetails: { - firstName: 'Robert', - lastName: 'Jahnsen', - telephoneNumber: '80002018' - }, - billingAddress: { - postalCode: '111111', - street: 'Silver Cloud Lane' + if (showComps.atome) { + window.atome = checkout + .create('atome', { + countryCode: 'SG', + data: { + personalDetails: { + firstName: 'Robert', + lastName: 'Jahnsen', + telephoneNumber: '80002018' + }, + billingAddress: { + postalCode: '111111', + street: 'Silver Cloud Lane' + } } - } - }) - .mount('.atome-field'); + }) + .mount('.atome-field'); + } });