From 0c4900f62d54de1abfcf49f3e3ece7de41b7f42f Mon Sep 17 00:00:00 2001 From: ameerul-deriv <103412909+ameerul-deriv@users.noreply.github.com> Date: Mon, 25 Mar 2024 19:16:14 +0800 Subject: [PATCH] [FEQ] / Ameerul / FEQ-1713 Create the order details section (#14236) * chore: added OrderDetails, Header, Info components * chore: added PaymentMethodAccordion component * chore: updated styles for PaymentMethodAccordion * chore: added routing and functionality for order details * chore: added review/rating block * chore: added responsive view * chore: fixed footer styles and added key in ActiveOrderInfo * fix: routing issues from BuySellForm to orderDetails * chore: added TOrder type for p2p_order_info * chore: combined chat with order details, added orderDetails provider * chore: added useState to handle showing chat on responsive * chore: added test cases for OrderDetails related components * fix: review order not showing disabled prompt * chore: updated TODO * chore: added error handler, updated ui issues * fix: failing test cases * chore: added suggestions * fix: remove useOrderInfo from useSendbird, pass order info using provider * chore: moved OrderDetailsProvider to providers folder under src, added comments * chore: remove !important * chore: remove TOrders * chore: added comment explainining recommended * chore: empty commit * chore: separated component out for recommended status * chore: updated test case for recommended status * chore: empty commit * chore: added comment --- .../entity/order/p2p-order/useOrderInfo.ts | 17 +- .../BuySellForm/BuySellData/BuySellData.tsx | 4 +- .../components/BuySellForm/BuySellForm.tsx | 15 +- .../BuySellFormFooter/BuySellFormFooter.tsx | 9 +- .../PaymentMethodWithIcon.tsx | 10 +- packages/p2p-v2/src/components/index.ts | 1 + .../src/hooks/useExtendedOrderDetails.ts | 30 ++- packages/p2p-v2/src/hooks/useSendbird.ts | 15 +- .../OrderDetailsCard/OrderDetailsCard.scss | 14 ++ .../OrderDetailsCard/OrderDetailsCard.tsx | 24 +++ .../OrderDetailsCardFooter.scss | 8 + .../OrderDetailsCardFooter.tsx | 63 +++++++ .../__tests__/OrderDetailsCardFooter.spec.tsx | 86 +++++++++ .../OrderDetailsCardFooter/index.ts | 1 + .../OrderDetailsCardHeader.tsx | 64 +++++++ .../__tests__/OrderDetailsCardHeader.spec.tsx | 111 +++++++++++ .../OrderDetailsCardHeader/index.ts | 1 + .../ActiveOrderInfo/ActiveOrderInfo.tsx | 51 +++++ .../__tests__/ActiveOrderInfo.spec.tsx | 158 ++++++++++++++++ .../ActiveOrderInfo/index.ts | 1 + .../OrderDetailsCardInfo.tsx | 50 +++++ .../PaymentMethodAccordion.scss | 16 ++ .../PaymentMethodAccordion.tsx | 96 ++++++++++ .../PaymentMethodAccordion/index.ts | 1 + .../__tests__/OrderDetailsCardInfo.spec.tsx | 60 ++++++ .../OrderDetailsCardInfo/index.ts | 1 + .../OrderDetailsCardReview.tsx | 65 +++++++ .../RecommendationStatus.tsx | 31 ++++ .../RecommendationStatus/index.ts | 1 + .../__tests__/OrderDetailsCardReview.spec.tsx | 120 ++++++++++++ .../OrderDetailsCardReview/index.ts | 1 + .../__tests__/OrderDetailsCard.spec.tsx | 42 +++++ .../components/OrderDetailsCard/index.ts | 1 + .../screens/OrderDetails/OrderDetails.scss | 26 +++ .../screens/OrderDetails/OrderDetails.tsx | 115 ++++++++++-- .../__tests__/OrderDetails.spec.tsx | 174 ++++++++++++++++++ .../pages/orders/screens/Orders/Orders.tsx | 8 +- .../Orders/OrdersTable/OrdersTable.scss | 10 +- .../Orders/OrdersTableRow/OrdersTableRow.scss | 2 + .../Orders/OrdersTableRow/OrdersTableRow.tsx | 12 +- .../screens/Orders/__tests__/Orders.spec.tsx | 25 ++- .../OrdersChatSection/OrdersChatSection.tsx | 7 +- .../OrderDetailsProvider.tsx | 21 +++ .../__tests__/OrderDetailsProvider.spec.tsx | 17 ++ .../providers/OrderDetailsProvider/index.ts | 1 + .../p2p-v2/src/routes/AppContent/index.tsx | 8 +- packages/p2p-v2/src/utils/time.ts | 14 ++ 47 files changed, 1556 insertions(+), 52 deletions(-) create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCard.scss create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCard.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/OrderDetailsCardFooter.scss create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/OrderDetailsCardFooter.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/OrderDetailsCardHeader.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/__tests__/OrderDetailsCardHeader.spec.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/ActiveOrderInfo.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/__tests__/ActiveOrderInfo.spec.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/OrderDetailsCardInfo.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.scss create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/__tests__/OrderDetailsCardInfo.spec.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/OrderDetailsCardReview.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/RecommendationStatus.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/__tests__/OrderDetailsCardReview.spec.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx create mode 100644 packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/index.ts create mode 100644 packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.scss create mode 100644 packages/p2p-v2/src/pages/orders/screens/OrderDetails/__tests__/OrderDetails.spec.tsx create mode 100644 packages/p2p-v2/src/providers/OrderDetailsProvider/OrderDetailsProvider.tsx create mode 100644 packages/p2p-v2/src/providers/OrderDetailsProvider/__tests__/OrderDetailsProvider.spec.tsx create mode 100644 packages/p2p-v2/src/providers/OrderDetailsProvider/index.ts diff --git a/packages/api-v2/src/hooks/p2p/entity/order/p2p-order/useOrderInfo.ts b/packages/api-v2/src/hooks/p2p/entity/order/p2p-order/useOrderInfo.ts index bdcd66dc920c..d50a38116553 100644 --- a/packages/api-v2/src/hooks/p2p/entity/order/p2p-order/useOrderInfo.ts +++ b/packages/api-v2/src/hooks/p2p/entity/order/p2p-order/useOrderInfo.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import useQuery from '../../../../../useQuery'; import useAuthorize from '../../../../useAuthorize'; +// TODO: Convert this to use useSubscribe as it is a subscribable endpoint /** This custom hook that returns information about the given order ID */ const useOrderInfo = (id: string) => { const { isSuccess } = useAuthorize(); @@ -53,13 +54,15 @@ const useOrderInfo = (id: string) => { is_reviewable: Boolean(is_reviewable), /** Indicates if the latest order changes have been seen by the current client. */ is_seen: Boolean(is_seen), - review_details: { - ...review_details, - /** Indicates if the advertiser is recommended or not. */ - is_recommended: Boolean(review_details?.recommended), - /** Indicates that the advertiser has not been recommended yet. */ - has_not_been_recommended: review_details?.recommended === null, - }, + review_details: review_details + ? { + ...review_details, + /** Indicates if the advertiser is recommended or not. */ + is_recommended: Boolean(review_details?.recommended), + /** Indicates that the advertiser has not been recommended yet. */ + has_not_been_recommended: review_details?.recommended === null, + } + : undefined, /** Indicates that the seller in the process of confirming the order. */ is_verification_pending: Boolean(verification_pending), }; diff --git a/packages/p2p-v2/src/components/BuySellForm/BuySellData/BuySellData.tsx b/packages/p2p-v2/src/components/BuySellForm/BuySellData/BuySellData.tsx index c3668644b7d8..3d614dffcdad 100644 --- a/packages/p2p-v2/src/components/BuySellForm/BuySellData/BuySellData.tsx +++ b/packages/p2p-v2/src/components/BuySellForm/BuySellData/BuySellData.tsx @@ -1,9 +1,8 @@ import React from 'react'; import { THooks } from 'types'; +import { PaymentMethodWithIcon } from '@/components'; import { formatTime } from '@/utils'; -import { p2p } from '@deriv/api-v2'; import { Text, useDevice } from '@deriv-com/ui'; -import { PaymentMethodWithIcon } from '../../PaymentMethodWithIcon'; import './BuySellData.scss'; type TBuySellDataProps = { @@ -64,6 +63,7 @@ const BuySellData = ({ {paymentMethodNames?.length ? paymentMethodNames.map(method => ( { - const { mutate } = p2p.order.useCreate(); + const { data: orderCreatedInfo, isSuccess, mutate } = p2p.order.useCreate(); const [selectedPaymentMethods, setSelectedPaymentMethods] = useState([]); const { @@ -89,6 +90,7 @@ const BuySellForm = ({ }; }); + const history = useHistory(); const { isMobile } = useDevice(); const isBuy = type === BUY_SELL.BUY; @@ -144,6 +146,13 @@ const BuySellForm = ({ } }; + useEffect(() => { + if (isSuccess && orderCreatedInfo) { + history.push(`${BASE_URL}/orders?order=${orderCreatedInfo.id}`); + onRequestClose(); + } + }, [isSuccess, orderCreatedInfo, history, onRequestClose]); + return (
- + + + ); + + if (shouldShowComplainAndReceivedButton) + return ( +
+ + +
+ ); + + if (shouldShowOnlyComplainButton) + return ( +
+ +
+ ); + + if (shouldShowOnlyReceivedButton) + return ( +
+ +
+ ); + + return null; +}; + +export default OrderDetailsCardFooter; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx new file mode 100644 index 000000000000..c97af6ca774f --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/__tests__/OrderDetailsCardFooter.spec.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { render, screen } from '@testing-library/react'; +import OrderDetailsCardFooter from '../OrderDetailsCardFooter'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: () => ({ isMobile: false }), +})); + +jest.mock('@/providers/OrderDetailsProvider', () => ({ + useOrderDetails: jest.fn().mockReturnValue({ + orderDetails: { + shouldShowCancelAndPaidButton: true, + shouldShowComplainAndReceivedButton: false, + shouldShowOnlyComplainButton: false, + shouldShowOnlyReceivedButton: false, + }, + }), +})); + +const mockUseOrderDetails = useOrderDetails as jest.Mock; + +describe('', () => { + it('should render cancel and paid buttons', () => { + render(); + expect(screen.getByRole('button', { name: 'Cancel order' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'I’ve paid' })).toBeInTheDocument(); + }); + + it('should render complain and received buttons', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldShowCancelAndPaidButton: false, + shouldShowComplainAndReceivedButton: true, + }, + }); + + render(); + + expect(screen.getByRole('button', { name: 'Complain' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'I’ve received payment' })).toBeInTheDocument(); + }); + + it('should render only complain button', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldShowComplainAndReceivedButton: false, + shouldShowOnlyComplainButton: true, + }, + }); + + render(); + + expect(screen.getByRole('button', { name: 'Complain' })).toBeInTheDocument(); + }); + + it('should render only received button', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldShowOnlyComplainButton: false, + shouldShowOnlyReceivedButton: true, + }, + }); + + render(); + + expect(screen.getByRole('button', { name: 'I’ve received payment' })).toBeInTheDocument(); + }); + + it('should not render any buttons', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldShowOnlyReceivedButton: false, + }, + }); + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/index.ts new file mode 100644 index 000000000000..1c390c33aae0 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardFooter/index.ts @@ -0,0 +1 @@ +export { default as OrderDetailsCardFooter } from './OrderDetailsCardFooter'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/OrderDetailsCardHeader.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/OrderDetailsCardHeader.tsx new file mode 100644 index 000000000000..19db422a0ea2 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/OrderDetailsCardHeader.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { getDistanceToServerTime } from '@/utils'; +import { useServerTime } from '@deriv/api-v2'; +import { Text, useDevice } from '@deriv-com/ui'; +import { OrderTimer } from '../../OrderTimer'; + +const OrderDetailsCardHeader = () => { + const { orderDetails } = useOrderDetails(); + + const { + displayPaymentAmount, + hasTimerExpired, + id, + isBuyerConfirmedOrder, + isPendingOrder, + local_currency: localCurrency, + orderExpiryMilliseconds, + shouldHighlightAlert, + shouldHighlightDanger, + shouldHighlightSuccess, + shouldShowOrderTimer, + statusString, + } = orderDetails; + + const { isMobile } = useDevice(); + const textSize = isMobile ? 'sm' : 'xs'; + const { data: serverTime } = useServerTime(); + const distance = getDistanceToServerTime(orderExpiryMilliseconds, serverTime?.server_time_moment); + const getStatusColor = () => { + if (shouldHighlightAlert) return 'warning'; + else if (shouldHighlightDanger) return 'error'; + else if (shouldHighlightSuccess) return 'success'; + return 'less-prominent'; + }; + + return ( +
+
+ + {statusString} + + {!hasTimerExpired && (isPendingOrder || isBuyerConfirmedOrder) && ( + + {displayPaymentAmount} {localCurrency} + + )} + + Order ID {id} + +
+ {shouldShowOrderTimer && ( +
+ + Time left + + +
+ )} +
+ ); +}; + +export default OrderDetailsCardHeader; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/__tests__/OrderDetailsCardHeader.spec.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/__tests__/OrderDetailsCardHeader.spec.tsx new file mode 100644 index 000000000000..6f3cb69dc066 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/__tests__/OrderDetailsCardHeader.spec.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { APIProvider, AuthProvider } from '@deriv/api-v2'; +import { render, screen } from '@testing-library/react'; +import OrderDetailsCardHeader from '../OrderDetailsCardHeader'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: () => ({ isMobile: false }), +})); + +jest.mock('@/providers/OrderDetailsProvider', () => ({ + useOrderDetails: jest.fn().mockReturnValue({ + orderDetails: { + displayPaymentAmount: '0.10', + hasTimerExpired: false, + id: '123', + isBuyerConfirmedOrder: true, + isPendingOrder: true, + local_currency: 'USD', + orderExpiryMilliseconds: 12345567, + shouldHighlightAlert: false, + shouldHighlightDanger: true, + shouldHighlightSuccess: false, + shouldShowOrderTimer: true, + statusString: 'Pay now', + }, + }), +})); + +jest.mock('../../../OrderTimer', () => ({ + OrderTimer: () =>
OrderTimer
, +})); + +const mockUseOrderDetails = useOrderDetails as jest.Mock; + +const wrapper = ({ children }: { children: JSX.Element }) => ( + + {children} + +); + +describe('', () => { + it('should show status with error class, the order ID and time left', () => { + render(, { wrapper }); + + const statusText = screen.getByText('Pay now'); + + expect(statusText).toBeInTheDocument(); + expect(statusText).toHaveClass('derivs-text__color--error'); + expect(screen.getByText('Order ID 123')).toBeInTheDocument(); + expect(screen.getByText('Time left')).toBeInTheDocument(); + expect(screen.getByText('OrderTimer')).toBeInTheDocument(); + }); + + it('should show status with success class', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldHighlightDanger: false, + shouldHighlightSuccess: true, + statusString: 'Completed', + }, + }); + + render(, { wrapper }); + + const statusText = screen.getByText('Completed'); + + expect(statusText).toBeInTheDocument(); + expect(statusText).toHaveClass('derivs-text__color--success'); + }); + + it('should show status with warning class', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldHighlightAlert: true, + shouldHighlightSuccess: false, + statusString: 'Waiting for seller to confirm', + }, + }); + + render(, { wrapper }); + + const statusText = screen.getByText('Waiting for seller to confirm'); + + expect(statusText).toBeInTheDocument(); + expect(statusText).toHaveClass('derivs-text__color--warning'); + }); + + it('should show status with less-prominent class and hide timer if shouldShowOrderTimer is false', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + shouldHighlightAlert: false, + shouldShowOrderTimer: false, + statusString: 'Expired', + }, + }); + + render(, { wrapper }); + + const statusText = screen.getByText('Expired'); + + expect(statusText).toBeInTheDocument(); + expect(statusText).toHaveClass('derivs-text__color--less-prominent'); + expect(screen.queryByText('Time left')).not.toBeInTheDocument(); + expect(screen.queryByText('OrderTimer')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/index.ts new file mode 100644 index 000000000000..5ba560db83d8 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardHeader/index.ts @@ -0,0 +1 @@ +export { default as OrderDetailsCardHeader } from './OrderDetailsCardHeader'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/ActiveOrderInfo.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/ActiveOrderInfo.tsx new file mode 100644 index 000000000000..f4bce14e61e5 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/ActiveOrderInfo.tsx @@ -0,0 +1,51 @@ +import React, { Fragment } from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { Divider, Text, useDevice } from '@deriv-com/ui'; +import { PaymentMethodAccordion } from '../PaymentMethodAccordion'; + +const ActiveOrderInfo = () => { + const { orderDetails } = useOrderDetails(); + const { + advert_details: { description }, + contact_info: contactInfo, + isActiveOrder, + labels, + payment_info: paymentInfo, + payment_method_details: paymentMethodDetails, + } = orderDetails; + const { isMobile } = useDevice(); + const textSize = isMobile ? 'md' : 'sm'; + + const adDetails = [ + { text: labels.contactDetails, value: contactInfo || '-' }, + { text: labels.instructions, value: description || '-' }, + ]; + + if (isActiveOrder) + return ( + <> + + + + {adDetails.map((detail, key) => ( + +
+ + {detail.text} + + {detail.value} +
+ {key === 0 && } +
+ ))} + + ); + + return null; +}; + +export default ActiveOrderInfo; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/__tests__/ActiveOrderInfo.spec.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/__tests__/ActiveOrderInfo.spec.tsx new file mode 100644 index 000000000000..5cc4be304a84 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/__tests__/ActiveOrderInfo.spec.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ActiveOrderInfo from '../ActiveOrderInfo'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: () => ({ isMobile: false }), +})); + +jest.mock('@/providers/OrderDetailsProvider', () => ({ + useOrderDetails: jest.fn().mockReturnValue({ + orderDetails: { + advert_details: { + description: 'This is my description', + }, + contact_info: 'This is my contact info', + isActiveOrder: true, + labels: { + contactDetails: 'Seller’s contact details', + instructions: 'Seller’s instructions', + paymentDetails: 'Seller’s payment details', + }, + payment_info: 'This is my payment info', + payment_method_details: { + '1': { + display_name: 'Alipay', + fields: { + account: { + display_name: 'Alipay ID', + required: 1, + type: 'text', + value: '12345', + }, + instructions: { + display_name: 'Instructions', + required: 0, + type: 'memo', + value: 'Alipay instructions', + }, + }, + is_enabled: 1, + method: 'alipay', + type: 'ewallet', + }, + }, + }, + }), +})); + +const mockUseOrderDetails = useOrderDetails as jest.Mock; + +describe('', () => { + it('should render the payment methods, payment details and instructions', () => { + render(); + + expect(screen.getByText('Seller’s payment details')).toBeInTheDocument(); + expect(screen.getByText('Expand all')).toBeInTheDocument(); + expect(screen.getByText('Alipay')).toBeInTheDocument(); + + expect(screen.getByText('Seller’s contact details')).toBeInTheDocument(); + expect(screen.getByText('This is my contact info')).toBeInTheDocument(); + + expect(screen.getByText('Seller’s instructions')).toBeInTheDocument(); + expect(screen.getByText('This is my description')).toBeInTheDocument(); + }); + + it('should show the expanded view of payment method details after clicking on a payment method and hide it after clicking on it again', () => { + render(); + + const alipayMethod = screen.getByText('Alipay'); + userEvent.click(alipayMethod); + + expect(screen.getByText('Alipay ID')).toBeInTheDocument(); + expect(screen.getByText('12345')).toBeInTheDocument(); + expect(screen.getByText('Instructions')).toBeInTheDocument(); + expect(screen.getByText('Alipay instructions')).toBeInTheDocument(); + + userEvent.click(alipayMethod); + + expect(screen.queryByText('Alipay ID')).not.toBeInTheDocument(); + expect(screen.queryByText('12345')).not.toBeInTheDocument(); + expect(screen.queryByText('Instructions')).not.toBeInTheDocument(); + expect(screen.queryByText('Alipay instructions')).not.toBeInTheDocument(); + }); + + it('should show the expanded view of payment method details after clicking on expand all and hide it after clicking the button again', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + payment_method_details: { + ...mockUseOrderDetails().orderDetails.payment_method_details, + '2': { + display_name: 'Bank transfer', + fields: { + account: { + display_name: 'Account Number', + required: 1, + type: 'text', + value: '54321', + }, + instructions: { + display_name: 'Bank Name', + required: 0, + type: 'memo', + value: 'test bank', + }, + }, + is_enabled: 1, + method: 'alipay', + type: 'ewallet', + }, + }, + }, + }); + + render(); + + const expandAllButton = screen.getByRole('button', { name: 'Expand all' }); + userEvent.click(expandAllButton); + + expect(screen.getByText('Alipay ID')).toBeInTheDocument(); + expect(screen.getByText('12345')).toBeInTheDocument(); + expect(screen.getByText('Instructions')).toBeInTheDocument(); + expect(screen.getByText('Alipay instructions')).toBeInTheDocument(); + + expect(screen.getByText('Account Number')).toBeInTheDocument(); + expect(screen.getByText('54321')).toBeInTheDocument(); + expect(screen.getByText('Bank Name')).toBeInTheDocument(); + expect(screen.getByText('test bank')).toBeInTheDocument(); + + const collapseAllButton = screen.getByRole('button', { name: 'Collapse all' }); + + expect(collapseAllButton).toBeInTheDocument(); + userEvent.click(collapseAllButton); + + expect(screen.queryByText('Alipay ID')).not.toBeInTheDocument(); + expect(screen.queryByText('12345')).not.toBeInTheDocument(); + expect(screen.queryByText('Instructions')).not.toBeInTheDocument(); + expect(screen.queryByText('Alipay instructions')).not.toBeInTheDocument(); + + expect(screen.queryByText('Account Number')).not.toBeInTheDocument(); + expect(screen.queryByText('54321')).not.toBeInTheDocument(); + expect(screen.queryByText('Bank Name')).not.toBeInTheDocument(); + expect(screen.queryByText('test bank')).not.toBeInTheDocument(); + }); + + it('should return null if isActiveOrder is false', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { ...mockUseOrderDetails().orderDetails, isActiveOrder: false }, + }); + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/index.ts new file mode 100644 index 000000000000..d2c6c9b0849d --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/ActiveOrderInfo/index.ts @@ -0,0 +1 @@ +export { default as ActiveOrderInfo } from './ActiveOrderInfo'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/OrderDetailsCardInfo.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/OrderDetailsCardInfo.tsx new file mode 100644 index 000000000000..18a42df4c156 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/OrderDetailsCardInfo.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { Text, useDevice } from '@deriv-com/ui'; +import { ActiveOrderInfo } from './ActiveOrderInfo'; + +const OrderDetailsCardInfo = () => { + const { orderDetails } = useOrderDetails(); + const { + account_currency: accountCurrency, + advertiser_details: { name }, + amount_display: amountDisplay, + displayPaymentAmount, + labels, + local_currency: localCurrency, + otherUserDetails, + purchaseTime, + rateAmount, + } = orderDetails; + const { isMobile } = useDevice(); + + const clientDetails = [ + { text: labels.counterpartyNicknameLabel, value: name }, + { + text: labels.counterpartyRealNameLabel, + value: `${otherUserDetails.first_name} ${otherUserDetails.last_name}`, + }, + { text: labels.leftSendOrReceive, value: `${displayPaymentAmount} ${localCurrency}` }, + { text: labels.rightSendOrReceive, value: `${amountDisplay} ${accountCurrency}` }, + { text: `Rate (1 ${accountCurrency})`, value: `${rateAmount} ${localCurrency}` }, + { text: 'Time', value: purchaseTime }, + ]; + + return ( +
+
+ {clientDetails.map(detail => ( +
+ + {detail.text} + + {detail.value} +
+ ))} +
+ +
+ ); +}; + +export default OrderDetailsCardInfo; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.scss b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.scss new file mode 100644 index 000000000000..4012ce458610 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.scss @@ -0,0 +1,16 @@ +.p2p-v2-payment-method-accordion { + &__button { + height: 0; + padding: 0; + + // TODO: Remove hover effect once deriv-com/ui Buttons allows removing hover styles + &:hover { + background-color: transparent; + + & > span { + // stylelint-disable-next-line declaration-no-important + color: #ff444f !important; + } + } + } +} diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.tsx new file mode 100644 index 000000000000..31f581af7e58 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/PaymentMethodAccordion.tsx @@ -0,0 +1,96 @@ +import React, { useState } from 'react'; +import { PaymentMethodWithIcon } from '@/components'; +import { useExtendedOrderDetails } from '@/hooks'; +import { LabelPairedChevronRightSmRegularIcon } from '@deriv/quill-icons'; +import { Button, Text, useDevice } from '@deriv-com/ui'; +import './PaymentMethodAccordion.scss'; + +type TPaymentMethodAccordionProps = { + paymentDetails: string; + paymentInfo: string; + paymentMethodDetails: ReturnType['data']['payment_method_details']; +}; + +const PaymentMethodAccordion = ({ + paymentDetails, + paymentInfo, + paymentMethodDetails, +}: TPaymentMethodAccordionProps) => { + const [expandedIds, setExpandedIds] = useState([]); + const paymentMethodKeys = paymentMethodDetails ? Object.keys(paymentMethodDetails) : []; + const { isMobile } = useDevice(); + const bigTextSize = isMobile ? 'md' : 'sm'; + const smallTextSize = isMobile ? 'sm' : 'xs'; + + return ( +
+
+ + {paymentDetails} + + {paymentMethodKeys.length > 0 && ( + + )} +
+ {paymentMethodKeys.length === 0 ? ( + {paymentInfo} + ) : ( + <> + {paymentMethodKeys.map(key => { + if (paymentMethodDetails?.[key]) { + const paymentMethodType = paymentMethodDetails[key].type; + const paymentMethodFields = paymentMethodDetails[key].fields; + + return ( +
+
{ + if (expandedIds.includes(key)) + setExpandedIds(expandedIds.filter(id => id !== key)); + else setExpandedIds([...expandedIds, key]); + }} + > + + +
+ {expandedIds.includes(key) && ( +
+ {Object.keys(paymentMethodFields).map(fieldKey => { + const field = paymentMethodFields[fieldKey]; + return ( +
+ + {field.display_name} + + {field.value || '-'} +
+ ); + })} +
+ )} +
+ ); + } + })} + + )} +
+ ); +}; + +export default PaymentMethodAccordion; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/index.ts new file mode 100644 index 000000000000..76e94400369d --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/PaymentMethodAccordion/index.ts @@ -0,0 +1 @@ +export { default as PaymentMethodAccordion } from './PaymentMethodAccordion'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/__tests__/OrderDetailsCardInfo.spec.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/__tests__/OrderDetailsCardInfo.spec.tsx new file mode 100644 index 000000000000..9e2056b48f08 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/__tests__/OrderDetailsCardInfo.spec.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import OrderDetailsCardInfo from '../OrderDetailsCardInfo'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: () => ({ isMobile: false }), +})); + +jest.mock('@/providers/OrderDetailsProvider', () => ({ + useOrderDetails: jest.fn().mockReturnValue({ + orderDetails: { + account_currency: 'USD', + advertiser_details: { name: 'Johnny123' }, + amount_display: '100', + displayPaymentAmount: '110', + labels: { + counterpartyNicknameLabel: 'Seller’s nickname', + counterpartyRealNameLabel: 'Seller’s real name', + leftSendOrReceive: 'Send', + paymentDetails: 'Payment details', + rightSendOrReceive: 'Receive', + }, + local_currency: 'IDR', + otherUserDetails: { first_name: 'John', last_name: 'Doe' }, + purchaseTime: '2021-09-01 12:00:00', + rateAmount: '10', + }, + }), +})); + +jest.mock('../ActiveOrderInfo', () => ({ + ActiveOrderInfo: () =>
ActiveOrderInfo
, +})); + +describe('', () => { + it('should render order details info', () => { + render(); + + expect(screen.getByText('Seller’s nickname')).toBeInTheDocument(); + expect(screen.getByText('Johnny123')).toBeInTheDocument(); + + expect(screen.getByText('Seller’s real name')).toBeInTheDocument(); + expect(screen.getByText('John Doe')).toBeInTheDocument(); + + expect(screen.getByText('Send')).toBeInTheDocument(); + expect(screen.getByText('110 IDR')).toBeInTheDocument(); + + expect(screen.getByText('Receive')).toBeInTheDocument(); + expect(screen.getByText('100 USD')).toBeInTheDocument(); + + expect(screen.getByText('Rate (1 USD)')).toBeInTheDocument(); + expect(screen.getByText('10 IDR')).toBeInTheDocument(); + + expect(screen.getByText('Time')).toBeInTheDocument(); + expect(screen.getByText('2021-09-01 12:00:00')).toBeInTheDocument(); + + expect(screen.getByText('ActiveOrderInfo')).toBeInTheDocument(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/index.ts new file mode 100644 index 000000000000..5ef9ace6967c --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardInfo/index.ts @@ -0,0 +1 @@ +export { default as OrderDetailsCardInfo } from './OrderDetailsCardInfo'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/OrderDetailsCardReview.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/OrderDetailsCardReview.tsx new file mode 100644 index 000000000000..95fbd7ca3162 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/OrderDetailsCardReview.tsx @@ -0,0 +1,65 @@ +import React, { useEffect, useState } from 'react'; +import { StarRating } from '@/components'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { getDateAfterHours } from '@/utils'; +import { p2p } from '@deriv/api-v2'; +import { StandaloneStarFillIcon } from '@deriv/quill-icons'; +import { Button, Text, useDevice } from '@deriv-com/ui'; +import { RecommendationStatus } from './RecommendationStatus'; + +const OrderDetailsCardReview = () => { + const { orderDetails } = useOrderDetails(); + const { + completion_time: completionTime, + hasReviewDetails, + is_reviewable: isReviewable, + isCompletedOrder, + review_details: reviewDetails, + } = orderDetails; + const { data: p2pSettingsData } = p2p.settings.useGetSettings(); + const [remainingReviewTime, setRemainingReviewTime] = useState(null); + const ratingAverageDecimals = reviewDetails ? Number(Number(reviewDetails.rating).toFixed(1)) : 0; + const { isMobile } = useDevice(); + + useEffect(() => { + if (completionTime && p2pSettingsData?.review_period) { + setRemainingReviewTime(getDateAfterHours(completionTime, p2pSettingsData.review_period)); + } + }, [completionTime, p2pSettingsData?.review_period]); + + if (isCompletedOrder && !hasReviewDetails) + return ( +
+ + + {isReviewable + ? `You have until ${remainingReviewTime} GMT to rate this transaction.` + : 'You can no longer rate this transaction.'} + +
+ ); + + if (hasReviewDetails) { + return ( +
+ Your transaction experience +
+ + +
+
+ ); + } + + return null; +}; + +export default OrderDetailsCardReview; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/RecommendationStatus.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/RecommendationStatus.tsx new file mode 100644 index 000000000000..0efe1f759e0d --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/RecommendationStatus.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { StandaloneThumbsDownRegularIcon, StandaloneThumbsUpRegularIcon } from '@deriv/quill-icons'; +import { Text, useDevice } from '@deriv-com/ui'; + +const RecommendationStatus = () => { + const { isMobile } = useDevice(); + const { orderDetails } = useOrderDetails(); + const { review_details: reviewDetails } = orderDetails; + + // If the user doesn't select any recommendation, we don't show the recommendation status + if (reviewDetails?.recommended === null) return null; + + return ( + + {reviewDetails?.recommended ? ( + <> + + Recommended + + ) : ( + <> + + Not Recommended + + )} + + ); +}; + +export default RecommendationStatus; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/index.ts new file mode 100644 index 000000000000..2411420784ad --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/RecommendationStatus/index.ts @@ -0,0 +1 @@ +export { default as RecommendationStatus } from './RecommendationStatus'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/__tests__/OrderDetailsCardReview.spec.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/__tests__/OrderDetailsCardReview.spec.tsx new file mode 100644 index 000000000000..4fcd597b26b4 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/__tests__/OrderDetailsCardReview.spec.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { useOrderDetails } from '@/providers/OrderDetailsProvider'; +import { render, screen } from '@testing-library/react'; +import OrderDetailsCardReview from '../OrderDetailsCardReview'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: () => ({ isMobile: false }), +})); + +jest.mock('@deriv/api-v2', () => ({ + p2p: { + settings: { + useGetSettings: jest.fn(() => ({ + data: { review_period: 24 }, + })), + }, + }, +})); + +jest.mock('@/providers/OrderDetailsProvider', () => ({ + useOrderDetails: jest.fn().mockReturnValue({ + orderDetails: { + completion_time: 1710897035, + hasReviewDetails: false, + is_reviewable: true, + isCompletedOrder: true, + review_details: undefined, + }, + }), +})); + +const mockUseOrderDetails = useOrderDetails as jest.Mock; + +describe('', () => { + it('should prompt the user to rate the order if isCompletedOrder is true and hasReviewDetails is false', () => { + render(); + + expect(screen.getByRole('button', { name: 'Rate this transaction' })).toBeInTheDocument(); + expect(screen.getByText('You have until 21 Mar 2024, 01:10 GMT to rate this transaction.')).toBeInTheDocument(); + }); + + it('should prompt the user that they cannot rate the order if is_reviewable is false', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { ...mockUseOrderDetails().orderDetails, is_reviewable: false }, + }); + + render(); + + const notRatedButton = screen.getByRole('button', { name: 'Not rated' }); + + expect(notRatedButton).toBeInTheDocument(); + expect(notRatedButton).toBeDisabled(); + expect(screen.getByText('You can no longer rate this transaction.')).toBeInTheDocument(); + }); + + it('should show review details if hasReviewDetails is true with recommended text of recommended is true', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + hasReviewDetails: true, + review_details: { + rating: 5, + recommended: true, + }, + }, + }); + + render(); + + expect(screen.getByText('Your transaction experience')).toBeInTheDocument(); + expect(screen.getByText('Recommended')).toBeInTheDocument(); + }); + + it('should show review details if hasReviewDetails is true with recommended text of Not Recommended is false', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + hasReviewDetails: true, + review_details: { + rating: 5, + recommended: false, + }, + }, + }); + + render(); + + expect(screen.getByText('Your transaction experience')).toBeInTheDocument(); + expect(screen.getByText('Not Recommended')).toBeInTheDocument(); + }); + + it('should not show recommended status if recommended is null', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { + ...mockUseOrderDetails().orderDetails, + hasReviewDetails: true, + review_details: { + rating: 5, + recommended: null, + }, + }, + }); + + render(); + + expect(screen.queryByText('Recommended')).not.toBeInTheDocument(); + expect(screen.queryByText('Not Recommended')).not.toBeInTheDocument(); + }); + + it('should return null if isCompletedOrder is false and hasReviewDetails is false', () => { + mockUseOrderDetails.mockReturnValue({ + orderDetails: { ...mockUseOrderDetails().orderDetails, hasReviewDetails: false, isCompletedOrder: false }, + }); + + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/index.ts new file mode 100644 index 000000000000..a4539b7ca747 --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/OrderDetailsCardReview/index.ts @@ -0,0 +1 @@ +export { default as OrderDetailsCardReview } from './OrderDetailsCardReview'; diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx new file mode 100644 index 000000000000..51cf49517fde --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/__tests__/OrderDetailsCard.spec.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { useDevice } from '@deriv-com/ui'; +import { render, screen } from '@testing-library/react'; +import OrderDetailsCard from '../OrderDetailsCard'; + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: jest.fn().mockReturnValue({ isDesktop: true }), +})); + +jest.mock('../OrderDetailsCardHeader', () => ({ + OrderDetailsCardHeader: () =>
OrderDetailsCardHeader
, +})); +jest.mock('../OrderDetailsCardInfo', () => ({ + OrderDetailsCardInfo: () =>
OrderDetailsCardInfo
, +})); +jest.mock('../OrderDetailsCardReview', () => ({ + OrderDetailsCardReview: () =>
OrderDetailsCardReview
, +})); +jest.mock('../OrderDetailsCardFooter', () => ({ + OrderDetailsCardFooter: () =>
OrderDetailsCardFooter
, +})); + +const mockUseDevice = useDevice as jest.Mock; + +describe('', () => { + it('should render the OrderDetailsCard component', () => { + render(); + expect(screen.getByText('OrderDetailsCardHeader')).toBeInTheDocument(); + expect(screen.getByText('OrderDetailsCardInfo')).toBeInTheDocument(); + expect(screen.getByText('OrderDetailsCardReview')).toBeInTheDocument(); + expect(screen.getByText('OrderDetailsCardFooter')).toBeInTheDocument(); + }); + + it('should not render the OrderDetailsCardFooter component on mobile', () => { + mockUseDevice.mockReturnValue({ isDesktop: false }); + + render(); + + expect(screen.queryByText('OrderDetailsCardFooter')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/index.ts b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/index.ts new file mode 100644 index 000000000000..d7905833858a --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/components/OrderDetailsCard/index.ts @@ -0,0 +1 @@ +export { default as OrderDetailsCard } from './OrderDetailsCard'; diff --git a/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.scss b/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.scss new file mode 100644 index 000000000000..e5c53aa60f2a --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.scss @@ -0,0 +1,26 @@ +.p2p-v2-order-details { + overflow-y: scroll; + overflow-x: hidden; + height: calc(100vh - 28rem); + + @include mobile { + position: absolute; + top: 4rem; + height: calc(100vh - 7.4rem); + + & .p2p-v2-mobile-wrapper { + &__header { + background: #fff; + } + + &__body { + overflow-y: scroll; + } + + &__footer { + padding: 0; + border: none; + } + } + } +} diff --git a/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.tsx b/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.tsx index 8219e04657f1..24795c2bdb95 100644 --- a/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.tsx +++ b/packages/p2p-v2/src/pages/orders/screens/OrderDetails/OrderDetails.tsx @@ -1,14 +1,27 @@ -import React from 'react'; -import { useLocation } from 'react-router-dom'; +import React, { useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { FullPageMobileWrapper, PageReturn } from '@/components'; import { useExtendedOrderDetails } from '@/hooks'; +import { OrderDetailsProvider } from '@/providers/OrderDetailsProvider'; import { p2p, useActiveAccount, useServerTime } from '@deriv/api-v2'; +import { Button, InlineMessage, Loader, Text, useDevice } from '@deriv-com/ui'; +import ChatIcon from '../../../../public/ic-chat.svg'; +import { OrderDetailsCard } from '../../components/OrderDetailsCard'; +import { OrderDetailsCardFooter } from '../../components/OrderDetailsCard/OrderDetailsCardFooter'; import { OrdersChatSection } from '../OrdersChatSection'; +import './OrderDetails.scss'; -const OrderDetails = () => { +type TOrderDetailsProps = { + orderId: string; +}; + +const OrderDetails = ({ orderId }: TOrderDetailsProps) => { + const history = useHistory(); const location = useLocation(); - const urlParams = new URLSearchParams(location.search); - const orderId = urlParams.get('order') ?? ''; - const { data: orderInfo } = p2p.order.useGet(orderId); + const showChatParam = new URLSearchParams(location.search).get('showChat'); + const [showChat, setShowChat] = useState(!!showChatParam); + + const { data: orderInfo, failureReason, isError: isErrorOrderInfo, isLoading } = p2p.order.useGet(orderId); const { data: activeAccount } = useActiveAccount(); const { data: serverTime } = useServerTime(); const { data: orderDetails } = useExtendedOrderDetails({ @@ -16,14 +29,90 @@ const OrderDetails = () => { orderDetails: orderInfo, serverTime, }); + const { isBuyOrderForUser, shouldShowLostFundsBanner } = orderDetails; + const { isMobile } = useDevice(); + + const headerText = `${isBuyOrderForUser ? 'Buy' : 'Sell'} USD order`; + const warningMessage = 'Don’t risk your funds with cash transactions. Use bank transfers or e-wallets instead.'; + + const onReturn = () => history.goBack(); + const onChatReturn = () => { + setShowChat(false); + if (showChatParam) onReturn(); + }; + + if (isLoading) return ; + + // TODO: replace with proper error screen once design is ready + if (isErrorOrderInfo) return {failureReason?.error.message}; + + if (isMobile) { + return ( + + {showChat ? ( + + ) : ( + } + renderHeader={() => ( + + {headerText} + + + )} + > + {shouldShowLostFundsBanner && ( + + {warningMessage} + + )} + + + )} + + ); + } + return ( -
- -
+ +
+ +
+ {shouldShowLostFundsBanner && ( + + {warningMessage} + + )} +
+ + +
+
+
+
); }; diff --git a/packages/p2p-v2/src/pages/orders/screens/OrderDetails/__tests__/OrderDetails.spec.tsx b/packages/p2p-v2/src/pages/orders/screens/OrderDetails/__tests__/OrderDetails.spec.tsx new file mode 100644 index 000000000000..c1b42cb41f4a --- /dev/null +++ b/packages/p2p-v2/src/pages/orders/screens/OrderDetails/__tests__/OrderDetails.spec.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { useExtendedOrderDetails } from '@/hooks'; +import { p2p } from '@deriv/api-v2'; +import { useDevice } from '@deriv-com/ui'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import OrderDetails from '../OrderDetails'; + +const mockHistoryPush = jest.fn(); +let mockSearch = ''; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + goBack: mockHistoryPush, + }), + useLocation: () => ({ + search: mockSearch, + }), +})); + +jest.mock('@deriv-com/ui', () => ({ + ...jest.requireActual('@deriv-com/ui'), + useDevice: jest.fn().mockReturnValue({ isMobile: false }), +})); + +jest.mock('@deriv/api-v2', () => ({ + p2p: { + order: { + useGet: jest.fn().mockReturnValue({ + data: {}, + failureReason: {}, + isError: false, + isLoading: true, + }), + }, + }, + useActiveAccount: jest.fn(() => ({ + data: { + currency: 'USD', + }, + })), + useServerTime: jest.fn(() => ({ + data: { + server_time: 1626864000, + }, + })), +})); + +jest.mock('@/hooks', () => ({ + useExtendedOrderDetails: jest.fn().mockReturnValue({ + data: { + isBuyOrderForUser: true, + shouldShowLostFundsBanner: true, + }, + }), + useSendbird: () => ({ + isOnline: true, + lastOnlineTime: 123546789, + nickname: 'John Doe', + }), +})); + +jest.mock('../../../components/OrderDetailsCard', () => ({ + OrderDetailsCard: () =>
OrderDetailsCard
, +})); +jest.mock('../../../components/OrderDetailsCard/OrderDetailsCardFooter', () => ({ + OrderDetailsCardFooter: () =>
OrderDetailsCardFooter
, +})); +jest.mock('../../../components/ChatFooter', () => ({ + ChatFooter: () =>
ChatFooter
, +})); +jest.mock('../../../components/ChatMessages', () => ({ + ChatMessages: () =>
ChatMessages
, +})); + +const mockUseDevice = useDevice as jest.Mock; +const mockUseGet = p2p.order.useGet as jest.Mock; +const mockUseExtendedOrderDetails = useExtendedOrderDetails as jest.Mock; + +describe('', () => { + it('should show loading screen if isLoading is true', () => { + render(); + + expect(screen.getByTestId('dt_derivs-loader')).toBeInTheDocument(); + }); + + it('should render Desktop view if isMobile is false', () => { + mockUseGet.mockReturnValue({ + data: {}, + isLoading: false, + }); + + render(); + + expect(screen.getByText('Buy USD order')).toBeInTheDocument(); + expect( + screen.getByText('Don’t risk your funds with cash transactions. Use bank transfers or e-wallets instead.') + ).toBeInTheDocument(); + expect(screen.getByText('OrderDetailsCard')).toBeInTheDocument(); + expect(screen.getByText('ChatMessages')).toBeInTheDocument(); + expect(screen.getByText('ChatFooter')).toBeInTheDocument(); + }); + + it('should call goBack when back button is clicked', () => { + render(); + + const backButton = screen.getByTestId('dt_p2p_v2_page_return_btn'); + userEvent.click(backButton); + + expect(mockHistoryPush).toHaveBeenCalled(); + }); + + it('should render Mobile view if isMobile is true', () => { + mockUseDevice.mockReturnValue({ isMobile: true }); + + render(); + + expect(screen.getByText('Buy USD order')).toBeInTheDocument(); + expect(screen.getByText('OrderDetailsCard')).toBeInTheDocument(); + expect(screen.queryByText('ChatMessages')).not.toBeInTheDocument(); + expect(screen.queryByText('ChatFooter')).not.toBeInTheDocument(); + }); + + it('should show OrdersChatSection if Chat icon is clicked', () => { + render(); + + const chatButton = screen.getByTestId('dt_p2p_v2_order_details_chat_button'); + userEvent.click(chatButton); + + expect(screen.getByText('ChatMessages')).toBeInTheDocument(); + expect(screen.getByText('ChatFooter')).toBeInTheDocument(); + expect(screen.queryByText('OrderDetailsCard')).not.toBeInTheDocument(); + }); + + it('should call goBack when back button is clicked in mobile view and showChat is true in search param', () => { + mockSearch = '?showChat=true'; + + render(); + + const backButton = screen.getByTestId('dt_p2p_v2_mobile_wrapper_button'); + userEvent.click(backButton); + + expect(mockHistoryPush).toHaveBeenCalled(); + + mockSearch = ''; + }); + + it('should show Sell USD order if isBuyOrderForUser is false', () => { + mockUseExtendedOrderDetails.mockReturnValue({ + data: { + isBuyOrderForUser: false, + shouldShowLostFundsBanner: true, + }, + }); + + render(); + + expect(screen.getByText('Sell USD order')).toBeInTheDocument(); + }); + + it('should show error message if isError is true', () => { + mockUseGet.mockReturnValue({ + data: {}, + failureReason: { error: { message: 'error message' } }, + isError: true, + isLoading: false, + }); + + render(); + + expect(screen.getByText('error message')).toBeInTheDocument(); + }); +}); diff --git a/packages/p2p-v2/src/pages/orders/screens/Orders/Orders.tsx b/packages/p2p-v2/src/pages/orders/screens/Orders/Orders.tsx index b31c49c36236..15d4297e0c8b 100644 --- a/packages/p2p-v2/src/pages/orders/screens/Orders/Orders.tsx +++ b/packages/p2p-v2/src/pages/orders/screens/Orders/Orders.tsx @@ -1,12 +1,16 @@ import React from 'react'; +import { useLocation } from 'react-router-dom'; import { ORDERS_STATUS } from '@/constants'; import { useQueryString } from '@/hooks'; import { p2p } from '@deriv/api-v2'; import { Divider, useDevice } from '@deriv-com/ui'; +import { OrderDetails } from '../OrderDetails'; import { OrdersTable } from './OrdersTable'; import { OrdersTableHeader } from './OrdersTableHeader'; const Orders = () => { + const location = useLocation(); + const orderId = new URLSearchParams(location.search).get('order'); const { queryString } = useQueryString(); const { isMobile } = useDevice(); const currentTab = queryString.tab ?? ORDERS_STATUS.ACTIVE_ORDERS; @@ -16,7 +20,9 @@ const Orders = () => { isFetching, isLoading, loadMoreOrders, - } = p2p.order.useGetList({ active: currentTab === ORDERS_STATUS.ACTIVE_ORDERS ? 1 : 0 }); + } = p2p.order.useGetList({ active: currentTab === ORDERS_STATUS.ACTIVE_ORDERS ? 1 : 0 }, { enabled: !orderId }); + + if (orderId) return ; return ( <> diff --git a/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTable/OrdersTable.scss b/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTable/OrdersTable.scss index 5227354c2521..20ffefb628c2 100644 --- a/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTable/OrdersTable.scss +++ b/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTable/OrdersTable.scss @@ -4,8 +4,14 @@ grid-template-columns: 1fr 1.5fr 2fr 3fr 1.5fr 1.5fr 1.5fr; } - &__content__row { - border-bottom: 1px solid #f2f3f4; + &__content { + @include mobile { + height: calc(100vh - 19.5rem); + } + + &__row { + border-bottom: 1px solid #f2f3f4; + } } } diff --git a/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.scss b/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.scss index afd67fb5efe2..3c82d35a5365 100644 --- a/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.scss +++ b/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.scss @@ -2,6 +2,8 @@ display: grid; align-items: center; grid-template-columns: 1fr 1.5fr 2fr 3fr 1.5fr 1.5fr 1.5fr; + cursor: pointer; + &--inactive { grid-template-columns: 2fr 1fr 1.5fr 2fr 3fr 1.5fr 1.5fr 2fr; gap: 1rem; diff --git a/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.tsx b/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.tsx index 5132489d711d..ec7370b9d64f 100644 --- a/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.tsx +++ b/packages/p2p-v2/src/pages/orders/screens/Orders/OrdersTableRow/OrdersTableRow.tsx @@ -43,10 +43,11 @@ const OrdersTableRow = ({ ...props }: THooks.Order.GetList[number]) => { const isBuyOrderForUser = orderDetails.isBuyOrderForUser; const transactionAmount = `${Number(priceDisplay).toFixed(2)} ${localCurrency}`; const offerAmount = `${amountDisplay} ${accountCurrency}`; + const showOrderDetails = () => history.push(`${BASE_URL}/orders?order=${id}`); if (isMobile) { return ( -
+
{