Skip to content

Commit 22b0bcb

Browse files
Arshad COJ-689 Leave confirm modal (deriv-com#14445)
* feat: added financial assessment and trading experience fields * test: updated unit tests * fix: refactoring financial assessment fields * fix: removed unused import * fix: extracted config, replaced variables * fix: fixed import path for trading information list * feat: Added Financial Assessment Wrapper for account-v2 * fix: Added unit test for hook, fixed changes for form * test: updated unit tests * fix: fix financial assessment types * fix: updated unit tests and fixed types * fix: fixed types * fix: refactored unit tests * fix: Remove trading experience fields * feat: Leave confirm modal for account-v2 forms * fix: remove appropriateness test modal * fix: removed unused imports * test: Add test for Leave Confirm Modal * fix: fix type * fix: refactored condition * refactor: refactored tests and removed unnecessary useCallback * refactor: refactored test and removed isRequired prop * refactor: revert scss prettier change * refactor: refactoed, updated unit tests * fix: fixed src path on modules webpack * fix: removed unnecessary fragment * fix: fix test and sonar cloud issue * refactor: extracted conditions to a constant * fix: remove unncessary styles
1 parent a4a6568 commit 22b0bcb

21 files changed

+292
-129
lines changed

packages/account-v2/.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ module.exports = {
7979
'^[a-z]',
8080
// Packages starting with `@`
8181
'^@',
82+
'^src',
8283
// Imports starting with `../`
8384
'^\\.\\.(?!/?$)',
8485
'^\\.\\./?$',

packages/account-v2/src/App.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
/* eslint-disable @typescript-eslint/no-empty-function */
22
import React from 'react';
33
import { APIProvider, AuthProvider } from '@deriv/api-v2';
4+
import { Modal } from '@deriv-com/ui';
45
import { AppOverlay } from './components/AppOverlay';
56
import { RouteLinks } from './router/components/RouteLinks';
67
import { ACCOUNT_MODAL_REF } from './constants';
78
import './index.scss';
89

910
const App: React.FC = () => {
11+
Modal.setAppElement(ACCOUNT_MODAL_REF);
1012
return (
1113
<APIProvider standalone>
1214
<AuthProvider>
13-
{/* This will be the used to bind modal in Accounts-v2 package*/}
14-
<div id={ACCOUNT_MODAL_REF.replace('#', '')} />
1515
<AppOverlay title='Settings'>
1616
<RouteLinks />
1717
</AppOverlay>
Loading

packages/account-v2/src/components/FormFields/FormDropDownField.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export const FormDropDownField = ({ handleSelect, name, validationSchema, ...res
3838
aria-label={rest.label}
3939
dropdownIcon={<LabelPairedChevronDownMdRegularIcon />}
4040
errorMessage={touched && error ? error : ''}
41-
isRequired={touched && !!error}
4241
onSearch={field.onChange}
4342
onSelect={
4443
handleSelect ? value => handleSelect(value as string) : value => form.setFieldValue(name, value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { useFormikContext } from 'formik';
3+
import { useHistory } from 'react-router-dom';
4+
import { Button, Modal, Text, useDevice } from '@deriv-com/ui';
5+
import UnsavedChanges from '../../assets/status-message/ic-unsaved-changes.svg';
6+
7+
type TLeaveConfirm = {
8+
onCancel?: () => void;
9+
onLeave?: () => void;
10+
};
11+
12+
export const LeaveConfirm = ({ onCancel, onLeave }: TLeaveConfirm) => {
13+
const history = useHistory();
14+
const { dirty } = useFormikContext();
15+
const { isMobile } = useDevice();
16+
17+
const [showPrompt, setShowPrompt] = useState(false);
18+
const [currentPath, setCurrentPath] = useState('');
19+
const unblock = useRef<ReturnType<ReturnType<typeof useHistory>['block']>>();
20+
21+
useEffect(() => {
22+
unblock.current = history.block((prompt: { pathname: React.SetStateAction<string> }) => {
23+
if (dirty) {
24+
setCurrentPath(prompt.pathname);
25+
setShowPrompt(true);
26+
}
27+
return !dirty;
28+
});
29+
return () => {
30+
unblock.current();
31+
};
32+
}, [dirty, history]);
33+
34+
const handleLeave = () => {
35+
onLeave?.();
36+
unblock.current();
37+
setShowPrompt(false);
38+
history.push(currentPath);
39+
};
40+
41+
const handleCancel = () => {
42+
onCancel?.();
43+
setShowPrompt(false);
44+
};
45+
46+
return (
47+
<Modal className='w-[440px]' isOpen={showPrompt}>
48+
<div className='grid justify-center w-full gap-20 p-48 lg:p-24 justify-items-center'>
49+
<UnsavedChanges />
50+
<div className='grid justify-center gap-10'>
51+
<Text align='center' size='md' weight='bold'>
52+
Unsaved Changes
53+
</Text>
54+
<div className='grid gap-4'>
55+
<Text align='center' size='sm'>
56+
You have unsaved changes. Are you sure you want to discard changes and leave this page?
57+
</Text>
58+
</div>
59+
</div>
60+
<div className='flex flex-col justify-center w-full gap-8 mt-24 lg:flex-row'>
61+
<Button isFullWidth={isMobile} onClick={handleCancel} type='button' variant='outlined'>
62+
Cancel
63+
</Button>
64+
<Button isFullWidth={isMobile} onClick={handleLeave} type='button'>
65+
Leave Settings
66+
</Button>
67+
</div>
68+
</div>
69+
</Modal>
70+
);
71+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import React from 'react';
2+
import { Formik } from 'formik';
3+
import { createBrowserHistory } from 'history';
4+
import { Router, useHistory } from 'react-router-dom';
5+
import { Modal } from '@deriv-com/ui';
6+
import { act, render, screen, waitFor } from '@testing-library/react';
7+
import userEvent from '@testing-library/user-event';
8+
import { LeaveConfirm } from '../LeaveConfirm';
9+
10+
jest.mock('@deriv-com/ui', () => ({
11+
...jest.requireActual('@deriv-com/ui'),
12+
useDevice: jest.fn(() => ({ isMobile: false })),
13+
}));
14+
15+
afterEach(() => {
16+
jest.clearAllMocks();
17+
});
18+
19+
let history: ReturnType<typeof useHistory>;
20+
21+
beforeEach(() => {
22+
history = createBrowserHistory();
23+
});
24+
25+
describe('LeaveConfirm', () => {
26+
const onCancelMock = jest.fn();
27+
const onLeaveMock = jest.fn();
28+
29+
const wrapper = ({ children }: { children: JSX.Element }) => {
30+
return (
31+
<Router history={history}>
32+
<Formik initialValues={{ name: '' }} onSubmit={jest.fn()}>
33+
{({ handleBlur, handleChange, handleSubmit, values }) => (
34+
<form onSubmit={handleSubmit}>
35+
{children}
36+
<input
37+
aria-label='name'
38+
name='name'
39+
onBlur={handleBlur}
40+
onChange={handleChange}
41+
value={values.name}
42+
/>
43+
</form>
44+
)}
45+
</Formik>
46+
</Router>
47+
);
48+
};
49+
50+
const renderComponent = () => {
51+
render(<LeaveConfirm onCancel={onCancelMock} onLeave={onLeaveMock} />, { wrapper });
52+
const inputField = screen.getByRole('textbox');
53+
userEvent.type(inputField, 'Hello');
54+
55+
act(() => {
56+
history.push('/something');
57+
});
58+
};
59+
60+
Modal.setAppElement('body');
61+
62+
const unsavedChangesText = 'Unsaved Changes';
63+
64+
it('should render the component', async () => {
65+
renderComponent();
66+
await waitFor(() => {
67+
expect(screen.getByText(unsavedChangesText)).toBeInTheDocument();
68+
expect(
69+
screen.getByText(
70+
'You have unsaved changes. Are you sure you want to discard changes and leave this page?'
71+
)
72+
).toBeInTheDocument();
73+
});
74+
});
75+
76+
it('should call onCancel when Cancel button is clicked', async () => {
77+
renderComponent();
78+
79+
const cancelButton = screen.getByLabelText('Cancel');
80+
userEvent.click(cancelButton);
81+
82+
await waitFor(() => {
83+
expect(screen.queryByText(unsavedChangesText)).not.toBeInTheDocument();
84+
expect(onCancelMock).toHaveBeenCalled();
85+
});
86+
});
87+
88+
it('should call onLeave when Leave Settings button is clicked', async () => {
89+
renderComponent();
90+
91+
const leaveButton = screen.getByLabelText('Leave Settings');
92+
userEvent.click(leaveButton);
93+
94+
await waitFor(() => {
95+
expect(screen.queryByText(unsavedChangesText)).not.toBeInTheDocument();
96+
expect(onLeaveMock).toHaveBeenCalled();
97+
});
98+
});
99+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { LeaveConfirm } from './LeaveConfirm';

packages/account-v2/src/containers/AccountClosureForm/AccountClosureConfirmModal.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const AccountClosureConfirmModal = ({
1313
handleSubmit,
1414
isModalOpen,
1515
}: TAccountClosureConfirmModalProps) => (
16-
<Modal className='p-24 md:w-[440px] sm:w-[312px] h-auto rounded-default' isOpen={isModalOpen}>
16+
<Modal className='p-24 md:w-[440px] sm:w-[312px]' isOpen={isModalOpen}>
1717
<Modal.Body className='flex flex-col'>
1818
<StandaloneTriangleExclamationRegularIcon className='self-center fill-status-light-danger' iconSize='2xl' />
1919
<Text align='center' as='h4' size='md' weight='bold'>
@@ -24,7 +24,7 @@ export const AccountClosureConfirmModal = ({
2424
as our legal obligations are met.
2525
</Text>
2626
</Modal.Body>
27-
<Modal.Footer className='mt-24 flex gap-x-16 justify-end' hideBorder>
27+
<Modal.Footer className='flex justify-end mt-24 gap-x-16' hideBorder>
2828
<Button color='black' onClick={handleCancel} rounded='sm' size='md' type='button' variant='outlined'>
2929
Go back
3030
</Button>

packages/account-v2/src/containers/AccountClosureForm/AccountClosureForm.tsx

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import React, { Fragment, useReducer, useRef } from 'react';
22
import { Field, Form, Formik, FormikProps } from 'formik';
3-
import { Button, Checkbox, Modal, Text, TextArea } from '@deriv-com/ui';
3+
import { Button, Checkbox, Text, TextArea } from '@deriv-com/ui';
44
import {
5-
ACCOUNT_MODAL_REF,
65
accountClosureReasons,
76
CHARACTER_LIMIT_FOR_CLOSING_ACCOUNT,
87
MAX_ALLOWED_REASONS_FOR_CLOSING_ACCOUNT,
@@ -17,7 +16,6 @@ import { AccountClosureConfirmModal } from './AccountClosureConfirmModal';
1716
import { AccountClosureSuccessModal } from './AccountClosureSuccessModal';
1817

1918
export const AccountClosureForm = ({ handleOnBack }: { handleOnBack: () => void }) => {
20-
Modal.setAppElement(ACCOUNT_MODAL_REF);
2119
const reasons = accountClosureReasons();
2220
const validationSchema = getAccountClosureValidationSchema();
2321

@@ -74,7 +72,7 @@ export const AccountClosureForm = ({ handleOnBack }: { handleOnBack: () => void
7472
{({ dirty, setFieldValue, values }) => (
7573
<Form>
7674
<section>
77-
<div className='gap-8 flex flex-col my-16'>
75+
<div className='flex flex-col gap-8 my-16'>
7876
{reasons.map(({ label, ref, value }) => (
7977
<Field
8078
as={Checkbox}
@@ -129,7 +127,7 @@ export const AccountClosureForm = ({ handleOnBack }: { handleOnBack: () => void
129127
textSize='sm'
130128
/>
131129
</section>
132-
<section className='mt-24 flex gap-x-16 justify-end'>
130+
<section className='flex justify-end mt-24 gap-x-16'>
133131
<Button
134132
color='black'
135133
onClick={handleOnBack}

packages/account-v2/src/containers/AccountClosureForm/AccountClosureSuccessModal.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type TAccountClosureSuccessModalProps = {
88

99
export const AccountClosureSuccessModal = ({ handleClose, isModalOpen }: TAccountClosureSuccessModalProps) => (
1010
<Modal
11-
className='p-24 md:w-[440px] sm:w-[312px] h-auto rounded-default'
11+
className='p-24 md:w-[440px] sm:w-[312px]'
1212
isOpen={isModalOpen}
1313
onRequestClose={handleClose}
1414
shouldCloseOnEsc
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import React from 'react';
22
import { Button, Modal, Text } from '@deriv-com/ui';
33
import TrashIcon from '../../assets/connectedApps/ic-account-trash-can.svg';
4-
import { ACCOUNT_MODAL_REF } from '../../constants';
54

65
type TConnectedAppsRevokeModalProps = {
76
handleRevokeAccess: () => void;
@@ -13,32 +12,22 @@ export const ConnectedAppsRevokeModal = ({
1312
handleRevokeAccess,
1413
handleToggleModal,
1514
isModalOpen,
16-
}: TConnectedAppsRevokeModalProps) => {
17-
Modal.setAppElement(ACCOUNT_MODAL_REF);
18-
return (
19-
<Modal className='p-24 md:w-[440px] sm:w-[328px] h-auto rounded-default' isOpen={isModalOpen}>
20-
<Modal.Body className='flex flex-col justify-center items-center'>
21-
{/* TODO: Replace this svg with trashIcon when quill-icons is updated */}
22-
<TrashIcon height={128} width={128} />
23-
<Text align='center' as='p' weight='bold'>
24-
Confirm revoke access
25-
</Text>
26-
</Modal.Body>
27-
<Modal.Footer className='mt-24 p-0 min-h-0 flex gap-x-8 justify-center' hideBorder>
28-
<Button
29-
color='black'
30-
onClick={handleToggleModal}
31-
rounded='sm'
32-
size='lg'
33-
type='button'
34-
variant='outlined'
35-
>
36-
Back
37-
</Button>
38-
<Button color='primary' onClick={handleRevokeAccess} rounded='sm' size='lg'>
39-
Confirm
40-
</Button>
41-
</Modal.Footer>
42-
</Modal>
43-
);
44-
};
15+
}: TConnectedAppsRevokeModalProps) => (
16+
<Modal className='p-24 md:w-[440px] sm:w-[328px]' isOpen={isModalOpen}>
17+
<Modal.Body className='flex flex-col items-center justify-center'>
18+
{/* TODO: Replace this svg with trashIcon when quill-icons is updated */}
19+
<TrashIcon height={128} width={128} />
20+
<Text align='center' as='p' weight='bold'>
21+
Confirm revoke access
22+
</Text>
23+
</Modal.Body>
24+
<Modal.Footer className='flex justify-center min-h-0 p-0 mt-24 gap-x-8' hideBorder>
25+
<Button color='black' onClick={handleToggleModal} rounded='sm' size='lg' type='button' variant='outlined'>
26+
Back
27+
</Button>
28+
<Button color='primary' onClick={handleRevokeAccess} rounded='sm' size='lg'>
29+
Confirm
30+
</Button>
31+
</Modal.Footer>
32+
</Modal>
33+
);

packages/account-v2/src/containers/POAForm/AddressDetailsForm.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom';
44
import { useDocumentUpload, useInvalidateQuery, useSettings } from '@deriv/api-v2';
55
import { DerivLightIcPoaLockIcon, StandaloneXmarkBoldIcon } from '@deriv/quill-icons';
66
import { Button, InlineMessage, Text, useDevice } from '@deriv-com/ui';
7+
import { LeaveConfirm } from 'src/components/LeaveConfirm';
78
import { IconWithMessage } from '../../components/IconWithMessage';
89
import { ACCOUNT_V2_DEFAULT_ROUTE } from '../../constants/routes';
910
import { AddressFields } from '../../modules/src/AddressFields';
@@ -101,6 +102,7 @@ export const AddressDetailsForm = ({ resubmitting }: TAddressDetailsForm) => {
101102
<Formik enableReinitialize initialValues={initialValues} onSubmit={handleFormSubmit}>
102103
{({ dirty, isSubmitting, isValid, status }) => (
103104
<Form>
105+
<LeaveConfirm />
104106
<div className='flex flex-col w-full min-h-screen space-y-16 lg:w-auto'>
105107
{(updateError || status || resubmitting) && (
106108
<InlineMessage type='filled' variant='error'>

packages/account-v2/src/containers/POAForm/__tests__/AddressDetailsForm.spec.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ jest.mock('../DocumentSubmission', () => ({
1414
DocumentSubmission: () => <div>DocumentSubmission</div>,
1515
}));
1616

17+
jest.mock('src/components/LeaveConfirm', () => ({
18+
LeaveConfirm: () => <div>LeaveConfirm</div>,
19+
}));
20+
1721
const updateSettings = jest.fn();
1822
const mockUploadDocument = jest.fn();
1923
const mockDefaultSettings = {

packages/account-v2/src/containers/PersonalDetailsForm/PersonalDetailsForm.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { Fragment } from 'react';
22
import { Form, Formik } from 'formik';
33
import { Checkbox, Text } from '@deriv-com/ui';
4+
import { LeaveConfirm } from 'src/components/LeaveConfirm';
45
import { FormSubHeader } from '../../components/FormSubHeader';
56
import { usePersonalDetails } from '../../hooks/usePersonalDetails';
67
import { AddressFields } from '../../modules/src/AddressFields';
@@ -25,6 +26,7 @@ export const PersonalDetailsForm = () => {
2526
validationSchema={validationSchema}
2627
>
2728
<Form>
29+
<LeaveConfirm />
2830
<FormSubHeader>Details</FormSubHeader>
2931
<PersonalDetails />
3032
<FormSubHeader>Tax information</FormSubHeader>

packages/account-v2/src/containers/PersonalDetailsForm/__tests__/PersonalDetailsForm.spec.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ jest.mock('../../../hooks/usePersonalDetails', () => ({
3030
usePersonalDetails: jest.fn(),
3131
}));
3232

33+
jest.mock('src/components/LeaveConfirm', () => ({
34+
LeaveConfirm: () => <div>LeaveConfirm</div>,
35+
}));
36+
3337
beforeEach(() => {
3438
(usePersonalDetails as jest.Mock).mockReturnValue({
3539
data: { isSupportProfessionalClient: true, isVirtual: false },

0 commit comments

Comments
 (0)