diff --git a/src/components/BuySellForm/BuySellAmount/BuySellAmount.tsx b/src/components/BuySellForm/BuySellAmount/BuySellAmount.tsx index ca0cb887..209e0eb8 100644 --- a/src/components/BuySellForm/BuySellAmount/BuySellAmount.tsx +++ b/src/components/BuySellForm/BuySellAmount/BuySellAmount.tsx @@ -19,6 +19,7 @@ type TBuySellAmountProps = { maxLimit: string; minLimit: string; paymentMethodNames?: string[]; + setValue: ReturnType['setValue']; }; const BuySellAmount = ({ accountCurrency, @@ -31,6 +32,7 @@ const BuySellAmount = ({ maxLimit, minLimit, paymentMethodNames, + setValue, }: TBuySellAmountProps) => { const { isMobile } = useDevice(); const labelSize = isMobile ? 'sm' : 'xs'; @@ -49,7 +51,8 @@ const BuySellAmount = ({ // causing the amount to be 0 useEffect(() => { setInputValue(minLimit); - }, [minLimit]); + setValue('amount', minLimit); + }, [minLimit, setValue]); return (
diff --git a/src/components/BuySellForm/BuySellForm.tsx b/src/components/BuySellForm/BuySellForm.tsx index 7a471bd8..f4439c91 100644 --- a/src/components/BuySellForm/BuySellForm.tsx +++ b/src/components/BuySellForm/BuySellForm.tsx @@ -141,6 +141,7 @@ const BuySellForm = ({ advertId, isModalOpen, onRequestClose }: TBuySellFormProp formState: { isValid }, getValues, handleSubmit, + setValue, } = useForm({ defaultValues: { amount: min_order_amount_limit ?? 1, @@ -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} /> diff --git a/src/components/BuySellForm/__tests__/BuySellForm.spec.tsx b/src/components/BuySellForm/__tests__/BuySellForm.spec.tsx index ac39a18e..4f878bf8 100644 --- a/src/components/BuySellForm/__tests__/BuySellForm.spec.tsx +++ b/src/components/BuySellForm/__tests__/BuySellForm.spec.tsx @@ -153,6 +153,7 @@ jest.mock('react-hook-form', () => ({ amount: 1, })), handleSubmit: mockHandleSubmit, + setValue: jest.fn(), }), })); diff --git a/src/components/FileDropzone/FileDropzone.scss b/src/components/FileDropzone/FileDropzone.scss index 6e01ba67..adb3838a 100644 --- a/src/components/FileDropzone/FileDropzone.scss +++ b/src/components/FileDropzone/FileDropzone.scss @@ -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; diff --git a/src/components/Modals/OrderDetailsConfirmModal/OrderDetailsConfirmModal.tsx b/src/components/Modals/OrderDetailsConfirmModal/OrderDetailsConfirmModal.tsx index 872e1ad8..85e7a0e5 100644 --- a/src/components/Modals/OrderDetailsConfirmModal/OrderDetailsConfirmModal.tsx +++ b/src/components/Modals/OrderDetailsConfirmModal/OrderDetailsConfirmModal.tsx @@ -1,13 +1,17 @@ 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 = { @@ -15,8 +19,17 @@ type TDocumentFile = { files: File[]; }; -const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetailsConfirmModalProps) => { +const OrderDetailsConfirmModal = ({ + isModalOpen, + onCancel, + onConfirm, + onRequestClose, + sendFile, +}: TOrderDetailsConfirmModalProps) => { const [documentFile, setDocumentFile] = useState({ errorMessage: null, files: [] }); + const { orderDetails } = useOrderDetails(); + const { displayPaymentAmount, local_currency: localCurrency, otherUserDetails } = orderDetails ?? {}; + const { name } = otherUserDetails ?? {}; const { isMobile } = useDevice(); const buttonTextSize = isMobile ? 'md' : 'sm'; @@ -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 ( - 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`} We accept JPG, PDF, or PNG (up to 5MB). @@ -72,8 +80,10 @@ const OrderDetailsConfirmModal = ({ isModalOpen, onRequestClose }: TOrderDetails - -
); }; diff --git a/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/OrderDetailsCardFooter.tsx b/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/OrderDetailsCardFooter.tsx index c1e7675f..59abac3a 100644 --- a/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/OrderDetailsCardFooter.tsx +++ b/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/OrderDetailsCardFooter.tsx @@ -1,11 +1,14 @@ -import { OrderDetailsCancelModal, OrderDetailsComplainModal } from '@/components/Modals'; +import { useEffect } from 'react'; +import { OrderDetailsCancelModal, OrderDetailsComplainModal, OrderDetailsConfirmModal } from '@/components/Modals'; +import { ERROR_CODES } from '@/constants'; +import { api } from '@/hooks'; import { useModalManager } from '@/hooks/custom-hooks'; import { useOrderDetails } from '@/providers/OrderDetailsProvider'; import { Button, useDevice } from '@deriv-com/ui'; import './OrderDetailsCardFooter.scss'; // TODO: Implement functionality for each button when integrating with the API and disable buttons while chat is loading -const OrderDetailsCardFooter = () => { +const OrderDetailsCardFooter = ({ sendFile }: { sendFile: (file: File) => void }) => { const { orderDetails } = useOrderDetails(); const { id, @@ -15,10 +18,34 @@ const OrderDetailsCardFooter = () => { shouldShowOnlyComplainButton, shouldShowOnlyReceivedButton, } = orderDetails; + const { isMobile } = useDevice(); const { hideModal, isModalOpenFor, showModal } = useModalManager({ shouldReinitializeModals: false }); + const { error, isError, mutate } = api.order.useConfirm(); const textSize = isMobile ? 'md' : 'sm'; + //TODO: handle email verification, invalid verification, and rating modals. + const handleModalDisplay = (isError: boolean, isBuyOrderForUser: boolean, code?: string) => { + if (isError) { + if (code === ERROR_CODES.ORDER_EMAIL_VERIFICATION_REQUIRED) { + showModal('EmailVerificationModal'); + } else if ( + code === ERROR_CODES.INVALID_VERIFICATION_TOKEN || + code === ERROR_CODES.EXCESSIVE_VERIFICATION_REQUESTS + ) { + showModal('InvalidVerificationLinkModal'); + } else if (code === ERROR_CODES.EXCESSIVE_VERIFICATION_FAILURES && isBuyOrderForUser) { + showModal('EmailLinkBlockedModal'); + } + } else if (!isBuyOrderForUser) { + showModal('RatingModal'); + } + }; + + useEffect(() => { + handleModalDisplay(isError, isBuyOrderForUser, error?.error?.code); + }, [error?.error, isBuyOrderForUser, isError]); + if ( !shouldShowCancelAndPaidButton && !shouldShowComplainAndReceivedButton && @@ -28,6 +55,11 @@ const OrderDetailsCardFooter = () => { return null; } + const onClickPaid = () => { + hideModal(); + mutate({ id }); + }; + return (
{shouldShowCancelAndPaidButton && ( @@ -42,7 +74,7 @@ const OrderDetailsCardFooter = () => { > Cancel order -
@@ -100,6 +132,15 @@ const OrderDetailsCardFooter = () => { onRequestClose={hideModal} /> )} + {!!isModalOpenFor('OrderDetailsConfirmModal') && ( + + )} ); }; diff --git a/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx b/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx index ab3b9714..f705c461 100644 --- a/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx +++ b/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx @@ -38,11 +38,20 @@ jest.mock('@/providers/OrderDetailsProvider', () => ({ }), })); -const mockUseOrderDetails = useOrderDetails as jest.Mock; +jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), + api: { + order: { + useConfirm: jest.fn().mockReturnValue({ error: null, isError: false, mutate: jest.fn() }), + }, + }, +})); +const mockUseOrderDetails = useOrderDetails as jest.Mock; +const mockProps = { sendFile: jest.fn() }; describe('', () => { it('should render cancel and paid buttons', () => { - render(); + render(); expect(screen.getByRole('button', { name: 'Cancel order' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'I’ve paid' })).toBeInTheDocument(); }); @@ -56,7 +65,7 @@ describe('', () => { }, }); - render(); + render(); expect(screen.getByRole('button', { name: 'Complain' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'I’ve received payment' })).toBeInTheDocument(); @@ -71,7 +80,7 @@ describe('', () => { }, }); - render(); + render(); expect(screen.getByRole('button', { name: 'Complain' })).toBeInTheDocument(); }); @@ -85,7 +94,7 @@ describe('', () => { }, }); - render(); + render(); expect(screen.getByRole('button', { name: 'I’ve received payment' })).toBeInTheDocument(); }); @@ -97,7 +106,7 @@ describe('', () => { }, }); - const { container } = render(); + const { container } = render(); expect(container).toBeEmptyDOMElement(); }); @@ -114,7 +123,7 @@ describe('', () => { }, }); - render(); + render(); const complainButton = screen.getByRole('button', { name: 'Complain' }); expect(complainButton).toBeInTheDocument(); @@ -134,7 +143,7 @@ describe('', () => { }, }); - render(); + render(); const cancelOrderButton = screen.getByRole('button', { name: 'Cancel order' }); expect(cancelOrderButton).toBeInTheDocument(); @@ -156,7 +165,7 @@ describe('', () => { }, }); - render(); + render(); const complainButton = screen.getByRole('button', { name: 'Complain' }); expect(complainButton).toBeInTheDocument(); diff --git a/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx b/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx index 18837259..16e286db 100644 --- a/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx +++ b/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx @@ -22,9 +22,11 @@ jest.mock('../OrderDetailsCardFooter', () => ({ const mockUseDevice = useDevice as jest.Mock; +const mockProps = { sendFile: jest.fn() }; + describe('', () => { it('should render the OrderDetailsCard component', () => { - render(); + render(); expect(screen.getByText('OrderDetailsCardHeader')).toBeInTheDocument(); expect(screen.getByText('OrderDetailsCardInfo')).toBeInTheDocument(); expect(screen.getByText('OrderDetailsCardReview')).toBeInTheDocument(); @@ -34,7 +36,7 @@ describe('', () => { it('should not render the OrderDetailsCardFooter component on mobile', () => { mockUseDevice.mockReturnValue({ isDesktop: false }); - render(); + render(); expect(screen.queryByText('OrderDetailsCardFooter')).not.toBeInTheDocument(); }); diff --git a/src/pages/orders/screens/OrderDetails/OrderDetails.tsx b/src/pages/orders/screens/OrderDetails/OrderDetails.tsx index dbd3378b..7c317174 100644 --- a/src/pages/orders/screens/OrderDetails/OrderDetails.tsx +++ b/src/pages/orders/screens/OrderDetails/OrderDetails.tsx @@ -3,7 +3,7 @@ import { useHistory, useLocation, useParams } from 'react-router-dom'; import { FullPageMobileWrapper, PageReturn } from '@/components'; import { BUY_SELL_URL, ORDERS_URL } from '@/constants'; import { api } from '@/hooks'; -import { useExtendedOrderDetails } from '@/hooks/custom-hooks'; +import { useExtendedOrderDetails, useSendbird } from '@/hooks/custom-hooks'; import { ExtendedOrderDetails } from '@/hooks/custom-hooks/useExtendedOrderDetails'; import { OrderDetailsProvider } from '@/providers/OrderDetailsProvider'; import { LegacyLiveChatOutlineIcon } from '@deriv/quill-icons'; @@ -32,6 +32,7 @@ const OrderDetails = () => { }); const { isBuyOrderForUser, shouldShowLostFundsBanner } = orderDetails; const { isMobile } = useDevice(); + const { sendFile, userId, ...rest } = useSendbird(orderDetails?.id, !!error, orderDetails?.chat_channel_url ?? ''); const headerText = `${isBuyOrderForUser ? 'Buy' : 'Sell'} USD order`; const warningMessage = 'Don’t risk your funds with cash transactions. Use bank transfers or e-wallets instead.'; @@ -69,16 +70,18 @@ const OrderDetails = () => { {showChat ? ( ) : ( } + renderFooter={() => } renderHeader={() => ( {headerText} @@ -103,7 +106,7 @@ const OrderDetails = () => { {warningMessage} )} - + )} @@ -121,11 +124,14 @@ const OrderDetails = () => { )}
- +
diff --git a/src/pages/orders/screens/OrdersChatSection/OrdersChatSection.tsx b/src/pages/orders/screens/OrdersChatSection/OrdersChatSection.tsx index 47f1f372..e3b12bdc 100644 --- a/src/pages/orders/screens/OrdersChatSection/OrdersChatSection.tsx +++ b/src/pages/orders/screens/OrdersChatSection/OrdersChatSection.tsx @@ -1,21 +1,29 @@ +import { TActiveChannel, TChatMessages } from 'types'; import { FullPageMobileWrapper, LightDivider } from '@/components'; -import { useExtendedOrderDetails, useSendbird } from '@/hooks/custom-hooks'; +import { useExtendedOrderDetails } from '@/hooks/custom-hooks'; import { Loader, useDevice } from '@deriv-com/ui'; import { ChatError, ChatFooter, ChatHeader, ChatMessages } from '../../components'; import './OrdersChatSection.scss'; type TOrdersChatSectionProps = { - id: string; + activeChatChannel: TActiveChannel; + isChatLoading: boolean; + isError: boolean; isInactive: boolean; + messages: TChatMessages; onReturn?: () => void; otherUserDetails: ReturnType['data']['otherUserDetails']; + refreshChat: () => void; + sendFile: (file: File) => void; + sendMessage: (message: string) => void; + userId: string; }; -const OrdersChatSection = ({ id, isInactive, onReturn, otherUserDetails }: TOrdersChatSectionProps) => { +const OrdersChatSection = ({ isInactive, onReturn, otherUserDetails, ...sendBirdData }: TOrdersChatSectionProps) => { + const { activeChatChannel, isChatLoading, isError, messages, refreshChat, sendFile, sendMessage, userId } = + sendBirdData; const { isMobile } = useDevice(); const { is_online: isOnline, last_online_time: lastOnlineTime, name } = otherUserDetails ?? {}; - const { activeChatChannel, isChatLoading, isError, messages, refreshChat, sendFile, sendMessage, userId } = - useSendbird(id); const isChannelClosed = isInactive || !!activeChatChannel?.isFrozen; if (isError) { diff --git a/types.ts b/types.ts index 577a7d94..eff5f68d 100644 --- a/types.ts +++ b/types.ts @@ -3,7 +3,7 @@ import { ComponentProps } from 'react'; import { AD_CONDITION_TYPES, ERROR_CODES } from '@/constants'; import { api } from '@/hooks'; import { useSendbirdServiceToken } from '@/hooks/api/account'; -import { useAdvertiserStats } from '@/hooks/custom-hooks'; +import { useAdvertiserStats, useSendbird } from '@/hooks/custom-hooks'; import { useExchangeRates } from '@deriv-com/api-hooks'; import { Text } from '@deriv-com/ui'; import { CurrencyConstants } from '@deriv-com/utils'; @@ -213,3 +213,6 @@ export type TName = THooks.AdvertiserPaymentMethods.Get[number]['fields']['name' export type TAccount = THooks.AdvertiserPaymentMethods.Get[number]['fields']['account']; export type TTextSize = ComponentProps['size']; + +export type TActiveChannel = ReturnType['activeChatChannel']; +export type TChatMessages = NonNullable['messages']>;