Skip to content

Commit

Permalink
Ameerul / FEQ-2189 integrate email verification flow (#69)
Browse files Browse the repository at this point in the history
* feat: added email verification flow up until before rating modal

* chore: added mixin for modals

* chore: added RatingModals in order details, table row, moved order review to order footer

* fix: style issues with order review footer and routing issue for orders page

* fix: go back to past orders if user views order details then rating modal then goes back

* chore: added Localize for texts

* fix: failing test cases for Order flows

* fix: remaining failing test cases

* chore: added suggestions

* Update src/components/Modals/EmailLinkBlockedModal/EmailLinkBlockedModal.tsx

Co-authored-by: Hasan Mobarak <[email protected]>

* Update src/components/Modals/InvalidVerificationLinkModal/InvalidVerificationLinkModal.tsx

Co-authored-by: Hasan Mobarak <[email protected]>

---------

Co-authored-by: Hasan Mobarak <[email protected]>
  • Loading branch information
ameerul-deriv and hasan-deriv authored May 17, 2024
1 parent 95f875f commit 948532a
Show file tree
Hide file tree
Showing 44 changed files with 1,045 additions and 174 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.email-link-blocked-modal {
@include default-modal;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { DerivLightIcEmailVerificationLinkBlockedIcon } from '@deriv/quill-icons';
import { Localize } from '@deriv-com/translations';
import { Modal, Text, useDevice } from '@deriv-com/ui';
import './EmailLinkBlockedModal.scss';

type TEmailLinkBlockedModalProps = {
errorMessage?: string;
isModalOpen: boolean;
onRequestClose: () => void;
};

const EmailLinkBlockedModal = ({ errorMessage, isModalOpen, onRequestClose }: TEmailLinkBlockedModalProps) => {
const { isMobile } = useDevice();
const iconSize = isMobile ? 96 : 128;

return (
<Modal
ariaHideApp={false}
className='email-link-blocked-modal'
isOpen={isModalOpen}
onRequestClose={onRequestClose}
>
<Modal.Header hideBorder onRequestClose={onRequestClose} />
<Modal.Body className='flex flex-col items-center lg:gap-[2.4rem] gap-8 p-[2.4rem] lg:pt-4 pt-0'>
<DerivLightIcEmailVerificationLinkBlockedIcon height={iconSize} width={iconSize} />
<Text align='center' weight='bold'>
<Localize i18n_default_text='Too many failed attempts' />
</Text>
{errorMessage && <Text align='center'>{errorMessage}</Text>}
</Modal.Body>
</Modal>
);
};

export default EmailLinkBlockedModal;
1 change: 1 addition & 0 deletions src/components/Modals/EmailLinkBlockedModal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as EmailLinkBlockedModal } from './EmailLinkBlockedModal';
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
.email-link-verified-modal {
height: auto;
width: 44rem;
border-radius: 8px;

@include mobile {
max-width: calc(100vw - 3.2rem);
}
@include default-modal;
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import { useOrderDetails } from '@/providers/OrderDetailsProvider';
import { DerivLightIcEmailVerificationLinkValidIcon } from '@deriv/quill-icons';
import { Localize } from '@deriv-com/translations';
import { Button, Modal, Text } from '@deriv-com/ui';
import './EmailLinkVerifiedModal.scss';

type TEmailLinkVerifiedModal = {
isModalOpen: boolean;
onRequestClose: () => void;
onSubmit: () => void;
};

// TODO: replace value, currency and username with actual values when implementing function
const EmailLinkVerifiedModal = ({ isModalOpen, onRequestClose }: TEmailLinkVerifiedModal) => {
const EmailLinkVerifiedModal = ({ isModalOpen, onRequestClose, onSubmit }: TEmailLinkVerifiedModal) => {
const { orderDetails } = useOrderDetails();

return (
<Modal ariaHideApp={false} className='email-link-verified-modal' isOpen={isModalOpen}>
<Modal.Header hideBorder onRequestClose={onRequestClose} />
<Modal.Body className='flex flex-col items-center gap-[2.4rem] px-[2.4rem] pt-[2.4rem]'>
<DerivLightIcEmailVerificationLinkValidIcon height={128} width={128} />
<Text align='center' weight='bold'>
One last step before we close this order
<Localize i18n_default_text='One last step before we close this order' />
</Text>
<Text align='center'>
If you’ve received 100 USD from Test in your bank account or e-wallet, hit the button below to
complete the order.
<Localize
i18n_default_text='If you’ve received {{amount}} {{currency}} from
{{name}} in your bank account or e-wallet, hit the button below to
complete the order.'
values={{
amount: orderDetails.amount,
currency: orderDetails.local_currency,
name: orderDetails.advertiser_details.name,
}}
/>
</Text>
</Modal.Body>
<Modal.Footer className='justify-center mt-4' hideBorder>
<Button
onClick={() => {
// add function here when implementing this modal
}}
size='md'
>
Confirm
<Modal.Footer className='justify-center lg:mt-4 mt-0' hideBorder>
<Button onClick={onSubmit} size='md'>
<Localize i18n_default_text='Confirm' />
</Button>
</Modal.Footer>
</Modal>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import { render, screen } from '@testing-library/react';
import EmailLinkVerifiedModal from '../EmailLinkVerifiedModal';

jest.mock('@/providers/OrderDetailsProvider', () => ({
useOrderDetails: jest.fn().mockReturnValue({
orderDetails: {
advertiser_details: {
name: 'Test',
},
amount: 100,
local_currency: 'USD',
},
}),
}));

describe('<EmailLinkVerifiedModal />', () => {
it('it should render the EmailLinkVerifiedModal', () => {
render(<EmailLinkVerifiedModal isModalOpen onRequestClose={jest.fn()} />);
render(<EmailLinkVerifiedModal isModalOpen onRequestClose={jest.fn()} onSubmit={jest.fn()} />);

expect(screen.getByText('One last step before we close this order')).toBeInTheDocument();
expect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
.email-verification-modal {
height: auto;
width: 44rem;
border-radius: 8px;

@include mobile {
max-width: calc(100vw - 3.2rem);
}
@include default-modal;

// TODO: Remove this when Button allows to pass prop to prevent hover styles
&__button {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,78 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useCountdown } from 'usehooks-ts';
import {
DerivLightIcEmailSentIcon,
DerivLightIcFirewallEmailPasskeyIcon,
DerivLightIcSpamEmailPasskeyIcon,
DerivLightIcTypoEmailPasskeyIcon,
DerivLightIcWrongEmailPasskeyIcon,
} from '@deriv/quill-icons';
import { Localize } from '@deriv-com/translations';
import { Button, Modal, Text, useDevice } from '@deriv-com/ui';
import './EmailVerificationModal.scss';

const reasons = [
{
icon: DerivLightIcSpamEmailPasskeyIcon,
text: 'The email is in your spam folder (sometimes things get lost there).',
text: <Localize i18n_default_text='The email is in your spam folder (sometimes things get lost there).' />,
},
{
icon: DerivLightIcWrongEmailPasskeyIcon,
text: 'You accidentally gave us another email address (usually a work or a personal one instead of the one you meant).',
text: (
<Localize i18n_default_text='You accidentally gave us another email address (usually a work or a personal one instead of the one you meant).' />
),
},
{
icon: DerivLightIcTypoEmailPasskeyIcon,
text: 'The email address you entered had a mistake or typo (happens to the best of us).',
text: (
<Localize i18n_default_text='The email address you entered had a mistake or typo (happens to the best of us).' />
),
},
{
icon: DerivLightIcFirewallEmailPasskeyIcon,
text: 'We can’t deliver the email to this address (usually because of firewalls or filtering).',
text: (
<Localize i18n_default_text='We can’t deliver the email to this address (usually because of firewalls or filtering).' />
),
},
];

type TEmailVerificationModalProps = {
isModalOpen: boolean;
nextRequestTime: number;
onRequestClose: () => void;
onResendEmail: () => void;
};

const EmailVerificationModal = ({ isModalOpen, onRequestClose }: TEmailVerificationModalProps) => {
const EmailVerificationModal = ({
isModalOpen,
nextRequestTime,
onRequestClose,
onResendEmail,
}: TEmailVerificationModalProps) => {
const [shouldShowReasons, setShouldShowReasons] = useState<boolean>(false);
const { isMobile } = useDevice();
const emailIconSize = isMobile ? 100 : 128;
const reasonIconSize = isMobile ? 32 : 36;

const timeNow = Date.now() / 1000;

const [timeLeft, setTimeLeft] = useState<number>(Math.round(nextRequestTime - timeNow));

const [, { startCountdown }] = useCountdown({
countStart: timeLeft,
intervalMs: 1000,
});

useEffect(() => {
if (timeLeft > 0) {
startCountdown();
}
}, [startCountdown, timeLeft]);

useEffect(() => {
setTimeLeft(Math.round(nextRequestTime - timeNow));
}, [nextRequestTime, timeNow]);

return (
<Modal
ariaHideApp={false}
Expand All @@ -50,32 +84,42 @@ const EmailVerificationModal = ({ isModalOpen, onRequestClose }: TEmailVerificat
<Modal.Body className='flex flex-col items-center justify-center lg:gap-[2.4rem] gap-8 lg:px-10 lg:pb-10 p-8 pt-0'>
<DerivLightIcEmailSentIcon height={emailIconSize} width={emailIconSize} />
<Text align='center' weight='bold'>
Has the buyer paid you?
<Localize i18n_default_text='Has the buyer paid you?' />
</Text>
<Text align='center' size={isMobile ? 'sm' : 'md'}>
Releasing funds before receiving payment may result in losses. Check your email and follow the
instructions <strong>within 10 minutes</strong> to release the funds.
<Localize
components={[<strong key={0} />]}
i18n_default_text='Releasing funds before receiving payment may result in losses. Check your email and follow the instructions <0>within 10 minutes</0> to release the funds.'
/>
</Text>
<Button
className='email-verification-modal__button'
onClick={() => setShouldShowReasons(true)}
variant='ghost'
>
I didn’t receive the email
<Localize i18n_default_text='I didn’t receive the email' />
</Button>
{shouldShowReasons && (
<div className='flex flex-col w-full gap-8'>
{reasons.map(reason => (
<div className='grid grid-cols-[11%_89%] gap-4 items-center' key={reason.text}>
<div className='grid grid-cols-[11%_89%] gap-4 items-center' key={reason.icon.toString()}>
<reason.icon height={reasonIconSize} width={reasonIconSize} />
<Text size='xs'>{reason.text}</Text>
</div>
))}
<div className='flex justify-center'>
{/* TODO: Replace 59s with epoch value (verification_next_request) from BE response
* and disable the button if the epoch value is not reached yet
*/}
<Button size='md'>Resend email 59s</Button>
<Button disabled={timeLeft > 0} onClick={onResendEmail} size='md'>
{timeLeft > 0 ? (
<Localize
i18n_default_text='Resend email {{timeLeft}}s'
values={{
timeLeft,
}}
/>
) : (
<Localize i18n_default_text='Resend email' />
)}
</Button>
</div>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ jest.mock('@deriv-com/ui', () => ({

const mockProps = {
isModalOpen: true,
nextRequestTime: 10,
onRequestClose: jest.fn(),
onResendEmail: jest.fn(),
};

describe('<EmailVerificationModal />', () => {
Expand All @@ -22,7 +24,7 @@ describe('<EmailVerificationModal />', () => {
/Releasing funds before receiving payment may result in losses. Check your email and follow the instructions/
)
).toBeInTheDocument();
expect(screen.getByText('within 10 minutes', { selector: 'strong' })).toBeInTheDocument();
expect(screen.getByText(/within 10 minutes/i)).toBeInTheDocument();
expect(screen.getByText(/to release the funds./)).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'I didn’t receive the email' })).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.invalid-verification-link-modal {
@include default-modal;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import clsx from 'clsx';
import { ERROR_CODES } from '@/constants';
import {
DerivLightIcEmailVerificationLinkBlockedIcon,
DerivLightIcEmailVerificationLinkInvalidIcon,
} from '@deriv/quill-icons';
import { Localize } from '@deriv-com/translations';
import { Button, Modal, Text, useDevice } from '@deriv-com/ui';
import './InvalidVerificationLinkModal.scss';

type TInvalidVerificationLinkModalProps = {
error?: {
code: string;
message: string;
};
isModalOpen: boolean;
mutate: () => void;
onRequestClose: () => void;
};

const InvalidVerificationLinkModal = ({
error,
isModalOpen,
mutate,
onRequestClose,
}: TInvalidVerificationLinkModalProps) => {
const { isMobile } = useDevice();
const iconSize = isMobile ? 96 : 128;
const isInvalidVerification = error?.code === ERROR_CODES.INVALID_VERIFICATION_TOKEN;
const isExcessiveErrorMobile = !isInvalidVerification && isMobile;

return (
<Modal
ariaHideApp={false}
className='invalid-verification-link-modal'
isOpen={isModalOpen}
onRequestClose={onRequestClose}
>
<Modal.Header hideBorder onRequestClose={onRequestClose} />
<Modal.Body
className={clsx('flex flex-col items-center gap-[2.4rem] p-[2.4rem] pb-4', {
'py-0 px-[1.4rem] gap-[1.4rem]': isExcessiveErrorMobile,
})}
>
{isInvalidVerification ? (
<DerivLightIcEmailVerificationLinkInvalidIcon height={iconSize} width={iconSize} />
) : (
<DerivLightIcEmailVerificationLinkBlockedIcon height={iconSize} width={iconSize} />
)}
{isInvalidVerification && (
<Text weight='bold'>
<Localize i18n_default_text='Invalid verification link' />
</Text>
)}
{error?.message && <Text align='center' weight={isInvalidVerification ? 'normal' : 'bold'}>
{error.message}
</Text>}
</Modal.Body>
<Modal.Footer
className={clsx('justify-center', {
'pt-2 min-h-fit': isExcessiveErrorMobile,
})}
hideBorder
>
<Button
onClick={() => (isInvalidVerification ? mutate() : onRequestClose())}
size={isMobile ? 'md' : 'lg'}
textSize='sm'
>
{isInvalidVerification ? (
<Localize i18n_default_text='Get new link' />
) : (
<Localize i18n_default_text='OK' />
)}
</Button>
</Modal.Footer>
</Modal>
);
};

export default InvalidVerificationLinkModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as InvalidVerificationLinkModal } from './InvalidVerificationLinkModal';
Loading

0 comments on commit 948532a

Please sign in to comment.