Skip to content

Commit

Permalink
[account-v2]/likhith/coj-707/Incorporated POO Form (deriv-com#14813)
Browse files Browse the repository at this point in the history
* chore: updated Deriv-ui

* feat: incorporated File upload field

* fix: incorporated File upload component

* feat: added POO form

* feat: added POO form

* feat: added POO form

* fix: incorporated review comments

* fix: incorporated review comment

* fix: incorporated code reviews

* fix: incorporated review comments
  • Loading branch information
likhith-deriv authored and vinu-deriv committed May 28, 2024
1 parent 19f55ba commit 7ea9c90
Show file tree
Hide file tree
Showing 26 changed files with 671 additions and 63 deletions.
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/account-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"start": "rimraf dist && npm run test && npm run serve"
},
"dependencies": {
"@deriv-com/ui": "^1.14.1",
"@deriv-com/ui": "^1.14.4",
"@deriv-com/utils": "^0.0.14",
"@deriv/api-types": "^1.0.172",
"@deriv/api-v2": "^1.0.0",
Expand Down
8 changes: 8 additions & 0 deletions packages/account-v2/src/components/LinkText/LinkText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React, { ComponentProps } from 'react';
import { Text } from '@deriv-com/ui';

export const LinkText = ({ children, ...rest }: ComponentProps<typeof Text>) => (
<Text as='a' color='red' rel='noreferrer' size='sm' target='_blank' {...rest}>
{children}
</Text>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { LinkText } from '../LinkText';

describe('LinkText', () => {
it('should render a link text', () => {
render(<LinkText href='link_text'>Link Text</LinkText>);
const linkText = screen.getByRole('link', { name: /Link Text/i });
expect(linkText).toBeInTheDocument();
});
});
21 changes: 15 additions & 6 deletions packages/account-v2/src/components/Timeline/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React, { ComponentProps, HTMLAttributes, PropsWithChildren, ReactElement
import { twMerge } from 'tailwind-merge';
import { Text } from '@deriv-com/ui';

type TTimelineItemProps = HTMLAttributes<HTMLDivElement> & { itemTitle: React.ReactNode };
type TTimelineItemProps = HTMLAttributes<HTMLDivElement> & { itemTitle?: React.ReactNode };

type TTimelineProps = HTMLAttributes<HTMLOListElement> & {
children: ReactElement<TTimelineItemProps>[];
children: ReactElement<TTimelineItemProps> | ReactElement<TTimelineItemProps>[];
lineHeight?: ComponentProps<typeof Text>['lineHeight'];
};

Expand All @@ -22,6 +22,7 @@ const Marker = ({ label }: { label: number }) => {
</div>
);
};

/**
* @deprecated TODO: Replace this component with the one from @deriv-com/ui is implemented.
*/
Expand All @@ -37,10 +38,18 @@ export const Timeline = ({ children, className, lineHeight }: TTimelineProps) =>
>
<Marker label={idx + 1} />
<div className='ml-20 w-full'>
<Text as='h2' className='max-w-[500px]' color='prominent' lineHeight={lineHeight} size='xs'>
{child.props.itemTitle}
</Text>
<div className='my-16 mx-0 text-system-light-prominent-text last-of-type:mb-0'>{child}</div>
{child.props.itemTitle && (
<Text
as='h2'
className='max-w-[500px]'
color='prominent'
lineHeight={lineHeight}
size='xs'
>
{child.props.itemTitle}
</Text>
)}
<div className='mx-0 text-system-light-prominent-text'>{child}</div>
</div>
</li>
);
Expand Down
16 changes: 16 additions & 0 deletions packages/account-v2/src/constants/manualFormConstants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ export const MANUAL_DOCUMENT_TYPES = Object.freeze({
selfieWithID: 'selfie_with_id',
});

export const UPLOAD_FILE_TYPE = Object.freeze({
amlglobalcheck: 'amlglobalcheck',
bankstatement: 'bankstatement',
docverification: 'docverification',
driverslicense: 'driverslicense',
driving_licence: 'driving_licence',
national_identity_card: 'national_identity_card',
other: 'other',
passport: 'passport',
powerOfAttorney: 'power_of_attorney',
proofaddress: 'proofaddress',
proofid: 'proofid',
proofOfOwnership: 'proof_of_ownership',
utilityBill: 'utility_bill',
});

export const MANUAL_FORM_PAGE_TYPES = Object.freeze({
back: 'back',
front: 'front',
Expand Down
9 changes: 8 additions & 1 deletion packages/account-v2/src/constants/paymentMethodsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const getPaymentMethodsConfig = () => ({

type TPaymentMethodIcon = Record<TPaymentMethod, { dark: IconTypes; light: IconTypes }>;

export const getPaymentMethodIcon: TPaymentMethodIcon = {
export const getPaymentMethodIcon = (): TPaymentMethodIcon => ({
advcash: {
dark: PaymentMethodAdvcashBrandDarkIcon,
light: PaymentMethodAdvcashBrandIcon,
Expand Down Expand Up @@ -164,4 +164,11 @@ export const getPaymentMethodIcon: TPaymentMethodIcon = {
dark: DerivLightWalletIcon,
light: DerivLightWalletIcon,
},
});

export const CARD_NUMBER = {
maxLength: 19,
minLength: 16,
};

export const MAX_FILE_SIZE = 8000; // 8MB
151 changes: 151 additions & 0 deletions packages/account-v2/src/containers/POOForm/FileUploadField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import React, { ChangeEvent, MouseEvent, SyntheticEvent, useRef, useState } from 'react';
import clsx from 'clsx';
import { Field, FormikErrors, useFormikContext } from 'formik';
import { StandaloneXmarkRegularIcon } from '@deriv/quill-icons';
import { Button, Input } from '@deriv-com/ui';
import { TPaymentMethod, TProofOfOwnershipFormValue } from 'src/types';
import { compressImageFiles, TFile } from 'src/utils';

type TFileUploadFieldProps = {
methodId: number;
paymentMethod: TPaymentMethod;
subIndex: number;
};

export const FileUploaderField = ({ methodId, paymentMethod, subIndex }: TFileUploadFieldProps) => {
const formik = useFormikContext<TProofOfOwnershipFormValue>();
const { errors, setFieldError, setFieldValue, values } = formik;
const [showBrowseButton, setShowBrowseButton] = useState(
!values[paymentMethod]?.[methodId]?.files?.[subIndex]?.name
);

if (!formik) {
throw new Error('FileUploaderField must be used within a Formik component');
}

// Create a reference to the hidden file input element
const hiddenInputFieldRef = useRef<HTMLInputElement>(null);

const preventEventBubble = (e: SyntheticEvent) => {
e.nativeEvent.preventDefault();
e.nativeEvent.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
};

const handleChange = async (event: ChangeEvent<HTMLInputElement>) => {
preventEventBubble(event);
// Check if files exist before proceeding
if (!event.target.files || event.target.files.length === 0) {
return;
}
const fileToUpload = await compressImageFiles([event.target.files[0]]);
const paymentFileData = [...(values[paymentMethod]?.[methodId]?.files ?? [])];
paymentFileData[subIndex] = fileToUpload[0] as TFile;
const selectedPaymentMethod = values?.[paymentMethod];
if (!selectedPaymentMethod) {
return;
}
selectedPaymentMethod[methodId] = {
...selectedPaymentMethod[methodId],
files: paymentFileData ?? [],
};
await setFieldValue(paymentMethod, { ...selectedPaymentMethod });
setShowBrowseButton(!fileToUpload[0]);
};

const handleClick = (event: MouseEvent) => {
preventEventBubble(event);
hiddenInputFieldRef?.current?.click();
};

const updateError = () => {
const paymentMethodError = errors?.[paymentMethod] ?? {};
const paymentMethodFileError = (paymentMethodError?.[methodId]?.files as FormikErrors<TFile>[]) ?? {};

delete paymentMethodFileError?.[subIndex];
paymentMethodError[methodId] = {
...(paymentMethodError[methodId] ?? {}),
files: paymentMethodFileError,
};

// [TODO] - Need to check the logic for removing the paymentMethodIdentifier
if (Object.keys(paymentMethodError[methodId]?.files as object).length === 0) {
delete paymentMethodError[methodId]?.paymentMethodIdentifier;
}
// @ts-expect-error Error is an array
setFieldError(paymentMethod, { ...paymentMethodFileError });
};

const handleIconClick = async (e: React.MouseEvent) => {
e.nativeEvent.preventDefault();
e.nativeEvent.stopPropagation();
e.nativeEvent.stopImmediatePropagation();

if (hiddenInputFieldRef.current && 'value' in hiddenInputFieldRef.current) {
hiddenInputFieldRef.current.value = '';
}
const paymentFileData = values[paymentMethod]?.[methodId]?.files ?? [];
const filteredFileData = paymentFileData.filter((_, i) => i !== subIndex);
const selectedPaymentMethod = values?.[paymentMethod];
if (!selectedPaymentMethod) {
return;
}
selectedPaymentMethod[methodId] = {
...selectedPaymentMethod[methodId],
files: filteredFileData,
};
await setFieldValue(paymentMethod, { ...selectedPaymentMethod });
setShowBrowseButton(prevState => !prevState);
updateError();
};

return (
<Field name={paymentMethod}>
{() => {
const errorMessage = errors?.[paymentMethod]?.[methodId]?.files?.[subIndex];
return (
<div className='flex gap-8'>
<input
accept='image/png, image/jpeg, image/jpg, application/pdf'
className='hidden'
name={paymentMethod}
onChange={handleChange}
ref={hiddenInputFieldRef}
type='file'
/>
<Input
error={Boolean(errorMessage)}
isFullWidth
label='Choose a photo'
maxLength={255}
message={
errorMessage
? errorMessage?.toString()
: 'Accepted formats: pdf, jpeg, jpg, and png. Max file size: 8MB'
}
name='cardImgName'
readOnly
rightPlaceholder={
<Button
className={clsx({ hidden: showBrowseButton })}
color='white'
onClick={handleIconClick}
size='md'
type='button'
variant='ghost'
>
<StandaloneXmarkRegularIcon height={20} width={20} />
</Button>
}
type='text'
value={values[paymentMethod]?.[methodId]?.files?.[subIndex]?.name ?? ''}
/>
<Button onClick={handleClick} size='lg' type='button'>
Browse
</Button>
</div>
);
}}
</Field>
);
};
66 changes: 66 additions & 0 deletions packages/account-v2/src/containers/POOForm/POOFom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React, { useEffect, useRef } from 'react';
import { Form, Formik, FormikProps } from 'formik';
import { useSettings } from '@deriv/api-v2';
import { Accordion, Button, Divider, Loader } from '@deriv-com/ui';
import { Timeline } from 'src/components/Timeline';
import { TPaymentMethod, TPaymentMethodData, TProofOfOwnershipFormValue } from 'src/types';
import { generatePOOInitialValues } from 'src/utils';
import { PaymentMethodForm, PaymentMethodTitle } from '../PaymentMethods';

type TPOOFormProps = {
paymentMethodData: TPaymentMethodData;
};

export const POOForm = ({ paymentMethodData }: TPOOFormProps) => {
const { isLoading } = useSettings();
const formRef = useRef<FormikProps<TProofOfOwnershipFormValue>>(null);
const paymentMethods = Object.keys(paymentMethodData) as TPaymentMethod[];

useEffect(() => {
if (formRef.current) {
formRef.current.resetForm();
}
}, [paymentMethods]);

if (isLoading) {
return <Loader isFullScreen />;
}

const initialFormValues = generatePOOInitialValues(paymentMethodData);

return (
<Formik
enableReinitialize
initialValues={initialFormValues}
innerRef={formRef}
onSubmit={() => {
//TODO: Implement onSubmit
}}
>
{({ dirty, isSubmitting, isValid }) => (
<Form className='grid h-full'>
<Timeline className='pt-0 px-14 pb-16 m-12 w-full text-lg'>
{paymentMethods.map((type, index) => (
<Timeline.Item key={`${type}_${index}`}>
<Accordion
title={<PaymentMethodTitle paymentMethod={paymentMethodData[type].paymentMethod} />}
variant='bordered'
>
<PaymentMethodForm paymentMethodDetail={paymentMethodData[type]} />
</Accordion>
</Timeline.Item>
))}
</Timeline>
<section className='flex gap-8 flex-col justify-end'>
<Divider />
<div className='flex gap-8 justify-end'>
<Button disabled={!isValid || isSubmitting || !dirty} rounded='sm' size='lg' type='submit'>
Submit
</Button>
</div>
</section>
</Form>
)}
</Formik>
);
};
Loading

0 comments on commit 7ea9c90

Please sign in to comment.