mirror of
https://github.com/jlengrand/adyen-web.git
synced 2026-03-10 08:01:22 +00:00
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
This commit is contained in:
@@ -375,6 +375,7 @@ const CardInput: FunctionalComponent<CardInputProps> = 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)
|
||||
|
||||
@@ -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<OpenInvoiceProps> {
|
||||
consentCheckboxLabel?: h.JSX.Element;
|
||||
@@ -86,14 +87,16 @@ export default class OpenInvoiceContainer extends UIElement<OpenInvoiceContainer
|
||||
render() {
|
||||
return (
|
||||
<CoreProvider i18n={this.props.i18n} loadingContext={this.props.loadingContext}>
|
||||
<OpenInvoice
|
||||
setComponentRef={this.setComponentRef}
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
onChange={this.setState}
|
||||
onSubmit={this.submit}
|
||||
payButton={this.payButton}
|
||||
/>
|
||||
<SRPanelProvider srPanel={this.props.modules.srPanel}>
|
||||
<OpenInvoice
|
||||
setComponentRef={this.setComponentRef}
|
||||
{...this.props}
|
||||
{...this.state}
|
||||
onChange={this.setState}
|
||||
onSubmit={this.submit}
|
||||
payButton={this.payButton}
|
||||
/>
|
||||
</SRPanelProvider>
|
||||
</CoreProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,8 @@ export default function Address(props: AddressProps) {
|
||||
const { data, errors, valid, isValid, handleChangeFor, triggerValidation } = useForm<AddressData>({
|
||||
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 (
|
||||
<Fragment>
|
||||
<Fieldset classNameModifiers={[label]} label={label}>
|
||||
<Fieldset classNameModifiers={[label || 'address']} label={label}>
|
||||
{addressSchema.map(field => (field instanceof Array ? getWrapper(field) : getComponent(field, {})))}
|
||||
</Fieldset>
|
||||
{/* Needed to easily test when showValidation is called */}
|
||||
|
||||
@@ -22,10 +22,7 @@ function getErrorMessage(errors: AddressStateError, fieldName: string, i18n: Lan
|
||||
* - then you should implement <CountryField> or <StateField> 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
|
||||
|
||||
16
packages/lib/src/components/internal/Address/utils.ts
Normal file
16
packages/lib/src/components/internal/Address/utils.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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<CompanyDetailsSchema>({
|
||||
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
|
||||
};
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,6 +162,9 @@ class IbanInput extends Component<IbanInputProps, IbanInputState> {
|
||||
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<IbanInputProps, IbanInputState> {
|
||||
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)
|
||||
})}
|
||||
</Field>
|
||||
)}
|
||||
|
||||
@@ -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<OpenInvoiceActiveFieldsets>(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]);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<keyof OpenInvoiceStateData> = [
|
||||
'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;
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
@@ -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<ISRPanelContext>({
|
||||
srPanel: null,
|
||||
setSRMessagesFromObjects: null,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,6 +12,15 @@
|
||||
<header></header>
|
||||
<main>
|
||||
<form class="merchant-checkout__form" method="post">
|
||||
<div class="merchant-checkout__payment-method">
|
||||
<div class="merchant-checkout__payment-method__header">
|
||||
<h2>RatePay</h2>
|
||||
</div>
|
||||
<div class="merchant-checkout__payment-method__details">
|
||||
<div class="ratepay-field"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="merchant-checkout__payment-method">
|
||||
<div class="merchant-checkout__payment-method__header">
|
||||
<h2>RatePay Direct Debit</h2>
|
||||
@@ -48,14 +57,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="merchant-checkout__payment-method">
|
||||
<div class="merchant-checkout__payment-method__header">
|
||||
<h2>RatePay</h2>
|
||||
</div>
|
||||
<div class="merchant-checkout__payment-method__details">
|
||||
<div class="ratepay-field"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="merchant-checkout__payment-method">
|
||||
<div class="merchant-checkout__payment-method__header">
|
||||
<h2>Affirm</h2>
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user