Skip to content

Commit

Permalink
[account-v2]/likhith/COJ-667/Create account-closure-form and modal (b…
Browse files Browse the repository at this point in the history
…inary-com#14208)

* feat: created form and modal

* fix: failing testcase

* fix: ESlint issues

* chore: added testcases

* feat: added modal and testcases

* feat: added modal and testcases

* chore: added additional testcases

* fix: failing testcase

* chore: added testcases for utils

* fix: resolved code comments
  • Loading branch information
likhith-deriv committed Mar 19, 2024
1 parent 183f2e7 commit b05e4b4
Show file tree
Hide file tree
Showing 17 changed files with 726 additions and 9 deletions.
3 changes: 3 additions & 0 deletions packages/account-v2/src/App.tsx
Expand Up @@ -3,12 +3,15 @@ import React from 'react';
import { APIProvider, AuthProvider } from '@deriv/api-v2';
import { AppOverlay } from './components/AppOverlay';
import { RouteLinks } from './router/components/RouteLinks';
import { ACCOUNT_MODAL_REF } from './constants';
import './index.scss';

const App: React.FC = () => {
return (
<APIProvider standalone>
<AuthProvider>
{/* This will be the used to bind modal in Accounts-v2 package*/}
<div id={ACCOUNT_MODAL_REF.replace('#', '')} />
<AppOverlay title='Settings'>
<RouteLinks />
</AppOverlay>
Expand Down
62 changes: 62 additions & 0 deletions packages/account-v2/src/constants/accountClosureReasons.ts
@@ -0,0 +1,62 @@
import { TAccountClosureReasonsFormValues } from '../utils';

export const accountClosureReasons = (): {
label: string;
ref: string;
value: string;
}[] => [
{
label: 'I have other financial priorities.',
ref: 'financialPriorities',
value: 'financial-priorities',
},
{
label: 'I want to stop myself from trading.',
ref: 'stopTrading',
value: 'stop-trading',
},
{
label: "I'm no longer interested in trading.",
ref: 'notInterested',
value: 'not-interested',
},
{
label: 'I prefer another trading website.',
ref: 'anotherWebsite',
value: 'another-website',
},
{
label: "The platforms aren't user friendly.",
ref: 'notUserFriendly',
value: 'not-user-friendly',
},
{
label: 'Making deposits and withdrawals is difficult.',
ref: 'difficultTransactions',
value: 'difficult-transactions',
},
{
label: 'The platforms lack key features or functionality.',
ref: 'lackOfFeatures',
value: 'lack-of-features',
},
{
label: 'Customer service was unsatisfactory.',
ref: 'unsatisfactoryService',
value: 'unsatisfactory-service',
},
{
label: "I'm closing my account for other reasons.",
ref: 'otherReasons',
value: 'other-reasons',
},
];

export const MAX_ALLOWED_REASONS_FOR_CLOSING_ACCOUNT = 3;
export const CHARACTER_LIMIT_FOR_CLOSING_ACCOUNT = 110;

export type TAccountClosureFormActions =
| { payload: boolean; type: 'displayConfirmModal' }
| { payload: boolean; type: 'displaySuccessModal' }
| { payload: TAccountClosureReasonsFormValues; type: 'disableCheckbox' }
| { payload: TAccountClosureReasonsFormValues; type: 'remainingCharacters' };
2 changes: 2 additions & 0 deletions packages/account-v2/src/constants/constants.ts
Expand Up @@ -20,3 +20,5 @@ export const POI_SERVICE = {
manual: 'manual',
onfido: 'onfido',
} as const;

export const ACCOUNT_MODAL_REF = '#account_modal';
7 changes: 7 additions & 0 deletions packages/account-v2/src/constants/index.ts
@@ -0,0 +1,7 @@
export {
accountClosureReasons,
CHARACTER_LIMIT_FOR_CLOSING_ACCOUNT,
MAX_ALLOWED_REASONS_FOR_CLOSING_ACCOUNT,
type TAccountClosureFormActions,
} from './accountClosureReasons';
export * from './constants';
@@ -0,0 +1,36 @@
import React from 'react';
import { StandaloneTriangleExclamationRegularIcon } from '@deriv/quill-icons';
import { Button, Modal, Text } from '@deriv-com/ui';

type TAccountClosureConfirmModalProps = {
handleCancel: () => void;
handleSubmit: () => void;
isModalOpen: boolean;
};

export const AccountClosureConfirmModal = ({
handleCancel,
handleSubmit,
isModalOpen,
}: TAccountClosureConfirmModalProps) => (
<Modal className='p-24 w-[440px] sm:w-[312px]' isOpen={isModalOpen}>
<Modal.Body className='flex flex-col'>
<StandaloneTriangleExclamationRegularIcon className='self-center fill-status-light-danger' iconSize='2xl' />
<Text align='center' as='h4' size='md' weight='bold'>
Close your account?
</Text>
<Text align='center' as='p' className='mt-24' size='sm'>
Closing your account will automatically log you out. We shall delete your personal information as soon
as our legal obligations are met.
</Text>
</Modal.Body>
<Modal.Footer className='mt-24 flex gap-x-16 justify-end' hideBorder>
<Button color='black' onClick={handleCancel} rounded='sm' size='md' type='button' variant='outlined'>
Go back
</Button>
<Button color='primary' onClick={handleSubmit} rounded='sm' size='md'>
Close account
</Button>
</Modal.Footer>
</Modal>
);
@@ -0,0 +1,176 @@
import React, { Fragment, useReducer, useRef } from 'react';
import { Field, Form, Formik, FormikProps } from 'formik';
import { Button, Checkbox, Modal, Text, TextArea } from '@deriv-com/ui';
import {
ACCOUNT_MODAL_REF,
accountClosureReasons,
CHARACTER_LIMIT_FOR_CLOSING_ACCOUNT,
MAX_ALLOWED_REASONS_FOR_CLOSING_ACCOUNT,
TAccountClosureFormActions,
} from '../../constants';
import {
getAccountClosureValidationSchema,
TAccountClosureReasonsFormValues,
validateAccountClosure,
} from '../../utils/accountClosureUtils';
import { AccountClosureConfirmModal } from './AccountClosureConfirmModal';
import { AccountClosureSuccessModal } from './AccountClosureSuccessModal';

export const AccountClosureForm = ({ handleOnBack }: { handleOnBack: () => void }) => {
Modal.setAppElement(ACCOUNT_MODAL_REF);
const reasons = accountClosureReasons();
const validationSchema = getAccountClosureValidationSchema();

const formRef = useRef<FormikProps<TAccountClosureReasonsFormValues>>(null);

const isReasonNotSelected = !validateAccountClosure(
formRef.current?.values as TAccountClosureReasonsFormValues,
formRef.current?.dirty ?? false
);

const initialState = {
disableCheckbox: false,
displayConfirmModal: false,
displaySuccessModal: false,
remainingCharacters: CHARACTER_LIMIT_FOR_CLOSING_ACCOUNT,
};

const reducer = (state: typeof initialState, action: TAccountClosureFormActions) => {
switch (action.type) {
case 'disableCheckbox': {
const disableCheckbox =
Object.values(action.payload).filter(Boolean).length >= MAX_ALLOWED_REASONS_FOR_CLOSING_ACCOUNT;
return { ...state, disableCheckbox };
}

case 'remainingCharacters': {
const { doToImprove, otherTradingPlatforms } = action.payload;
const remainingCharacters =
CHARACTER_LIMIT_FOR_CLOSING_ACCOUNT -
(doToImprove ?? '').concat(otherTradingPlatforms ?? '').length;
return { ...state, remainingCharacters };
}
case 'displayConfirmModal': {
return { ...state, displayConfirmModal: !state.displayConfirmModal };
}
case 'displaySuccessModal': {
return { ...state, displaySuccessModal: !state.displaySuccessModal };
}
default:
return state;
}
};

const [state, dispatch] = useReducer(reducer, initialState);

return (
<Fragment>
<Formik
initialValues={validationSchema.getDefault()}
innerRef={formRef}
onSubmit={() => dispatch({ payload: true, type: 'displayConfirmModal' })}
validationSchema={validationSchema}
>
{({ dirty, setFieldValue, values }) => (
<Form>
<section>
<div className='gap-8 flex flex-col my-16'>
{reasons.map(({ label, ref, value }) => (
<Field
as={Checkbox}
disabled={
state.disableCheckbox &&
!values[ref as keyof TAccountClosureReasonsFormValues]
}
key={value}
label={label}
name={ref}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setFieldValue(ref, event.target.checked);
dispatch({
payload: { ...values, [ref]: event.target.checked },
type: 'disableCheckbox',
});
}}
type='checkbox'
/>
))}
</div>
<Field
aria-label="If you don't mind sharing, which other trading platforms do you use?"
as={TextArea}
className='mb-12'
label="If you don't mind sharing, which other trading platforms do you use?"
name='otherTradingPlatforms'
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
setFieldValue('otherTradingPlatforms', event.target.value);
dispatch({
payload: { ...values, otherTradingPlatforms: event.target.value },
type: 'remainingCharacters',
});
}}
role='textarea'
textSize='sm'
/>
<Field
aria-label='What could we do to improve?'
as={TextArea}
hint={`Remaining characters: ${state.remainingCharacters}`}
label='What could we do to improve?'
name='doToImprove'
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
setFieldValue('doToImprove', event.target.value);
dispatch({
payload: { ...values, doToImprove: event.target.value },
type: 'remainingCharacters',
});
}}
role='textarea'
textSize='sm'
/>
</section>
<section className='mt-24 flex gap-x-16 justify-end'>
<Button
color='black'
onClick={handleOnBack}
rounded='sm'
size='md'
type='button'
variant='outlined'
>
Back
</Button>
<Button
color='primary'
disabled={!dirty || isReasonNotSelected}
rounded='sm'
size='md'
type='submit'
variant='contained'
>
Continue
</Button>
</section>
{isReasonNotSelected && (
<Text as='p' className='mt-16' color='error' size='xs' weight='bold'>
Please select at least one reason
</Text>
)}
</Form>
)}
</Formik>
<AccountClosureConfirmModal
handleCancel={() => dispatch({ payload: false, type: 'displayConfirmModal' })}
handleSubmit={() => {
dispatch({ payload: false, type: 'displayConfirmModal' });
dispatch({ payload: true, type: 'displaySuccessModal' });
}}
isModalOpen={state.displayConfirmModal}
/>
<AccountClosureSuccessModal
handleClose={() => dispatch({ payload: false, type: 'displaySuccessModal' })}
isModalOpen={state.displaySuccessModal}
/>
</Fragment>
);
};
@@ -0,0 +1,26 @@
import React from 'react';
import { Modal, Text } from '@deriv-com/ui';

type TAccountClosureSuccessModalProps = {
handleClose: () => void;
isModalOpen: boolean;
};

export const AccountClosureSuccessModal = ({ handleClose, isModalOpen }: TAccountClosureSuccessModalProps) => (
<Modal
className='p-24 w-[440px] sm:w-[312px]'
isOpen={isModalOpen}
onRequestClose={handleClose}
shouldCloseOnEsc
shouldCloseOnOverlayClick
>
<Modal.Body className='flex flex-col'>
<Text align='center' as='p' size='md' weight='bold'>
We&apos;re sorry to see you leave.
</Text>
<Text align='center' as='p' size='md' weight='bold'>
Your account is now closed.
</Text>
</Modal.Body>
</Modal>
);
@@ -0,0 +1,65 @@
import React from 'react';
import { Modal } from '@deriv-com/ui';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ACCOUNT_MODAL_REF } from '../../../constants';
import { AccountClosureConfirmModal } from '../AccountClosureConfirmModal';

describe('AccountClosureConfirmModal', () => {
let elModalRoot: HTMLElement;
beforeAll(() => {
elModalRoot = document.createElement('div');
elModalRoot.setAttribute('id', ACCOUNT_MODAL_REF.replace('#', ''));
document.body.appendChild(elModalRoot);
Modal.setAppElement(ACCOUNT_MODAL_REF);
});

afterAll(() => {
document.body.removeChild(elModalRoot);
});

const handleCancel = jest.fn();
const handleSubmit = jest.fn();

it('should render modal', () => {
render(
<AccountClosureConfirmModal handleCancel={handleCancel} handleSubmit={handleSubmit} isModalOpen={true} />
);

expect(screen.getByRole('dialog')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Go back/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Close account/i })).toBeInTheDocument();
});

it('should call handleCancel when clicking Go back', () => {
render(
<AccountClosureConfirmModal handleCancel={handleCancel} handleSubmit={handleSubmit} isModalOpen={true} />
);

const goBackButton = screen.getByRole('button', { name: /Go back/i });
userEvent.click(goBackButton);

expect(handleCancel).toHaveBeenCalledTimes(1);
});

it('should call handleSubmit when clicking Close account', () => {
render(
<AccountClosureConfirmModal handleCancel={handleCancel} handleSubmit={handleSubmit} isModalOpen={true} />
);

const closeAccountButton = screen.getByRole('button', { name: /Close account/i });
userEvent.click(closeAccountButton);

expect(handleSubmit).toHaveBeenCalledTimes(1);
});

it('should not call handleCancel when clicking escape button', () => {
render(
<AccountClosureConfirmModal handleCancel={handleCancel} handleSubmit={handleSubmit} isModalOpen={true} />
);

userEvent.keyboard('{esc}');

expect(screen.getByRole('dialog')).toBeInTheDocument();
});
});

0 comments on commit b05e4b4

Please sign in to comment.