Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Arshad/ COJ-481 / Address Fields #13074

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, { ComponentProps } from 'react';
import { Field, FieldProps } from 'formik';
import * as Yup from 'yup';
import { useBreakpoint } from '@deriv/quill-design';
import { WalletDropdown as DropDown } from '../base/WalletDropdown';

type FormDropDownFieldProps = Omit<
ComponentProps<typeof DropDown>,
'errorMessage' | 'isRequired' | 'onSelect' | 'variant'
> & {
name: string;
validationSchema?: Yup.AnySchema;
};

/**
* FormDropDownField is a wrapper around Dropdown that can be used with Formik.
* @name FormDropDownField
* @param name - Name of the field
* @param [props] - Other props to pass to Input
* @returns ReactNode
*/
const FormDropDownField = ({ name, validationSchema, ...rest }: FormDropDownFieldProps) => {
const { isMobile } = useBreakpoint();

const validateField = (value: unknown) => {
try {
if (validationSchema) {
validationSchema.validateSync(value);
}
} catch (err: unknown) {
return (err as Yup.ValidationError).message;
}
};

return (
<Field name={name} validate={validateField}>
{({ field, form, meta: { error, touched } }: FieldProps<string>) => (
<DropDown
{...field}
{...rest}
errorMessage={error}
isRequired={touched && !!error}
onSelect={(value: string) => form.setFieldValue(name, value)}
variant={isMobile ? 'prompt' : 'comboBox'}
/>
)}
</Field>
);
};

export default FormDropDownField;
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, { ComponentProps } from 'react';
import { Field, FieldProps } from 'formik';
import * as Yup from 'yup';
import { WalletTextField as TextField } from '../base/WalletTextField';

type FormInputFieldProps = Omit<ComponentProps<typeof TextField>, 'errorMessage' | 'isInvalid' | 'showMessage'> & {
name: string;
validationSchema?: Yup.AnySchema;
};

/**
* FormInputField is a wrapper around Input that can be used with Formik.
* @name FormInputField
* @param name - Name of the field
* @param [validationSchema] - Yup validation schema to use for the field
* @param [props] - Other props to pass to Input
* @returns ReactNode
*/
const FormInputField = ({ name, validationSchema, ...rest }: FormInputFieldProps) => {
const validateField = (value: unknown) => {
try {
if (validationSchema) {
validationSchema.validateSync(value);
}
} catch (err: unknown) {
return (err as Yup.ValidationError).message;
}
};

return (
<Field name={name} validate={validateField}>
{({ field, meta: { error, touched } }: FieldProps<string>) => (
<TextField
{...field}
{...rest}
autoComplete='off'
errorMessage={touched && error}
isInvalid={touched && !!error}
showMessage
type='text'
/>
)}
</Field>
);
};

export default FormInputField;
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
label,
&__field:focus ~ &__label {
position: absolute;
top: -0.5rem;
top: 0;
display: block;
transition: 0.2s;
font-size: 1rem;
Expand Down
70 changes: 70 additions & 0 deletions packages/account-v2/src/modules/AddressFields/AddressFields.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import { useAuthorize, useSettings, useStatesList } from '@deriv/api';
import FormDropDownField from '../../components/FormFields/FormDropDownField';
import FormInputField from '../../components/FormFields/FormInputField';
import { addressDetailValidations } from './validations';

export const AddressFields = () => {
const { data: activeAccount } = useAuthorize();
const { data: settings } = useSettings();

const { landing_company_name: landingCompanyName, upgradeable_landing_companies: upgradableLandingCompanies } =
activeAccount;

const isSvg = landingCompanyName === 'svg' || !!upgradableLandingCompanies?.includes('svg');
const { data: statesList, isFetched: statesListFetched } = useStatesList(settings.country_code || '', {
enabled: !!settings.country_code,
});

const {
addressCity: addressCitySchema,
addressLine1: addressLine1Schema,
addressLine2: addressLine2Schema,
addressPostcode: addressPostcodeSchema,
addressState: addressStateSchema,
} = addressDetailValidations(settings.country_code || '', isSvg);

return (
<div className='space-y-600'>
<FormInputField
label='First line of address*'
name='addressLine1'
placeholder='First line of address'
validationSchema={addressLine1Schema}
/>
<FormInputField
label='Second line of address'
name='addressLine2'
placeholder='Second line of address'
validationSchema={addressLine2Schema}
/>
<FormInputField
label='Town/City*'
name='addressCity'
placeholder='Town/City'
validationSchema={addressCitySchema}
/>
{statesListFetched && statesList.length ? (
<FormDropDownField
label='State/Province'
list={statesList}
name='addressState'
validationSchema={addressStateSchema}
/>
) : (
<FormInputField
label='State/Province'
name='addressState'
placeholder='State/Province'
validationSchema={addressStateSchema}
/>
)}
<FormInputField
label='Postal/ZIP Code'
name='addressPostcode'
placeholder='Postal/ZIP Code'
validationSchema={addressPostcodeSchema}
/>
</div>
);
};
1 change: 1 addition & 0 deletions packages/account-v2/src/modules/AddressFields/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AddressFields } from './AddressFields';
66 changes: 66 additions & 0 deletions packages/account-v2/src/modules/AddressFields/validations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as Yup from 'yup';

const regexChecks = {
address_details: {
address_city: /^\p{L}[\p{L}\s'.-]{0,99}$/u,
address_line_1: /^[\p{L}\p{Nd}\s'.,:;()\u00b0@#/-]{1,70}$/u,
address_line_2: /^[\p{L}\p{Nd}\s'.,:;()\u00b0@#/-]{0,70}$/u,
address_postcode: /^[a-zA-Z0-9\s-]{0,20}$/,
address_state: /^[\w\s\W'.;,-]{0,99}$/,
non_jersey_postcode: /^(?!\s*je.*)[a-zA-Z0-9\s-]*/i,
},
};

const addressPermittedSpecialCharactersMessage = ". , ' : ; ( ) ° @ # / -";

export const addressDetailValidations = (countryCode: string, isSvg: boolean) => ({
addressCity: Yup.string()
.required('City is required')
.max(99, 'Only 99 characters, please.')
.matches(
regexChecks.address_details.address_city,
'Only letters, periods, hyphens, apostrophes, and spaces, please.'
),
addressLine1: Yup.string()
.required('First line of address is required')
.max(70, 'Only 70 characters, please.')
.matches(
regexChecks.address_details.address_line_1,
`Use only the following special characters: ${addressPermittedSpecialCharactersMessage}`
)
.when({
is: () => isSvg,
then: Yup.string().test(
'po_box',
'P.O. Box is not accepted in address',
value => !/p[.\s]+o[.\s]+box/i.test(value ?? '')
),
}),
addressLine2: Yup.string()
.max(70, 'Only 70 characters, please.')
.matches(
regexChecks.address_details.address_line_2,
`Use only the following special characters: ${addressPermittedSpecialCharactersMessage}`
)
.when({
is: () => isSvg,
then: Yup.string().test(
'po_box',
'P.O. Box is not accepted in address',
value => !/p[.\s]+o[.\s]+box/i.test(value ?? '')
),
}),
addressPostcode: Yup.string()
.max(20, 'Please enter a postal/ZIP code under 20 characters.')
.matches(regexChecks.address_details.address_postcode, 'Letters, numbers, spaces, hyphens only')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we dont have localize in v2 packages for now @suisin-deriv

.when({
is: () => countryCode === 'gb',
then: Yup.string().matches(
regexChecks.address_details.non_jersey_postcode,
'Our accounts and services are unavailable for the Jersey postal code.'
),
}),
addressState: Yup.string()
.required('State is required')
.matches(regexChecks.address_details.address_state, 'State is not in a proper format'),
});
4 changes: 3 additions & 1 deletion packages/api/src/hooks/useStatesList.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useMemo } from 'react';
import useQuery from '../useQuery';
import useSettings from './useSettings';
import { TSocketRequestQueryOptions } from '../../types';

/** Custom hook to get states list for a particular country. */
type TStatesList = Exclude<NonNullable<ReturnType<typeof useSettings>['data']['residence' | 'country']>, undefined>;

const useStatesList = (country: TStatesList) => {
const useStatesList = (country: TStatesList, options?: TSocketRequestQueryOptions<'states_list'>) => {
const { data, ...rest } = useQuery('states_list', {
// @ts-expect-error The `states_list` type from `@deriv/api-types` is not correct.
// The type should be `string`, but it's an alias to string type.
payload: { states_list: country },
options,
});

const modified_states_list = useMemo(() => [...(data?.states_list || [])], [data?.states_list]);
Expand Down