Skip to content

Commit

Permalink
Nada/feq 2188/order details confirm modal (#63)
Browse files Browse the repository at this point in the history
* feat: integrate order details confirm modal

* fix: pr review comments

* fix: destructure props
  • Loading branch information
nada-deriv authored May 13, 2024
1 parent 5a4125f commit 321a564
Show file tree
Hide file tree
Showing 16 changed files with 189 additions and 90 deletions.
5 changes: 4 additions & 1 deletion src/components/BuySellForm/BuySellAmount/BuySellAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type TBuySellAmountProps = {
maxLimit: string;
minLimit: string;
paymentMethodNames?: string[];
setValue: ReturnType<typeof useForm>['setValue'];
};
const BuySellAmount = ({
accountCurrency,
Expand All @@ -31,6 +32,7 @@ const BuySellAmount = ({
maxLimit,
minLimit,
paymentMethodNames,
setValue,
}: TBuySellAmountProps) => {
const { isMobile } = useDevice();
const labelSize = isMobile ? 'sm' : 'xs';
Expand All @@ -49,7 +51,8 @@ const BuySellAmount = ({
// causing the amount to be 0
useEffect(() => {
setInputValue(minLimit);
}, [minLimit]);
setValue('amount', minLimit);
}, [minLimit, setValue]);

return (
<div className='flex flex-col gap-[2rem] py-[1.6rem]'>
Expand Down
2 changes: 2 additions & 0 deletions src/components/BuySellForm/BuySellForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ const BuySellForm = ({ advertId, isModalOpen, onRequestClose }: TBuySellFormProp
formState: { isValid },
getValues,
handleSubmit,
setValue,
} = useForm({
defaultValues: {
amount: min_order_amount_limit ?? 1,
Expand Down Expand Up @@ -254,6 +255,7 @@ const BuySellForm = ({ advertId, isModalOpen, onRequestClose }: TBuySellFormProp
)}
minLimit={min_order_amount_limit_display ?? '0'}
paymentMethodNames={payment_method_names}
setValue={setValue as unknown as (name: string, value: string) => void}
/>
</BuySellFormDisplayWrapper>
</form>
Expand Down
1 change: 1 addition & 0 deletions src/components/BuySellForm/__tests__/BuySellForm.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ jest.mock('react-hook-form', () => ({
amount: 1,
})),
handleSubmit: mockHandleSubmit,
setValue: jest.fn(),
}),
}));

Expand Down
8 changes: 6 additions & 2 deletions src/components/FileDropzone/FileDropzone.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
@mixin file-dropzone-message {
display: block;
display: flex;
flex-direction: column;
align-items: center;
max-width: 16.8rem;
opacity: 1;
pointer-events: none;
position: absolute;
transform: translate3d(0, 0, 0);
transition: transform 0.25s ease, opacity 0.15s linear;
transition:
transform 0.25s ease,
opacity 0.15s linear;

@include mobile {
max-width: 26rem;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { useState } from 'react';
import { FileRejection } from 'react-dropzone';
import { FileUploaderComponent } from '@/components/FileUploaderComponent';
import { useOrderDetails } from '@/providers/OrderDetailsProvider';
import { getErrorMessage, maxPotFileSize, TFile } from '@/utils';
import { Button, InlineMessage, Modal, Text, useDevice } from '@deriv-com/ui';
import './OrderDetailsConfirmModal.scss';

type TOrderDetailsConfirmModalProps = {
isModalOpen: boolean;
onCancel: () => void;
onConfirm: () => void;
onRequestClose: () => void;
sendFile: (file: File) => void;
};

type TDocumentFile = {
errorMessage: string | null;
files: File[];
};

const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetailsConfirmModalProps) => {
const OrderDetailsConfirmModal = ({
isModalOpen,
onCancel,
onConfirm,
onRequestClose,
sendFile,
}: TOrderDetailsConfirmModalProps) => {
const [documentFile, setDocumentFile] = useState<TDocumentFile>({ errorMessage: null, files: [] });
const { orderDetails } = useOrderDetails();
const { displayPaymentAmount, local_currency: localCurrency, otherUserDetails } = orderDetails ?? {};
const { name } = otherUserDetails ?? {};
const { isMobile } = useDevice();
const buttonTextSize = isMobile ? 'md' : 'sm';

Expand All @@ -37,11 +50,6 @@ const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetails
});
};

// TODO: uncomment this when implementing the OrderDetailsConfirmModal
// const displayPaymentAmount = removeTrailingZeros(
// formatMoney(local_currency, amount_display * Number(roundOffDecimal(rate, setDecimalPlaces(rate, 6))), true)
// );

return (
<Modal
ariaHideApp={false}
Expand All @@ -59,8 +67,8 @@ const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetails
</Modal.Header>
<Modal.Body className='flex flex-col lg:px-[2.4rem] px-[1.6rem]'>
<Text size='sm'>
Please make sure that you’ve paid 9.99 IDR to client CR90000012, and upload the receipt as proof of
your payment
{`Please make sure that you’ve paid ${displayPaymentAmount} ${localCurrency} to client ${name}, and upload the receipt as proof of
your payment`}
</Text>
<Text className='pt-[0.8rem] pb-[2.4rem]' color='less-prominent' size='sm'>
We accept JPG, PDF, or PNG (up to 5MB).
Expand All @@ -72,8 +80,10 @@ const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetails
</InlineMessage>
<FileUploaderComponent
accept={{
'application/*': ['.pdf'],
'image/*': ['.jpg', '.jpeg', './png'],
'application/pdf': ['.pdf'],
'image/jpeg': ['.jpeg'],
'image/jpg': ['.jpg'],
'image/png': ['.png'],
}}
hoverMessage='Upload receipt here'
maxSize={maxPotFileSize}
Expand All @@ -86,12 +96,18 @@ const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetails
/>
</Modal.Body>
<Modal.Footer className='gap-4 border-none lg:p-[2.4rem] p-[1.6rem]'>
<Button className='border-2' color='black' size='lg' variant='outlined'>
<Button className='border-2' color='black' onClick={onCancel} size='lg' variant='outlined'>
<Text lineHeight='6xl' size={buttonTextSize} weight='bold'>
Go Back
</Text>
</Button>
<Button size='lg'>
<Button
onClick={() => {
sendFile(documentFile.files[0]);
onConfirm();
}}
size='lg'
>
<Text lineHeight='6xl' size={buttonTextSize} weight='bold'>
Confirm
</Text>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import OrderDetailsConfirmModal from '../OrderDetailsConfirmModal';

Expand All @@ -9,9 +9,22 @@ jest.mock('@deriv-com/ui', () => ({
}),
}));

jest.mock('@/providers/OrderDetailsProvider', () => ({
useOrderDetails: jest.fn().mockReturnValue({
orderDetails: {
displayPaymentAmount: '0.10',
local_currency: 'INR',
otherUserDetails: { name: 'John Doe' },
},
}),
}));

const mockProps = {
isModalOpen: true,
onCancel: jest.fn(),
onConfirm: jest.fn(),
onRequestClose: jest.fn(),
sendFile: jest.fn(),
};

describe('<OrderDetailsConfirmModal />', () => {
Expand All @@ -21,7 +34,7 @@ describe('<OrderDetailsConfirmModal />', () => {
expect(screen.getByText('Payment confirmation')).toBeInTheDocument();
expect(
screen.getByText(
/Please make sure that you’ve paid 9.99 IDR to client CR90000012, and upload the receipt as proof of your payment/
/Please make sure that you’ve paid 0.10 INR to client John Doe, and upload the receipt as proof of your payment/
)
).toBeInTheDocument();
expect(screen.getByText('We accept JPG, PDF, or PNG (up to 5MB).')).toBeInTheDocument();
Expand Down Expand Up @@ -67,10 +80,7 @@ describe('<OrderDetailsConfirmModal />', () => {
value: fileList,
});

// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {
fireEvent.change(fileInput);
});
fireEvent.change(fileInput);

await waitFor(() => {
expect(screen.getByText('The file you uploaded is not supported. Upload another.')).toBeInTheDocument();
Expand All @@ -82,22 +92,7 @@ describe('<OrderDetailsConfirmModal />', () => {

const blob = new Blob([new Array(6 * 1024 * 1024).join('a')], { type: 'image/png' });
const file = new File([blob], 'test.png');
const fileInput = screen.getByTestId('dt_file_upload_input');

const fileList = {
0: file,
item: () => file,
length: 1,
};

Object.defineProperty(fileInput, 'files', {
value: fileList,
});

// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {
fireEvent.change(fileInput);
});
await userEvent.upload(screen.getByTestId('dt_file_upload_input'), file);

expect(screen.getByText('Cannot upload a file over 5MB')).toBeInTheDocument();
});
Expand All @@ -108,19 +103,10 @@ describe('<OrderDetailsConfirmModal />', () => {
const file = new File(['test'], 'test.png', { type: 'image/png' });
const fileInput = screen.getByTestId('dt_file_upload_input');

const fileList = {
0: file,
item: () => file,
length: 1,
};

Object.defineProperty(fileInput, 'files', {
value: fileList,
});
await userEvent.upload(fileInput, file);

// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {
fireEvent.change(fileInput);
await waitFor(() => {
expect(screen.getByText('test.png')).toBeInTheDocument();
});

const closeIcon = screen.getByTestId('dt_remove_file_icon');
Expand All @@ -131,4 +117,22 @@ describe('<OrderDetailsConfirmModal />', () => {
expect(screen.queryByText('test.png')).not.toBeInTheDocument();
});
});

it('should handle confirm button click', async () => {
render(<OrderDetailsConfirmModal {...mockProps} />);

const button = screen.getByRole('button', { name: 'Confirm' });
await userEvent.click(button);

expect(mockProps.onConfirm).toHaveBeenCalled();
});

it('should handle goback click', async () => {
render(<OrderDetailsConfirmModal {...mockProps} />);

const button = screen.getByRole('button', { name: 'Go Back' });
await userEvent.click(button);

expect(mockProps.onCancel).toHaveBeenCalled();
});
});
4 changes: 4 additions & 0 deletions src/constants/api-error-codes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ export const ERROR_CODES = {
ADVERTISER_NOT_FOUND: 'AdvertiserNotFound',
ADVERTISER_TEMP_BAN: 'advertiser_temp_ban',
DUPLICATE_ADVERT: 'DuplicateAdvert',
EXCESSIVE_VERIFICATION_FAILURES: 'ExcessiveVerificationFailures',
EXCESSIVE_VERIFICATION_REQUESTS: 'ExcessiveVerificationRequests',
INVALID_VERIFICATION_TOKEN: 'InvalidVerificationToken',
ORDER_EMAIL_VERIFICATION_REQUIRED: 'OrderEmailVerificationRequired',
} as const;
17 changes: 8 additions & 9 deletions src/hooks/custom-hooks/useSendbird.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { TFileType } from 'types';
import { useOrderDetails } from '@/providers/OrderDetailsProvider';
import SendbirdChat, { BaseChannel, User } from '@sendbird/chat';
import { GroupChannel, GroupChannelHandler, GroupChannelModule } from '@sendbird/chat/groupChannel';
import { BaseMessage, MessageType, MessageTypeFilter } from '@sendbird/chat/message';
Expand Down Expand Up @@ -72,7 +71,7 @@ function createChatMessage(sendbirdMessage: BaseMessage): ChatMessage {
};
}

const useSendbird = (orderId: string) => {
const useSendbird = (orderId: string, isErrorOrderInfo: boolean, chatChannelUrl: string) => {
const sendbirdApiRef = useRef<ReturnType<typeof SendbirdChat.init<GroupChannelModule[]>>>();

const [isChatLoading, setIsChatLoading] = useState(false);
Expand All @@ -91,7 +90,6 @@ const useSendbird = (orderId: string) => {
const { data: advertiserInfo } = api.advertiser.useGetInfo();
//TODO: p2p_chat_create endpoint to be removed once chat_channel_url is created from p2p_order_create
const { isError: isErrorChatCreate, mutate: createChat } = api.chat.useCreate();
const { isErrorOrderInfo, orderDetails } = useOrderDetails();
const { data: serverTime, isError: isErrorServerTime } = api.account.useServerTime();

const getUser = async (userId: string, token: string) => {
Expand Down Expand Up @@ -236,11 +234,11 @@ const useSendbird = (orderId: string) => {
const user = await getUser(advertiserInfo.chat_user_id, token || '');
if (!user) {
setIsChatError(true);
} else if (orderDetails?.chat_channel_url) {
} else if (chatChannelUrl) {
setUser(user);
// if there is no chat_channel_url, it needs to be created using useCreateChat hook first
// 2. Retrieve the P2P channel for the specific order
const channel = await getChannel(orderDetails.chat_channel_url);
const channel = await getChannel(chatChannelUrl);
if (!channel) {
setIsChatError(true);
} else {
Expand All @@ -260,7 +258,7 @@ const useSendbird = (orderId: string) => {
isSuccessSendbirdServiceToken,
sendbirdServiceToken,
advertiserInfo?.chat_user_id,
orderDetails?.chat_channel_url,
chatChannelUrl,
getMessages,
]);

Expand All @@ -271,14 +269,15 @@ const useSendbird = (orderId: string) => {

useEffect(() => {
// if the user has not created a chat URL for the order yet, create one using p2p_create_chat endpoint
if (!orderDetails?.chat_channel_url) {
if (orderId && !chatChannelUrl) {
createChat({
order_id: orderId,
});
} else {
} else if (sendbirdServiceToken?.app_id) {
initialiseChat();
}
}, [orderId, orderDetails?.chat_channel_url]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [orderId, chatChannelUrl, sendbirdServiceToken?.app_id]);

return {
activeChatChannel: chatChannel,
Expand Down
5 changes: 1 addition & 4 deletions src/pages/orders/components/ChatMessages/ChatMessages.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import { Fragment, SyntheticEvent, useEffect, useRef } from 'react';
import clsx from 'clsx';
import { TTextColors } from 'types';
import { TActiveChannel, TChatMessages, TTextColors } from 'types';
import { CHAT_FILE_TYPE, CHAT_MESSAGE_TYPE } from '@/constants';
import { useSendbird } from '@/hooks/custom-hooks';
import { convertToMB, formatMilliseconds } from '@/utils';
import { Text, useDevice } from '@deriv-com/ui';
import { ReactComponent as PDFIcon } from '../../../../public/ic-pdf.svg';
import { ChatMessageReceipt } from '../ChatMessageReceipt';
import { ChatMessageText } from '../ChatMessageText';
import './ChatMessages.scss';

type TActiveChannel = ReturnType<typeof useSendbird>['activeChatChannel'];
type TChatMessages = NonNullable<ReturnType<typeof useSendbird>['messages']>;
type TChatMessagesProps = {
chatChannel: TActiveChannel;
chatMessages: TChatMessages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OrderDetailsCardInfo } from './OrderDetailsCardInfo';
import { OrderDetailsCardReview } from './OrderDetailsCardReview';
import './OrderDetailsCard.scss';

const OrderDetailsCard = () => {
const OrderDetailsCard = ({ sendFile }: { sendFile: (file: File) => void }) => {
const { isDesktop } = useDevice();

return (
Expand All @@ -16,7 +16,7 @@ const OrderDetailsCard = () => {
<OrderDetailsCardInfo />
<LightDivider />
<OrderDetailsCardReview />
{isDesktop && <OrderDetailsCardFooter />}
{isDesktop && <OrderDetailsCardFooter sendFile={sendFile} />}
</div>
);
};
Expand Down
Loading

0 comments on commit 321a564

Please sign in to comment.