diff --git a/.github/workflows/sync-translations.yml b/.github/workflows/sync-translations.yml index 305086e1..36929960 100644 --- a/.github/workflows/sync-translations.yml +++ b/.github/workflows/sync-translations.yml @@ -4,6 +4,8 @@ on: push: branches: - 'master' + schedule: + - cron: '0 */12 * * *' jobs: sync_translations: diff --git a/__mocks__/LocalizeMock.js b/__mocks__/LocalizeMock.js index 869dff9e..cc9782d0 100644 --- a/__mocks__/LocalizeMock.js +++ b/__mocks__/LocalizeMock.js @@ -1,5 +1,5 @@ -import React from 'react'; +const Localize = ({ i18n_default_text }) => { + return i18n_default_text || null; +}; -const Localize = () =>
Mock Localize
; - -export default Localize; +export { Localize }; diff --git a/jest.config.cjs b/jest.config.cjs index 4698df4d..16ec02d7 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,7 +1,8 @@ module.exports = { moduleNameMapper: { - '@deriv-com/(.*)': '/node_modules/@deriv-com/$1', '@deriv-com/translations': '/__mocks__/LocalizeMock.js', + // eslint-disable-next-line sort-keys + '@deriv-com/(.*)': '/node_modules/@deriv-com/$1', '\\.(css|less|sass|scss)$': 'identity-obj-proxy', '\\.svg': '/__mocks__/svgMock.js', '^@/(.*)$': '/src/$1', diff --git a/src/components/AdvertiserName/__tests__/AdvertiserNameStats.spec.tsx b/src/components/AdvertiserName/__tests__/AdvertiserNameStats.spec.tsx index 2fd80d6e..a7d4a914 100644 --- a/src/components/AdvertiserName/__tests__/AdvertiserNameStats.spec.tsx +++ b/src/components/AdvertiserName/__tests__/AdvertiserNameStats.spec.tsx @@ -22,16 +22,15 @@ describe('AdvertiserNameStats', () => { expect(screen.getByText('(29 ratings)')).toBeInTheDocument(); }); - // TODO: uncomment this once Localize has been fixed for test cases - // it('should render correct advertiser stats based on availability', () => { - // const mockUseAdvertiserStats = { - // advertiserStats: { - // blocked_by_count: 1, - // daysSinceJoined: 22, - // rating_count: 29, - // }, - // }; - // render(); - // expect(screen.getByText('Not rated yet')).toBeInTheDocument(); - // }); + it('should render correct advertiser stats based on availability', () => { + const mockUseAdvertiserStats = { + advertiserStats: { + blocked_by_count: 1, + daysSinceJoined: 22, + rating_count: 29, + }, + }; + render(); + expect(screen.getByText('Not rated yet')).toBeInTheDocument(); + }); }); diff --git a/src/components/AdvertsTableRow/AdvertsTableRow.tsx b/src/components/AdvertsTableRow/AdvertsTableRow.tsx index 1bbc74ec..656188b1 100644 --- a/src/components/AdvertsTableRow/AdvertsTableRow.tsx +++ b/src/components/AdvertsTableRow/AdvertsTableRow.tsx @@ -10,6 +10,7 @@ import { useIsAdvertiser, useIsAdvertiserBarred, useModalManager } from '@/hooks import { generateEffectiveRate, getCurrentRoute } from '@/utils'; import { LabelPairedChevronRightMdRegularIcon } from '@deriv/quill-icons'; import { useExchangeRates } from '@deriv-com/api-hooks'; +import { Localize } from '@deriv-com/translations'; import { Button, Text, useDevice } from '@deriv-com/ui'; import './AdvertsTableRow.scss'; @@ -125,7 +126,7 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { ) : ( - Not rated yet + )} 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/AdCreateEditErrorModal/AdCreateEditErrorModal.tsx b/src/components/Modals/AdCreateEditErrorModal/AdCreateEditErrorModal.tsx index 52552c6a..237aeab6 100644 --- a/src/components/Modals/AdCreateEditErrorModal/AdCreateEditErrorModal.tsx +++ b/src/components/Modals/AdCreateEditErrorModal/AdCreateEditErrorModal.tsx @@ -12,20 +12,15 @@ type TAdCreateEditErrorModalProps = { type ErrorContent = { [key in TErrorCodes]?: { - description: string; title: string; }; }; const errorContent: ErrorContent = { [ERROR_CODES.ADVERT_SAME_LIMITS]: { - description: - 'Please set a different minimum and/or maximum order limit. \n\nThe range of your ad should not overlap with any of your active ads.', title: 'You already have an ad with this range', }, [ERROR_CODES.DUPLICATE_ADVERT]: { - description: - 'You already have an ad with the same exchange rate for this currency pair and order type. \n\nPlease set a different rate for your ad.', title: 'You already have an ad with this rate', }, }; @@ -49,7 +44,7 @@ const AdCreateEditErrorModal = ({ {(errorCode && errorContent?.[errorCode]?.title) ?? 'Something’s not right'} - {(errorCode && errorContent?.[errorCode]?.description) ?? errorMessage} + {errorMessage} -
); }; 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/Orders/OrdersEmpty/OrdersEmpty.tsx b/src/pages/orders/screens/Orders/OrdersEmpty/OrdersEmpty.tsx index 7b12c7cb..deae7b5a 100644 --- a/src/pages/orders/screens/Orders/OrdersEmpty/OrdersEmpty.tsx +++ b/src/pages/orders/screens/Orders/OrdersEmpty/OrdersEmpty.tsx @@ -1,6 +1,7 @@ import { useHistory } from 'react-router-dom'; import { BUY_SELL_URL } from '@/constants'; import { DerivLightOrderIcon } from '@deriv/quill-icons'; +import { Localize } from '@deriv-com/translations'; import { ActionScreen, Button, Text, useDevice } from '@deriv-com/ui'; const OrdersEmpty = () => { @@ -18,7 +19,7 @@ const OrdersEmpty = () => { icon={} title={ - You have no orders. + } /> 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/src/utils/ad-utils.ts b/src/utils/ad-utils.ts index 673b442c..bbf7bd95 100644 --- a/src/utils/ad-utils.ts +++ b/src/utils/ad-utils.ts @@ -53,7 +53,8 @@ const decimalPointValidation = (value: string) => 'Only up to 2 decimals are allowed.'; export const getValidationRules = ( fieldName: string, - getValues: (fieldName: string) => number | string + getValues: (fieldName: string) => number | string, + localize: (key: string) => string ): ValidationRules => { switch (fieldName) { case 'amount': @@ -79,7 +80,7 @@ export const getValidationRules = ( case 'rate-value': return { validation_1: value => requiredValidation(value, 'Fixed rate'), - validation_2: value => !isNaN(Number(value)) || 'Enter a valid amount', + validation_2: value => !isNaN(Number(value)) || localize('Enter a valid amount'), validation_3: value => { if (getValues('rate-type-string') === RATE_TYPE.FIXED) { return decimalPointValidation(value); 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']>;