From 230f15cdf2bdc9248bd0b7cff79cde6e809e0fa8 Mon Sep 17 00:00:00 2001 From: ameerul-deriv <103412909+ameerul-deriv@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:52:56 +0800 Subject: [PATCH] [FEQ] / Ameerul / FEQ-1969 Refactor useAdvertiserInfo hook (#14611) * chore: refactored useAdvertiserInfo hook * fix: failing test cases * fix: payload type and useAdvertiserStats test case * fix: sonar cloud issue * fix: online status always online in chat, minor Type errors * chore: added comments and did minor cleanups * chore: added useIsAdvertiser hook, fixed handling useAdvertiserInfo states issue * fix: reverted removal of provider, fixed issues with verification flow * chore: added tsdocs --- .../p2p-advertiser/useAdvertiserInfo.ts | 122 ++++++++++++------ .../useAdvertiserPaymentMethods.ts | 6 +- .../AdvertiserName/AdvertiserName.tsx | 4 +- .../AdvertiserName/AdvertiserNameBadges.tsx | 2 +- .../AdvertiserName/AdvertiserNameStats.tsx | 11 +- .../AdvertiserName/AdvertiserNameToggle.tsx | 6 +- .../__tests__/AdvertiserName.spec.tsx | 2 +- .../__tests__/AdvertiserNameToggle.spec.tsx | 2 +- .../AdvertsTableRow/AdvertsTableRow.tsx | 4 +- .../FilterModalPaymentMethods.tsx | 2 +- .../Modals/NicknameModal/NicknameModal.tsx | 7 +- .../__tests__/NicknameModal.spec.tsx | 13 +- .../OnlineStatus/OnlineStatusLabel.tsx | 2 +- .../ProfileBalance/ProfileBalance.tsx | 2 +- .../__tests__/ProfileBalance.spec.tsx | 10 +- .../ProfileStats/ProfileStats.tsx | 7 +- .../p2p-v2/src/constants/api-error-codes.ts | 1 + .../__tests__/useAdvertiserStats.spec.tsx | 119 ++++++++++------- .../hooks/__tests__/useIsAdvertiser.spec.tsx | 41 ++++++ packages/p2p-v2/src/hooks/index.ts | 1 + .../p2p-v2/src/hooks/useAdvertiserStats.ts | 48 +++++-- packages/p2p-v2/src/hooks/useIsAdvertiser.ts | 25 ++++ packages/p2p-v2/src/hooks/useSendbird.ts | 10 +- .../screens/Advertiser/Advertiser.tsx | 8 +- .../Advertiser/__tests__/Advertiser.spec.tsx | 28 +++- .../SellAdPaymentSelection.tsx | 4 +- .../__tests__/SellAdPaymentSelection.spec.tsx | 5 + .../screens/MyAds/MyAdsTable/MyAdsTable.tsx | 37 ++++-- .../screens/MyProfile/MyProfile.tsx | 9 +- .../MyProfile/__tests__/MyProfile.spec.tsx | 10 +- .../screens/MyProfileStats/MyProfileStats.tsx | 2 +- .../screens/PaymentMethods/PaymentMethods.tsx | 4 +- .../__tests__/PaymentMethods.spec.tsx | 5 + .../components/ChatHeader/ChatHeader.tsx | 2 +- .../AdvertiserInfoStateProvider.tsx | 32 +++++ .../AdvertiserInfoStateProvider/index.ts | 1 + .../AppContent/__tests__/AppContent.spec.tsx | 9 ++ .../p2p-v2/src/routes/AppContent/index.tsx | 44 +++++-- packages/p2p-v2/src/utils/time.ts | 4 +- 39 files changed, 474 insertions(+), 177 deletions(-) create mode 100644 packages/p2p-v2/src/hooks/__tests__/useIsAdvertiser.spec.tsx create mode 100644 packages/p2p-v2/src/hooks/useIsAdvertiser.ts create mode 100644 packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/AdvertiserInfoStateProvider.tsx create mode 100644 packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/index.ts diff --git a/packages/api-v2/src/hooks/p2p/entity/advertiser/p2p-advertiser/useAdvertiserInfo.ts b/packages/api-v2/src/hooks/p2p/entity/advertiser/p2p-advertiser/useAdvertiserInfo.ts index 2a60aa42ecc0..6479d231a2f1 100644 --- a/packages/api-v2/src/hooks/p2p/entity/advertiser/p2p-advertiser/useAdvertiserInfo.ts +++ b/packages/api-v2/src/hooks/p2p/entity/advertiser/p2p-advertiser/useAdvertiserInfo.ts @@ -1,53 +1,91 @@ -import { useMemo } from 'react'; -import useQuery from '../../../../../useQuery'; -import useAuthorize from '../../../../useAuthorize'; +import { useCallback, useEffect } from 'react'; +import { useLocalStorage } from 'usehooks-ts'; +import useSubscription from '../../../../../useSubscription'; +import { TSocketRequestPayload, TSocketResponseData } from '../../../../../../types'; + +type TP2PAdvertiserInfo = TSocketResponseData<'p2p_advertiser_info'>['p2p_advertiser_info'] & { + has_basic_verification: boolean; + has_full_verification: boolean; + is_approved_boolean: boolean; + is_blocked_boolean: boolean; + is_favourite_boolean: boolean; + is_listed_boolean: boolean; + is_online_boolean: boolean; + should_show_name: boolean; +}; + +type TPayload = NonNullable> & { id?: string }; /** This custom hook returns information about the given advertiser ID */ const useAdvertiserInfo = (id?: string) => { - const { isSuccess } = useAuthorize(); - const { data, ...rest } = useQuery('p2p_advertiser_info', { payload: { id }, options: { enabled: isSuccess } }); + const { data, error, subscribe: subscribeAdvertiserInfo, ...rest } = useSubscription('p2p_advertiser_info'); + + /** + * Use different local storage key for each advertiser, one to keep the current user's info, the other to keep the advertiser's info + * This is to prevent the current user's info from being overwritten by the advertiser's info when the current user views the advertiser's profile. + * + * Key removal is handled in useAdvertiserStats hook's useEffect. + * */ + const local_storage_key = id ? `p2p_v2_p2p_advertiser_info_${id}` : 'p2p_v2_p2p_advertiser_info'; + const [p2p_advertiser_info, setP2PAdvertiserInfo] = useLocalStorage>( + local_storage_key, + {} + ); + + const subscribe = useCallback( + (payload?: TPayload) => { + subscribeAdvertiserInfo({ payload }); + }, + [subscribeAdvertiserInfo] + ); // Add additional information to the p2p_advertiser_info data - const modified_data = useMemo(() => { - const advertiser_info = data?.p2p_advertiser_info; - - if (!advertiser_info) return undefined; - - const { - basic_verification, - full_verification, - is_approved, - is_blocked, - is_favourite, - is_listed, - is_online, - show_name, - } = advertiser_info; - - return { - ...advertiser_info, - /** Indicating whether the advertiser's identify has been verified. */ - basic_verification: Boolean(basic_verification), - /** Indicating whether the advertiser's address has been verified. */ - full_verification: Boolean(full_verification), - /** The approval status of the advertiser. */ - is_approved: Boolean(is_approved), - /** Indicates that the advertiser is blocked by the current user. */ - is_blocked: Boolean(is_blocked), - /** Indicates that the advertiser is a favourite of the current user. */ - is_favourite: Boolean(is_favourite), - /** Indicates if the advertiser's active adverts are listed. When false, adverts won't be listed regardless if they are active or not. */ - is_listed: Boolean(is_listed), - /** Indicates if the advertiser is currently online. */ - is_online: Boolean(is_online), - /** When true, the advertiser's real name will be displayed on to other users on adverts and orders. */ - show_name: Boolean(show_name), - }; - }, [data?.p2p_advertiser_info]); + useEffect(() => { + if (data) { + const advertiser_info = data?.p2p_advertiser_info; + + if (!advertiser_info) return; + + const { + basic_verification, + full_verification, + is_approved, + is_blocked, + is_favourite, + is_listed, + is_online, + show_name, + } = advertiser_info; + + setP2PAdvertiserInfo({ + ...advertiser_info, + /** Indicating whether the advertiser's identify has been verified. */ + has_basic_verification: Boolean(basic_verification), + /** Indicating whether the advertiser's address has been verified. */ + has_full_verification: Boolean(full_verification), + /** The approval status of the advertiser. */ + is_approved_boolean: Boolean(is_approved), + /** Indicates that the advertiser is blocked by the current user. */ + is_blocked_boolean: Boolean(is_blocked), + /** Indicates that the advertiser is a favourite of the current user. */ + is_favourite_boolean: Boolean(is_favourite), + /** Indicates if the advertiser's active adverts are listed. When false, adverts won't be listed regardless if they are active or not. */ + is_listed_boolean: Boolean(is_listed), + /** Indicates if the advertiser is currently online. */ + is_online_boolean: Boolean(is_online), + /** When true, the advertiser's real name will be displayed on to other users on adverts and orders. */ + should_show_name: Boolean(show_name), + }); + } else if (error) { + setP2PAdvertiserInfo({}); + } + }, [data, error, setP2PAdvertiserInfo]); return { /** P2P advertiser information */ - data: modified_data, + data: p2p_advertiser_info, + error, + subscribe, ...rest, }; }; diff --git a/packages/api-v2/src/hooks/p2p/entity/payment-method/p2p-advertiser-payment-methods/useAdvertiserPaymentMethods.ts b/packages/api-v2/src/hooks/p2p/entity/payment-method/p2p-advertiser-payment-methods/useAdvertiserPaymentMethods.ts index be50f9c0f97c..0f2890e2db32 100644 --- a/packages/api-v2/src/hooks/p2p/entity/payment-method/p2p-advertiser-payment-methods/useAdvertiserPaymentMethods.ts +++ b/packages/api-v2/src/hooks/p2p/entity/payment-method/p2p-advertiser-payment-methods/useAdvertiserPaymentMethods.ts @@ -3,9 +3,11 @@ import useAuthorize from '../../../../useAuthorize'; import useQuery from '../../../../../useQuery'; /** A custom hook that returns the list of P2P Advertiser Payment Methods */ -const useAdvertiserPaymentMethods = () => { +const useAdvertiserPaymentMethods = (is_enabled = true) => { const { isSuccess } = useAuthorize(); - const { data, ...rest } = useQuery('p2p_advertiser_payment_methods', { options: { enabled: isSuccess } }); + const { data, ...rest } = useQuery('p2p_advertiser_payment_methods', { + options: { enabled: isSuccess && is_enabled }, + }); // Modify the response to add additional information const modified_data = useMemo(() => { diff --git a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserName.tsx b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserName.tsx index 1607558fa986..36095729db1c 100644 --- a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserName.tsx +++ b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserName.tsx @@ -10,7 +10,7 @@ import AdvertiserNameStats from './AdvertiserNameStats'; import AdvertiserNameToggle from './AdvertiserNameToggle'; import './AdvertiserName.scss'; -const AdvertiserName = ({ advertiserStats }: { advertiserStats: TAdvertiserStats }) => { +const AdvertiserName = ({ advertiserStats }: { advertiserStats: DeepPartial }) => { const { data: { email }, } = useSettings(); @@ -27,7 +27,7 @@ const AdvertiserName = ({ advertiserStats }: { advertiserStats: TAdvertiserStats {name} - {(advertiserStats?.show_name || !isMyProfile) && ( + {(advertiserStats?.should_show_name || !isMyProfile) && ( ({advertiserStats?.fullName}) diff --git a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameBadges.tsx b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameBadges.tsx index 6e002044b8d2..1ac6d70ef10e 100644 --- a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameBadges.tsx +++ b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameBadges.tsx @@ -9,7 +9,7 @@ import './AdvertiserNameBadges.scss'; * * Use cases are usually in My Profile page and Advertiser page used under the advertiser's name */ -const AdvertiserNameBadges = ({ advertiserStats }: { advertiserStats: TAdvertiserStats }) => { +const AdvertiserNameBadges = ({ advertiserStats }: { advertiserStats: DeepPartial }) => { const { isAddressVerified, isIdentityVerified, totalOrders } = advertiserStats || {}; return ( diff --git a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameStats.tsx b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameStats.tsx index 2042646a958f..464ee9836454 100644 --- a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameStats.tsx +++ b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameStats.tsx @@ -15,7 +15,7 @@ import './AdvertiserNameStats.scss'; * * Use cases are to show this in My Profile and Advertiser page */ -const AdvertiserNameStats = ({ advertiserStats }: { advertiserStats: TAdvertiserStats }) => { +const AdvertiserNameStats = ({ advertiserStats }: { advertiserStats: DeepPartial }) => { const { isMobile } = useDevice(); const isMyProfile = getCurrentRoute() === 'my-profile'; @@ -39,12 +39,15 @@ const AdvertiserNameStats = ({ advertiserStats }: { advertiserStats: TAdvertiser
{!isMyProfile && (
- - + +
)} - Joined {daysSinceJoined}d + Joined {daysSinceJoined && daysSinceJoined > 0 ? `${daysSinceJoined}d` : 'Today'}
{!ratingAverage && ( diff --git a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameToggle.tsx b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameToggle.tsx index c41c3d0d0e85..301fa74bd9ba 100644 --- a/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameToggle.tsx +++ b/packages/p2p-v2/src/components/AdvertiserName/AdvertiserNameToggle.tsx @@ -5,7 +5,7 @@ import { Text, ToggleSwitch } from '@deriv-com/ui'; import './AdvertiserNameToggle.scss'; type TAdvertiserNameToggle = { - advertiserInfo: TAdvertiserStats; + advertiserInfo: DeepPartial; onToggle?: (shouldShowRealName: boolean) => void; }; const AdvertiserNameToggle = memo(({ advertiserInfo, onToggle }: TAdvertiserNameToggle) => { @@ -13,8 +13,8 @@ const AdvertiserNameToggle = memo(({ advertiserInfo, onToggle }: TAdvertiserName const { mutate: advertiserUpdate } = p2p.advertiser.useUpdate(); useEffect(() => { - setShouldShowRealName(advertiserInfo?.show_name || false); - }, [advertiserInfo?.show_name]); + setShouldShowRealName(advertiserInfo?.should_show_name || false); + }, [advertiserInfo?.should_show_name]); const onToggleShowRealName = () => { advertiserUpdate({ diff --git a/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx b/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx index 32e5eb00d31d..cc1e70509e2d 100644 --- a/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx +++ b/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx @@ -13,7 +13,7 @@ const mockProps = { advertiserStats: { fullName: 'Jane Doe', name: 'Jane', - show_name: 1, + should_show_name: true, }, }; diff --git a/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserNameToggle.spec.tsx b/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserNameToggle.spec.tsx index 7b6404dcb447..98d1570ee94a 100644 --- a/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserNameToggle.spec.tsx +++ b/packages/p2p-v2/src/components/AdvertiserName/__tests__/AdvertiserNameToggle.spec.tsx @@ -12,7 +12,7 @@ const wrapper = ({ children }: { children: JSX.Element }) => ( const mockProps = { advertiserInfo: { fullName: 'Jane Doe', - show_name: 0, + should_show_name: false, }, onToggle: jest.fn(), }; diff --git a/packages/p2p-v2/src/components/AdvertsTableRow/AdvertsTableRow.tsx b/packages/p2p-v2/src/components/AdvertsTableRow/AdvertsTableRow.tsx index c1205ef7153a..1212d665e21f 100644 --- a/packages/p2p-v2/src/components/AdvertsTableRow/AdvertsTableRow.tsx +++ b/packages/p2p-v2/src/components/AdvertsTableRow/AdvertsTableRow.tsx @@ -5,6 +5,7 @@ import { useHistory } from 'react-router-dom'; import { TAdvertsTableRowRenderer } from 'types'; import { Badge, BuySellForm, PaymentMethodLabel, StarRating, UserAvatar } from '@/components'; import { ADVERTISER_URL, BUY_SELL } from '@/constants'; +import { useIsAdvertiser } from '@/hooks'; import { generateEffectiveRate, getCurrentRoute } from '@/utils'; import { p2p, useExchangeRateSubscription } from '@deriv/api-v2'; import { LabelPairedChevronRightMdRegularIcon } from '@deriv/quill-icons'; @@ -20,8 +21,9 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { const history = useHistory(); const isBuySellPage = getCurrentRoute() === 'buy-sell'; + const isAdvertiser = useIsAdvertiser(); const { data: paymentMethods } = p2p.paymentMethods.useGet(); - const { data: advertiserPaymentMethods } = p2p.advertiserPaymentMethods.useGet(); + const { data: advertiserPaymentMethods } = p2p.advertiserPaymentMethods.useGet(isAdvertiser); const { data } = p2p.advertiser.useGetInfo() || {}; const { daily_buy = 0, daily_buy_limit = 0, daily_sell = 0, daily_sell_limit = 0 } = data || {}; diff --git a/packages/p2p-v2/src/components/Modals/FilterModal/FilterModalPaymentMethods/FilterModalPaymentMethods.tsx b/packages/p2p-v2/src/components/Modals/FilterModal/FilterModalPaymentMethods/FilterModalPaymentMethods.tsx index e32a8480f90c..682ab3ce501e 100644 --- a/packages/p2p-v2/src/components/Modals/FilterModal/FilterModalPaymentMethods/FilterModalPaymentMethods.tsx +++ b/packages/p2p-v2/src/components/Modals/FilterModal/FilterModalPaymentMethods/FilterModalPaymentMethods.tsx @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; +import { THooks } from 'types'; import { Search } from '@/components/Search'; import { p2p } from '@deriv/api-v2'; import { Checkbox, Text } from '@deriv-com/ui'; import './FilterModalPaymentMethods.scss'; -import { THooks } from 'types'; type TFilterModalPaymentMethodsProps = { selectedPaymentMethods: string[]; diff --git a/packages/p2p-v2/src/components/Modals/NicknameModal/NicknameModal.tsx b/packages/p2p-v2/src/components/Modals/NicknameModal/NicknameModal.tsx index 67754535e27e..fa9e386cc5ca 100644 --- a/packages/p2p-v2/src/components/Modals/NicknameModal/NicknameModal.tsx +++ b/packages/p2p-v2/src/components/Modals/NicknameModal/NicknameModal.tsx @@ -3,6 +3,7 @@ import { debounce } from 'lodash'; import { Controller, useForm } from 'react-hook-form'; import { useHistory } from 'react-router-dom'; import { BUY_SELL_URL } from '@/constants'; +import { useAdvertiserInfoState } from '@/providers/AdvertiserInfoStateProvider'; import { p2p } from '@deriv/api-v2'; import { DerivLightIcCashierUserIcon } from '@deriv/quill-icons'; import { Button, Input, Modal, Text, useDevice } from '@deriv-com/ui'; @@ -28,6 +29,7 @@ const NicknameModal = ({ isModalOpen, setIsModalOpen }: TNicknameModalProps) => const history = useHistory(); const { error: createError, isError, isSuccess, mutate, reset } = p2p.advertiser.useCreate(); + const { setHasCreatedAdvertiser } = useAdvertiserInfoState(); const { isMobile } = useDevice(); const textSize = isMobile ? 'md' : 'sm'; const debouncedReset = debounce(reset, 3000); @@ -39,13 +41,14 @@ const NicknameModal = ({ isModalOpen, setIsModalOpen }: TNicknameModalProps) => useEffect(() => { if (isSuccess) { setIsModalOpen(false); + setHasCreatedAdvertiser(true); } else if (isError) { debouncedReset(); } - }, [isError, isSuccess]); + }, [isError, isSuccess, setHasCreatedAdvertiser]); return ( - +
diff --git a/packages/p2p-v2/src/components/Modals/NicknameModal/__tests__/NicknameModal.spec.tsx b/packages/p2p-v2/src/components/Modals/NicknameModal/__tests__/NicknameModal.spec.tsx index 045dad1ff56d..3d72079e5d7d 100644 --- a/packages/p2p-v2/src/components/Modals/NicknameModal/__tests__/NicknameModal.spec.tsx +++ b/packages/p2p-v2/src/components/Modals/NicknameModal/__tests__/NicknameModal.spec.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { useAdvertiserInfoState } from '@/providers/AdvertiserInfoStateProvider'; import { APIProvider, AuthProvider, p2p } from '@deriv/api-v2'; import { act, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -17,6 +18,7 @@ const mockedMutate = jest.fn(); const mockedReset = jest.fn(); const mockedUseAdvertiserCreate = p2p.advertiser.useCreate as jest.MockedFunction; const mockPush = jest.fn(); +const mockUseAdvertiserInfoState = useAdvertiserInfoState as jest.MockedFunction; jest.mock('lodash', () => ({ ...jest.requireActual('lodash'), @@ -52,6 +54,12 @@ jest.mock('@deriv/api-v2', () => ({ }, })); +jest.mock('@/providers/AdvertiserInfoStateProvider', () => ({ + useAdvertiserInfoState: jest.fn().mockReturnValue({ + setHasCreatedAdvertiser: jest.fn(), + }), +})); + describe('NicknameModal', () => { it('should render title and description correctly', () => { render(, { wrapper }); @@ -75,9 +83,10 @@ describe('NicknameModal', () => { expect(mockedMutate).toHaveBeenCalledWith({ name: 'Nahida', }); + expect(mockUseAdvertiserInfoState().setHasCreatedAdvertiser).toBeCalledWith(true); }); it('should invoke reset when there is an error from creating advertiser', async () => { - mockedUseAdvertiserCreate.mockImplementationOnce(() => ({ + (mockedUseAdvertiserCreate as jest.Mock).mockImplementationOnce(() => ({ error: undefined, isError: true, isSuccess: false, @@ -92,7 +101,7 @@ describe('NicknameModal', () => { expect(mockedReset).toBeCalled(); }); it('should close the modal when Cancel button is clicked', async () => { - mockedUseAdvertiserCreate.mockImplementationOnce(() => ({ + (mockedUseAdvertiserCreate as jest.Mock).mockImplementationOnce(() => ({ error: undefined, isError: false, isSuccess: true, diff --git a/packages/p2p-v2/src/components/OnlineStatus/OnlineStatusLabel.tsx b/packages/p2p-v2/src/components/OnlineStatus/OnlineStatusLabel.tsx index 787ab5213ae2..9f4d51c3b29e 100644 --- a/packages/p2p-v2/src/components/OnlineStatus/OnlineStatusLabel.tsx +++ b/packages/p2p-v2/src/components/OnlineStatus/OnlineStatusLabel.tsx @@ -4,7 +4,7 @@ import { Text } from '@deriv-com/ui'; type TOnlineStatusLabelProps = { isOnline?: boolean; - lastOnlineTime: number; + lastOnlineTime?: number; }; const OnlineStatusLabel = ({ isOnline = false, lastOnlineTime }: TOnlineStatusLabelProps) => { diff --git a/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/ProfileBalance.tsx b/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/ProfileBalance.tsx index 8c89182430f8..68fb8d0bafb7 100644 --- a/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/ProfileBalance.tsx +++ b/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/ProfileBalance.tsx @@ -9,7 +9,7 @@ import { Text } from '@deriv-com/ui'; import { ProfileDailyLimit } from '../ProfileDailyLimit'; import './ProfileBalance.scss'; -const ProfileBalance = ({ advertiserStats }: { advertiserStats: TAdvertiserStats }) => { +const ProfileBalance = ({ advertiserStats }: { advertiserStats: DeepPartial }) => { const { data: activeAccount } = useActiveAccount(); const { isDesktop } = useDevice(); const [shouldShowAvailableBalanceModal, setShouldShowAvailableBalanceModal] = useState(false); diff --git a/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/__tests__/ProfileBalance.spec.tsx b/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/__tests__/ProfileBalance.spec.tsx index ffbaa2bf8369..aa8368e9e4f8 100644 --- a/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/__tests__/ProfileBalance.spec.tsx +++ b/packages/p2p-v2/src/components/ProfileContent/ProfileBalance/__tests__/ProfileBalance.spec.tsx @@ -16,14 +16,14 @@ const wrapper = ({ children }: { children: JSX.Element }) => ( let mockAdvertiserStatsProp = { advertiserStats: { balance_available: 50000, - daily_buy_limit: 500, - daily_sell_limit: 500, + daily_buy_limit: '500', + daily_sell_limit: '500', dailyAvailableBuyLimit: 10, dailyAvailableSellLimit: 10, fullName: 'Jane Doe', isEligibleForLimitUpgrade: false, name: 'Jane', - show_name: 0, + should_show_name: false, }, }; const mockUseActiveAccount = { @@ -58,8 +58,8 @@ describe('ProfileBalance', () => { mockAdvertiserStatsProp = { advertiserStats: { ...mockAdvertiserStatsProp.advertiserStats, - daily_buy_limit: 500, - daily_sell_limit: 2000, + daily_buy_limit: '500', + daily_sell_limit: '2000', dailyAvailableBuyLimit: 100, dailyAvailableSellLimit: 600, }, diff --git a/packages/p2p-v2/src/components/ProfileContent/ProfileStats/ProfileStats.tsx b/packages/p2p-v2/src/components/ProfileContent/ProfileStats/ProfileStats.tsx index 3916b19325e9..be3f15cec5ca 100644 --- a/packages/p2p-v2/src/components/ProfileContent/ProfileStats/ProfileStats.tsx +++ b/packages/p2p-v2/src/components/ProfileContent/ProfileStats/ProfileStats.tsx @@ -24,13 +24,14 @@ const ProfileStats = ({ advertiserStats }: { advertiserStats: Partial 0 ? `${buyCompletionRate}% (${buyOrdersCount})` : '-', + value: buyCompletionRate && buyCompletionRate > 0 ? `${buyCompletionRate}% (${buyOrdersCount})` : '-', }, { text: 'Sell completion 30d', - value: sellCompletionRate > 0 ? `${sellCompletionRate}% (${sellOrdersCount})` : '-', + value: + sellCompletionRate && sellCompletionRate > 0 ? `${sellCompletionRate}% (${sellOrdersCount})` : '-', }, - { text: 'Trade volume 30d', value: `${tradeVolume.toFixed(2)} USD` }, + { text: 'Trade volume 30d', value: `${tradeVolume ? tradeVolume.toFixed(2) : '0.00'} USD` }, { text: 'Avg pay time 30d', value: averagePayTime !== -1 ? `${averagePayTime} min` : '-' }, { text: 'Avg release time 30d', value: averageReleaseTime !== -1 ? `${averageReleaseTime} min` : '-' }, { text: 'Trade partners', value: tradePartners }, diff --git a/packages/p2p-v2/src/constants/api-error-codes.ts b/packages/p2p-v2/src/constants/api-error-codes.ts index 085a58071982..7cbf7e523346 100644 --- a/packages/p2p-v2/src/constants/api-error-codes.ts +++ b/packages/p2p-v2/src/constants/api-error-codes.ts @@ -7,6 +7,7 @@ export const ERROR_CODES = { ADVERT_REMAINING: 'advert_remaining', ADVERT_SAME_LIMITS: 'AdvertSameLimits', ADVERTISER_ADS_PAUSED: 'advertiser_ads_paused', + ADVERTISER_NOT_FOUND: 'AdvertiserNotFound', ADVERTISER_TEMP_BAN: 'advertiser_temp_ban', DUPLICATE_ADVERT: 'DuplicateAdvert', } as const; diff --git a/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx b/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx index 6a8650e44970..8c66987778d0 100644 --- a/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx +++ b/packages/p2p-v2/src/hooks/__tests__/useAdvertiserStats.spec.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; -import { APIProvider, AuthProvider, p2p, useAuthentication, useSettings } from '@deriv/api-v2'; +import { APIProvider, AuthProvider, p2p, useAuthentication, useAuthorize, useSettings } from '@deriv/api-v2'; import { renderHook } from '@testing-library/react-hooks'; import useAdvertiserStats from '../useAdvertiserStats'; const mockUseSettings = useSettings as jest.MockedFunction; const mockUseAuthentication = useAuthentication as jest.MockedFunction; const mockUseAdvertiserInfo = p2p.advertiser.useGetInfo as jest.MockedFunction; +const mockUseAuthorize = useAuthorize as jest.MockedFunction; jest.mock('@deriv/api-v2', () => ({ ...jest.requireActual('@deriv/api-v2'), @@ -16,7 +17,8 @@ jest.mock('@deriv/api-v2', () => ({ currency: 'USD', }, isLoading: false, - isSuccess: true, + subscribe: jest.fn(), + unsubscribe: jest.fn(), }), }, }, @@ -27,6 +29,9 @@ jest.mock('@deriv/api-v2', () => ({ isLoading: false, isSuccess: true, }), + useAuthorize: jest.fn().mockReturnValue({ + isSuccess: false, + }), useSettings: jest.fn().mockReturnValue({ data: { currency: 'USD', @@ -38,18 +43,19 @@ jest.mock('@deriv/api-v2', () => ({ describe('useAdvertiserStats', () => { test('should not return data when useSettings and useAuthentication is still fetching', () => { - mockUseAuthentication.mockReturnValueOnce({ + (mockUseAuthentication as jest.Mock).mockReturnValueOnce({ ...mockUseAuthentication, isSuccess: false, }); - mockUseSettings.mockReturnValueOnce({ + (mockUseSettings as jest.Mock).mockReturnValueOnce({ ...mockUseSettings, isSuccess: false, }); - mockUseAdvertiserInfo.mockReturnValueOnce({ + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ ...mockUseAdvertiserInfo, - isSuccess: false, + data: {}, }); + const wrapper = ({ children }: { children: JSX.Element }) => ( {children} @@ -65,12 +71,21 @@ describe('useAdvertiserStats', () => { {children} ); - mockUseSettings.mockReturnValueOnce({ - data: { first_name: 'Jane', last_name: 'Doe' }, + (mockUseAuthorize as jest.Mock).mockReturnValueOnce({ + isSuccess: true, + }); + (mockUseSettings as jest.Mock).mockReturnValueOnce({ + data: { + first_name: 'Jane', + has_submitted_personal_details: false, + last_name: 'Doe', + }, }); jest.useFakeTimers('modern').setSystemTime(new Date('2024-02-20')); - mockUseAdvertiserInfo.mockReturnValueOnce({ + + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ + ...mockUseAdvertiserInfo('2'), data: { buy_orders_count: 10, created_time: 1698034883, @@ -78,13 +93,13 @@ describe('useAdvertiserStats', () => { sell_orders_count: 5, }, }); - const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); + const { result } = renderHook(() => useAdvertiserStats('2'), { wrapper }); - expect(result.current.data.fullName).toBe('Jane Doe'); - expect(result.current.data.tradePartners).toBe(1); - expect(result.current.data.buyOrdersCount).toBe(10); - expect(result.current.data.sellOrdersCount).toBe(5); - expect(result.current.data.daysSinceJoined).toBe(120); + expect(result?.current?.data?.fullName).toBe('Jane Doe'); + expect(result?.current?.data?.tradePartners).toBe(1); + expect(result?.current?.data?.buyOrdersCount).toBe(10); + expect(result?.current?.data?.sellOrdersCount).toBe(5); + expect(result?.current?.data?.daysSinceJoined).toBe(120); }); test('should return the correct total count and lifetime', () => { const wrapper = ({ children }: { children: JSX.Element }) => ( @@ -93,23 +108,24 @@ describe('useAdvertiserStats', () => { ); - mockUseAdvertiserInfo.mockReturnValueOnce({ + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ + ...mockUseAdvertiserInfo, data: { - buy_orders_amount: 10, + buy_orders_amount: '10', buy_orders_count: 10, partner_count: 1, - sell_orders_amount: 50, + sell_orders_amount: '50', sell_orders_count: 5, total_orders_count: 30, - total_turnover: 100, + total_turnover: '100', }, }); const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); - expect(result.current.data.totalOrders).toBe(15); - expect(result.current.data.totalOrdersLifetime).toBe(30); - expect(result.current.data.tradeVolume).toBe(60); - expect(result.current.data.tradeVolumeLifetime).toBe(100); + expect(result?.current?.data?.totalOrders).toBe(15); + expect(result?.current?.data?.totalOrdersLifetime).toBe(30); + expect(result?.current?.data?.tradeVolume).toBe(60); + expect(result?.current?.data?.tradeVolumeLifetime).toBe(100); }); test('should return the correct rates and limits', () => { const wrapper = ({ children }: { children: JSX.Element }) => ( @@ -117,24 +133,28 @@ describe('useAdvertiserStats', () => { {children} ); - mockUseAdvertiserInfo.mockReturnValueOnce({ + + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ data: { buy_completion_rate: 1.4, - daily_buy: 10, - daily_buy_limit: 100, - daily_sell: 40, - daily_sell_limit: 50, + daily_buy: '10', + daily_buy_limit: '100', + daily_sell: '40', + daily_sell_limit: '50', sell_completion_rate: 2.4, - upgradable_daily_limits: 1, + upgradable_daily_limits: { + max_daily_buy: '1000', + max_daily_sell: '1000', + }, }, }); const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); - expect(result.current.data.buyCompletionRate).toBe(1.4); - expect(result.current.data.sellCompletionRate).toBe(2.4); - expect(result.current.data.dailyAvailableBuyLimit).toBe(90); - expect(result.current.data.dailyAvailableSellLimit).toBe(10); - expect(result.current.data.isEligibleForLimitUpgrade).toBe(true); + expect(result?.current?.data?.buyCompletionRate).toBe(1.4); + expect(result?.current?.data?.sellCompletionRate).toBe(2.4); + expect(result?.current?.data?.dailyAvailableBuyLimit).toBe(90); + expect(result?.current?.data?.dailyAvailableSellLimit).toBe(10); + expect(result?.current?.data?.isEligibleForLimitUpgrade).toBe(true); }); test('should return the correct buy/release times', () => { const wrapper = ({ children }: { children: JSX.Element }) => ( @@ -142,7 +162,8 @@ describe('useAdvertiserStats', () => { {children} ); - mockUseAdvertiserInfo.mockReturnValueOnce({ + + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ data: { buy_time_avg: 150, release_time_avg: 40, @@ -150,8 +171,8 @@ describe('useAdvertiserStats', () => { }); const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); - expect(result.current.data.averagePayTime).toBe(3); - expect(result.current.data.averageReleaseTime).toBe(1); + expect(result?.current?.data?.averagePayTime).toBe(3); + expect(result?.current?.data?.averageReleaseTime).toBe(1); }); test('should return the correct verification statuses', () => { const wrapper = ({ children }: { children: JSX.Element }) => ( @@ -159,13 +180,13 @@ describe('useAdvertiserStats', () => { {children} ); - mockUseAdvertiserInfo.mockReturnValueOnce({ + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ data: { - full_verification: false, - is_approved: false, + has_full_verification: false, + is_approved_boolean: false, }, }); - mockUseAuthentication.mockReturnValueOnce({ + (mockUseAuthentication as jest.Mock).mockReturnValueOnce({ data: { document: { status: 'verified', @@ -177,16 +198,16 @@ describe('useAdvertiserStats', () => { }); const { result } = renderHook(() => useAdvertiserStats(), { wrapper }); - expect(result.current.data.isAddressVerified).toBe(true); - expect(result.current.data.isIdentityVerified).toBe(false); + expect(result?.current?.data?.isAddressVerified).toBe(true); + expect(result?.current?.data?.isIdentityVerified).toBe(false); - mockUseAdvertiserInfo.mockReturnValueOnce({ + (mockUseAdvertiserInfo as jest.Mock).mockReturnValueOnce({ data: { - full_verification: true, - is_approved: true, + has_full_verification: true, + is_approved_boolean: true, }, }); - mockUseAuthentication.mockReturnValueOnce({ + (mockUseAuthentication as jest.Mock).mockReturnValueOnce({ data: { document: { status: 'verified', @@ -198,7 +219,7 @@ describe('useAdvertiserStats', () => { }); const { result: verifiedResult } = renderHook(() => useAdvertiserStats(), { wrapper }); - expect(verifiedResult.current.data.isAddressVerified).toBe(true); - expect(verifiedResult.current.data.isIdentityVerified).toBeUndefined(); + expect(verifiedResult?.current?.data?.isAddressVerified).toBe(true); + expect(verifiedResult?.current?.data?.isIdentityVerified).toBe(undefined); }); }); diff --git a/packages/p2p-v2/src/hooks/__tests__/useIsAdvertiser.spec.tsx b/packages/p2p-v2/src/hooks/__tests__/useIsAdvertiser.spec.tsx new file mode 100644 index 000000000000..226866717e10 --- /dev/null +++ b/packages/p2p-v2/src/hooks/__tests__/useIsAdvertiser.spec.tsx @@ -0,0 +1,41 @@ +import { p2p } from '@deriv/api-v2'; +import { renderHook } from '@testing-library/react-hooks'; +import useIsAdvertiser from '../useIsAdvertiser'; + +jest.mock('@deriv/api-v2', () => ({ + ...jest.requireActual('@deriv/api-v2'), + p2p: { + advertiser: { + useGetInfo: jest.fn().mockReturnValue({ + data: { + currency: 'USD', + }, + isLoading: false, + subscribe: jest.fn(), + unsubscribe: jest.fn(), + }), + }, + }, +})); + +const mockUseGetInfo = p2p.advertiser.useGetInfo as jest.MockedFunction; + +describe('useIsAdvertiser', () => { + it('should return true if data is not empty and there is no error in the response', () => { + const { result } = renderHook(() => useIsAdvertiser()); + expect(result.current).toBeTruthy(); + }); + + it('should return false if error.code is AdvertiserNotFound, and data is empty', () => { + (mockUseGetInfo as jest.Mock).mockReturnValueOnce({ + ...mockUseGetInfo, + data: {}, + error: { + code: 'AdvertiserNotFound', + }, + }); + + const { result } = renderHook(() => useIsAdvertiser()); + expect(result.current).toBeFalsy(); + }); +}); diff --git a/packages/p2p-v2/src/hooks/index.ts b/packages/p2p-v2/src/hooks/index.ts index 3edd25bb81c7..5bc139b60fc7 100644 --- a/packages/p2p-v2/src/hooks/index.ts +++ b/packages/p2p-v2/src/hooks/index.ts @@ -4,6 +4,7 @@ export { default as useDevice } from './useDevice'; export { default as useExtendedOrderDetails } from './useExtendedOrderDetails'; export { default as useFetchMore } from './useFetchMore'; export { default as useFloatingRate } from './useFloatingRate'; +export { default as useIsAdvertiser } from './useIsAdvertiser'; export { default as useModalManager } from './useModalManager'; export { default as usePoiPoaStatus } from './usePoiPoaStatus'; export { default as useQueryString } from './useQueryString'; diff --git a/packages/p2p-v2/src/hooks/useAdvertiserStats.ts b/packages/p2p-v2/src/hooks/useAdvertiserStats.ts index c01a2ee41c2a..bb1f7e1d9b3e 100644 --- a/packages/p2p-v2/src/hooks/useAdvertiserStats.ts +++ b/packages/p2p-v2/src/hooks/useAdvertiserStats.ts @@ -1,6 +1,7 @@ -import { useMemo } from 'react'; -import { daysSince } from '@/utils'; -import { p2p, useAuthentication, useSettings } from '@deriv/api-v2'; +import { useEffect, useMemo } from 'react'; +import { useAdvertiserInfoState } from '@/providers/AdvertiserInfoStateProvider'; +import { daysSince, isEmptyObject } from '@/utils'; +import { p2p, useAuthentication, useAuthorize, useSettings } from '@deriv/api-v2'; /** * Formats the advertiser duration into the following format: @@ -20,14 +21,28 @@ const toAdvertiserMinutes = (duration?: number | null) => { * @param advertiserId - ID of the advertiser stats to reveal. If not provided, by default it will return the user's own stats. */ const useAdvertiserStats = (advertiserId?: string) => { - const { data, isSuccess, ...rest } = p2p.advertiser.useGetInfo(advertiserId); + const { isSuccess } = useAuthorize(); + const { data, subscribe, unsubscribe } = p2p.advertiser.useGetInfo(advertiserId); const { data: settings, isSuccess: isSuccessSettings } = useSettings(); const { data: authenticationStatus, isSuccess: isSuccessAuthenticationStatus } = useAuthentication(); + const { error, isIdle, isLoading, isSubscribed } = useAdvertiserInfoState(); + + useEffect(() => { + if (isSuccess && advertiserId) { + subscribe({ id: advertiserId }); + } + + return () => { + localStorage.removeItem(`p2p_v2_p2p_advertiser_info_${advertiserId}`); + unsubscribe(); + }; + }, [advertiserId, isSuccess, subscribe, unsubscribe]); const transformedData = useMemo(() => { - if (!isSuccess && !isSuccessSettings && !isSuccessAuthenticationStatus) return; + if (!isSubscribed && isEmptyObject(data) && !isSuccessSettings && !isSuccessAuthenticationStatus) + return undefined; - const isAdvertiser = data?.is_approved; + const isAdvertiser = data.is_approved_boolean; return { ...data, @@ -60,7 +75,7 @@ const useAdvertiserStats = (advertiserId?: string) => { /** Checks if the advertiser has completed proof of address verification */ isAddressVerified: isAdvertiser - ? data?.full_verification + ? data.has_full_verification : authenticationStatus?.document?.status === 'verified', /** Checks if the user is already an advertiser */ @@ -71,7 +86,7 @@ const useAdvertiserStats = (advertiserId?: string) => { /** Checks if the advertiser has completed proof of identity verification */ isIdentityVerified: isAdvertiser - ? data?.basic_verification + ? data.has_basic_verification : authenticationStatus?.identity?.status === 'verified', /** The percentage of completed orders out of total orders as a seller within the past 30 days. */ @@ -95,12 +110,23 @@ const useAdvertiserStats = (advertiserId?: string) => { /** The total trade volume since registration */ tradeVolumeLifetime: Number(data?.total_turnover) || 0, }; - }, [data, settings, isSuccess, isSuccessSettings, isSuccessAuthenticationStatus, authenticationStatus]); + }, [ + isSubscribed, + data, + isSuccessSettings, + isSuccessAuthenticationStatus, + settings?.first_name, + settings?.last_name, + authenticationStatus?.document?.status, + authenticationStatus?.identity?.status, + ]); return { data: transformedData, - isSuccess, - ...rest, + error, + isIdle, + isLoading, + isSubscribed, }; }; diff --git a/packages/p2p-v2/src/hooks/useIsAdvertiser.ts b/packages/p2p-v2/src/hooks/useIsAdvertiser.ts new file mode 100644 index 000000000000..47734a23a3fb --- /dev/null +++ b/packages/p2p-v2/src/hooks/useIsAdvertiser.ts @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; +import { ERROR_CODES } from '@/constants'; +import { isEmptyObject } from '@/utils'; +import { p2p } from '@deriv/api-v2'; + +/** + * Custom hook to check if the current user is an advertiser. + * @returns {boolean} isAdvertiser - True if the current user is an advertiser, false otherwise. + */ +const useIsAdvertiser = (): boolean => { + const { data, error } = p2p.advertiser.useGetInfo(); + const [isAdvertiser, setIsAdvertiser] = useState(!error && !isEmptyObject(data)); + + useEffect(() => { + if (error && error.code === ERROR_CODES.ADVERTISER_NOT_FOUND) { + setIsAdvertiser(false); + } else if (!error && !isEmptyObject(data)) { + setIsAdvertiser(true); + } + }, [data, error]); + + return isAdvertiser; +}; + +export default useIsAdvertiser; diff --git a/packages/p2p-v2/src/hooks/useSendbird.ts b/packages/p2p-v2/src/hooks/useSendbird.ts index 0985ba80da5f..9f46d324e683 100644 --- a/packages/p2p-v2/src/hooks/useSendbird.ts +++ b/packages/p2p-v2/src/hooks/useSendbird.ts @@ -87,7 +87,7 @@ const useSendbird = (orderId: string) => { isError: isErrorSendbirdServiceToken, isSuccess: isSuccessSendbirdServiceToken, } = useSendbirdServiceToken(); - const { data: advertiserInfo, isSuccess: isSuccessAdvertiserInfo } = p2p.advertiser.useGetInfo(); + const { data: advertiserInfo } = p2p.advertiser.useGetInfo(); //TODO: p2p_chat_create endpoint to be removed once chat_channel_url is created from p2p_order_create const { isError: isErrorChatCreate, mutate: createChat } = useChatCreate(); const { isErrorOrderInfo, orderDetails } = useOrderDetails(); @@ -221,12 +221,7 @@ const useSendbird = (orderId: string) => { const initialiseChat = useCallback(async () => { try { - if ( - isSuccessSendbirdServiceToken && - isSuccessAdvertiserInfo && - sendbirdServiceToken?.app_id && - advertiserInfo?.chat_user_id - ) { + if (isSuccessSendbirdServiceToken && sendbirdServiceToken?.app_id && advertiserInfo?.chat_user_id) { setIsChatError(false); setIsChatLoading(true); const { app_id: appId, token } = sendbirdServiceToken; @@ -262,7 +257,6 @@ const useSendbird = (orderId: string) => { } }, [ isSuccessSendbirdServiceToken, - isSuccessAdvertiserInfo, sendbirdServiceToken, advertiserInfo?.chat_user_id, orderDetails?.chat_channel_url, diff --git a/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/Advertiser.tsx b/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/Advertiser.tsx index 5903474d4c9d..4472f34a5a72 100644 --- a/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/Advertiser.tsx +++ b/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/Advertiser.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { useHistory, useLocation, useParams } from 'react-router-dom'; import { PageReturn, ProfileContent } from '@/components'; import { BUY_SELL_URL, MY_PROFILE_URL } from '@/constants'; +import { p2p } from '@deriv/api-v2'; import { LabelPairedEllipsisVerticalLgRegularIcon } from '@deriv/quill-icons'; import { useDevice } from '@deriv-com/ui'; import { AdvertiserAdvertsTable } from '../AdvertiserAdvertsTable'; @@ -10,6 +11,11 @@ import './Advertiser.scss'; const Advertiser = () => { const { isMobile } = useDevice(); const { advertiserId } = useParams<{ advertiserId: string }>(); + const { data: advertiserInfo } = p2p.advertiser.useGetInfo(); + + // Need to return undefined if the id is the same as the logged in user + // This will prevent the API from trying to resubscribe to the same user and grab the data from local storage + const id = advertiserId !== advertiserInfo.id ? advertiserId : undefined; const history = useHistory(); const location = useLocation(); @@ -29,7 +35,7 @@ const Advertiser = () => { })} weight='bold' /> - + ); diff --git a/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/__tests__/Advertiser.spec.tsx b/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/__tests__/Advertiser.spec.tsx index 45e3cdbccccb..5e3e3d6b4c4e 100644 --- a/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/__tests__/Advertiser.spec.tsx +++ b/packages/p2p-v2/src/pages/advertiser/screens/Advertiser/__tests__/Advertiser.spec.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { APIProvider, AuthProvider } from '@deriv/api-v2'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Advertiser from '../Advertiser'; @@ -12,6 +13,12 @@ const mockUseLocation = { state: { from: '' }, }; +const wrapper = ({ children }: { children: JSX.Element }) => ( + + {children} + +); + jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => mockUseHistory, @@ -19,6 +26,21 @@ jest.mock('react-router-dom', () => ({ useParams: () => ({ advertiserId: '123' }), })); +jest.mock('@deriv/api-v2', () => ({ + ...jest.requireActual('@deriv/api-v2'), + p2p: { + advertiser: { + useGetInfo: jest.fn(() => ({ + data: { + advertiser_info: { + id: '123', + }, + }, + })), + }, + }, +})); + jest.mock('@deriv-com/ui', () => ({ ...jest.requireActual('@deriv-com/ui'), useDevice: jest.fn(() => ({ isMobile: false })), @@ -35,7 +57,7 @@ jest.mock('../../AdvertiserAdvertsTable', () => ({ describe('', () => { it('should render the Advertiser page component', () => { - render(); + render(, { wrapper }); expect(screen.getByText('Advertiser’s page')).toBeInTheDocument(); expect(screen.getByText('ProfileContent')).toBeInTheDocument(); @@ -43,7 +65,7 @@ describe('', () => { }); it('should call navigate back to buy-sell page when the back button is clicked', () => { - render(); + render(, { wrapper }); const backButton = screen.getByTestId('dt_p2p_v2_page_return_btn'); userEvent.click(backButton); expect(mockUseHistory.push).toHaveBeenCalledWith('/cashier/p2p-v2/buy-sell'); @@ -51,7 +73,7 @@ describe('', () => { it('should call navigate back to my-profile page when the back button is clicked', () => { mockUseLocation.state.from = 'MyProfile'; - render(); + render(, { wrapper }); const backButton = screen.getByTestId('dt_p2p_v2_page_return_btn'); userEvent.click(backButton); expect(mockUseHistory.push).toHaveBeenCalledWith('/cashier/p2p-v2/my-profile?tab=My+counterparties'); diff --git a/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/SellAdPaymentSelection.tsx b/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/SellAdPaymentSelection.tsx index 7860c9f37a27..29c612ba7e29 100644 --- a/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/SellAdPaymentSelection.tsx +++ b/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/SellAdPaymentSelection.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { PaymentMethodCard } from '@/components'; +import { useIsAdvertiser } from '@/hooks'; import { p2p } from '@deriv/api-v2'; import { LabelPairedPlusLgBoldIcon } from '@deriv/quill-icons'; import { Button, Text } from '@deriv-com/ui'; @@ -10,7 +11,8 @@ type TSellAdPaymentSelectionProps = { selectedPaymentMethodIds: number[]; }; const SellAdPaymentSelection = ({ onSelectPaymentMethod, selectedPaymentMethodIds }: TSellAdPaymentSelectionProps) => { - const { data: advertiserPaymentMethods } = p2p.advertiserPaymentMethods.useGet(); + const isAdvertiser = useIsAdvertiser(); + const { data: advertiserPaymentMethods } = p2p.advertiserPaymentMethods.useGet(isAdvertiser); return (
diff --git a/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/__tests__/SellAdPaymentSelection.spec.tsx b/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/__tests__/SellAdPaymentSelection.spec.tsx index a832b119a267..8148cc99a3a1 100644 --- a/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/__tests__/SellAdPaymentSelection.spec.tsx +++ b/packages/p2p-v2/src/pages/my-ads/components/SellAdPaymentSelection/__tests__/SellAdPaymentSelection.spec.tsx @@ -32,6 +32,11 @@ jest.mock('@deriv/api-v2', () => ({ }, })); +jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), + useIsAdvertiser: jest.fn(() => true), +})); + const mockProps = { onSelectPaymentMethod: jest.fn(), selectedPaymentMethodIds: [], diff --git a/packages/p2p-v2/src/pages/my-ads/screens/MyAds/MyAdsTable/MyAdsTable.tsx b/packages/p2p-v2/src/pages/my-ads/screens/MyAds/MyAdsTable/MyAdsTable.tsx index fcdecdaa4fcf..4e60bd834897 100644 --- a/packages/p2p-v2/src/pages/my-ads/screens/MyAds/MyAdsTable/MyAdsTable.tsx +++ b/packages/p2p-v2/src/pages/my-ads/screens/MyAds/MyAdsTable/MyAdsTable.tsx @@ -5,6 +5,7 @@ import { Table } from '@/components'; import { MyAdsDeleteModal } from '@/components/Modals'; import { ShareAdsModal } from '@/components/Modals/ShareAdsModal'; import { AD_ACTION, MY_ADS_URL } from '@/constants'; +import { useIsAdvertiser } from '@/hooks'; import { p2p } from '@deriv/api-v2'; import { Loader } from '@deriv-com/ui'; import { MyAdsEmpty } from '../../MyAdsEmpty'; @@ -48,8 +49,23 @@ const columns = [ ]; const MyAdsTable = () => { - const { data = [], isFetching, isLoading, loadMoreAdverts } = p2p.advertiserAdverts.useGet(); + const isAdvertiser = useIsAdvertiser(); + const { + data = [], + isFetching, + isLoading, + loadMoreAdverts, + } = p2p.advertiserAdverts.useGet(undefined, { + enabled: isAdvertiser, + }); const { data: advertiserInfo } = p2p.advertiser.useGetInfo(); + const { + balance_available: balanceAvailable, + blocked_until: blockedUntil, + daily_buy_limit: dailyBuyLimit, + daily_sell_limit: dailySellLimit, + is_listed_boolean: isListed, + } = advertiserInfo || {}; const { mutate } = p2p.advert.useUpdate(); const { mutate: updateAds } = p2p.advertiser.useUpdate(); const { error, isSuccess, mutate: deleteAd } = p2p.advert.useDelete(); @@ -67,7 +83,7 @@ const MyAdsTable = () => { } }, [error?.error?.message, isSuccess]); - if (isLoading) return ; + if (isLoading && isFetching) return ; if (!data.length) return ; @@ -99,7 +115,7 @@ const MyAdsTable = () => { } }; - const onClickToggle = () => updateAds({ is_listed: advertiserInfo?.is_listed ? 0 : 1 }); + const onClickToggle = () => updateAds({ is_listed: isListed ? 0 : 1 }); const onRequestClose = () => { if (isModalOpen) { @@ -113,10 +129,7 @@ const MyAdsTable = () => { }; return ( - +
{ rowRender={(rowData: unknown) => ( )} diff --git a/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/MyProfile.tsx b/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/MyProfile.tsx index 3567ac865562..33e9796d4621 100644 --- a/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/MyProfile.tsx +++ b/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/MyProfile.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { ProfileContent, Verification } from '@/components'; import { NicknameModal } from '@/components/Modals'; -import { useAdvertiserStats, usePoiPoaStatus, useQueryString } from '@/hooks'; +import { useAdvertiserStats, useIsAdvertiser, usePoiPoaStatus, useQueryString } from '@/hooks'; import { Loader, Tab, Tabs, useDevice } from '@deriv-com/ui'; import { MyProfileAdDetails } from '../MyProfileAdDetails'; import { MyProfileCounterparties } from '../MyProfileCounterparties'; @@ -16,8 +16,9 @@ const MyProfile = () => { const { isMobile } = useDevice(); const { queryString, setQueryString } = useQueryString(); const { data } = usePoiPoaStatus(); - const { data: advertiserStats, failureReason, isLoading } = useAdvertiserStats(); + const { data: advertiserStats, isLoading } = useAdvertiserStats(); const { isP2PPoaRequired, isPoaVerified, isPoiVerified } = data || {}; + const isAdvertiser = useIsAdvertiser(); const [isNicknameModalOpen, setIsNicknameModalOpen] = useState(false); const currentTab = queryString.tab; @@ -31,8 +32,8 @@ const MyProfile = () => { useEffect(() => { const isPoaPoiVerified = (!isP2PPoaRequired || isPoaVerified) && isPoiVerified; - if (isPoaPoiVerified && !!failureReason) setIsNicknameModalOpen(true); - }, [failureReason, isP2PPoaRequired, isPoaVerified, isPoiVerified]); + if (isPoaPoiVerified && !isAdvertiser) setIsNicknameModalOpen(true); + }, [isAdvertiser, isP2PPoaRequired, isPoaVerified, isPoiVerified]); if (isLoading && !advertiserStats) { return ; diff --git a/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx b/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx index e7b2efc1e5db..1da2bd50ec7d 100644 --- a/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx +++ b/packages/p2p-v2/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useAdvertiserStats, usePoiPoaStatus } from '@/hooks'; +import { useAdvertiserStats, useIsAdvertiser, usePoiPoaStatus } from '@/hooks'; import { APIProvider, AuthProvider } from '@deriv/api-v2'; import { useDevice } from '@deriv-com/ui'; import { render, screen } from '@testing-library/react'; @@ -56,14 +56,16 @@ jest.mock('../MyProfileMobile', () => ({ const mockUseDevice = useDevice as jest.MockedFunction; const mockUsePoiPoaStatus = usePoiPoaStatus as jest.MockedFunction; const mockUseAdvertiserStats = useAdvertiserStats as jest.MockedFunction; +const mockUseIsAdvertiser = useIsAdvertiser as jest.MockedFunction; jest.mock('@/hooks', () => ({ useAdvertiserStats: jest.fn().mockReturnValue({ data: { fullName: 'Jane Done', }, - failureReason: undefined, + error: undefined, isLoading: false, }), + useIsAdvertiser: jest.fn().mockReturnValue(true), usePoiPoaStatus: jest.fn().mockReturnValue({ data: { isP2PPoaRequired: false, @@ -118,11 +120,13 @@ describe('MyProfile', () => { isLoading: false, }); + (mockUseIsAdvertiser as jest.Mock).mockReturnValueOnce(false); + (mockUseAdvertiserStats as jest.Mock).mockReturnValueOnce({ data: { fullName: 'Jane Doe', }, - failureReason: 'Failure', + error: 'Failure', isLoading: false, }); diff --git a/packages/p2p-v2/src/pages/my-profile/screens/MyProfileStats/MyProfileStats.tsx b/packages/p2p-v2/src/pages/my-profile/screens/MyProfileStats/MyProfileStats.tsx index 2b110ad04436..fe39786bdae2 100644 --- a/packages/p2p-v2/src/pages/my-profile/screens/MyProfileStats/MyProfileStats.tsx +++ b/packages/p2p-v2/src/pages/my-profile/screens/MyProfileStats/MyProfileStats.tsx @@ -16,7 +16,7 @@ const MyProfileStats = ({ advertiserId }: TMyProfileStatsProps) => { const { data, isLoading } = useAdvertiserStats(advertiserId); const { data: activeAccount } = useActiveAccount(); - if (isLoading || !data) return ; + if (isLoading || !data) return ; const { averagePayTime, diff --git a/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/PaymentMethods.tsx b/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/PaymentMethods.tsx index aa231c9f65f5..2971f5a9a02d 100644 --- a/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/PaymentMethods.tsx +++ b/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/PaymentMethods.tsx @@ -1,6 +1,7 @@ import React, { useCallback, useReducer } from 'react'; import { TSelectedPaymentMethod } from 'types'; import { PaymentMethodForm } from '@/components'; +import { useIsAdvertiser } from '@/hooks'; import { advertiserPaymentMethodsReducer } from '@/reducers'; import { p2p } from '@deriv/api-v2'; import { Loader } from '@deriv-com/ui'; @@ -13,7 +14,8 @@ import { PaymentMethodsList } from './PaymentMethodsList'; * @example * **/ const PaymentMethods = () => { - const { data: p2pAdvertiserPaymentMethods, isLoading } = p2p.advertiserPaymentMethods.useGet(); + const isAdvertiser = useIsAdvertiser(); + const { data: p2pAdvertiserPaymentMethods, isLoading } = p2p.advertiserPaymentMethods.useGet(isAdvertiser); const [formState, dispatch] = useReducer(advertiserPaymentMethodsReducer, {}); const handleAddPaymentMethod = (selectedPaymentMethod?: TSelectedPaymentMethod) => { diff --git a/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/__tests__/PaymentMethods.spec.tsx b/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/__tests__/PaymentMethods.spec.tsx index 0568e880b2e7..18d5c0bd836e 100644 --- a/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/__tests__/PaymentMethods.spec.tsx +++ b/packages/p2p-v2/src/pages/my-profile/screens/PaymentMethods/__tests__/PaymentMethods.spec.tsx @@ -85,6 +85,11 @@ jest.mock('@/components', () => ({ )), })); +jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), + useIsAdvertiser: jest.fn(() => true), +})); + jest.mock('../PaymentMethodsEmpty', () => ({ PaymentMethodsEmpty: jest.fn(() =>
PaymentMethodsEmpty
), })); diff --git a/packages/p2p-v2/src/pages/orders/components/ChatHeader/ChatHeader.tsx b/packages/p2p-v2/src/pages/orders/components/ChatHeader/ChatHeader.tsx index 5a9148f70483..3180d21746ab 100644 --- a/packages/p2p-v2/src/pages/orders/components/ChatHeader/ChatHeader.tsx +++ b/packages/p2p-v2/src/pages/orders/components/ChatHeader/ChatHeader.tsx @@ -12,7 +12,7 @@ const ChatHeader = ({ isOnline, lastOnlineTime, nickname }: TChatHeaderProps) => const { isMobile } = useDevice(); return (
- +
{nickname} diff --git a/packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/AdvertiserInfoStateProvider.tsx b/packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/AdvertiserInfoStateProvider.tsx new file mode 100644 index 000000000000..084ae1c9752c --- /dev/null +++ b/packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/AdvertiserInfoStateProvider.tsx @@ -0,0 +1,32 @@ +import React, { createContext, PropsWithChildren, useContext } from 'react'; +import { TSocketError } from '@deriv/api-v2/types'; + +type TContextValue = { + error: TSocketError<'p2p_advertiser_info'> | undefined; + isIdle: boolean; + isLoading: boolean; + isSubscribed: boolean; + setHasCreatedAdvertiser: (hasCreatedAdvertiser: boolean) => void; +}; + +const AdvertiserInfoStateContext = createContext({} as TContextValue); + +type TAdvertiserInfoStateProviderProps = { + value: TContextValue; +}; + +/** + * A provider that contains the state of the current logged in user's advertiser info. + */ +export const AdvertiserInfoStateProvider = ({ + children, + value, +}: PropsWithChildren) => ( + {children} +); + +/** + * Custom hook to access the current logged in user's advertiser info state. + * @returns {TContextValue} The current logged in user's advertiser info state. + */ +export const useAdvertiserInfoState = (): TContextValue => useContext(AdvertiserInfoStateContext); diff --git a/packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/index.ts b/packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/index.ts new file mode 100644 index 000000000000..ded29d0ed3bf --- /dev/null +++ b/packages/p2p-v2/src/providers/AdvertiserInfoStateProvider/index.ts @@ -0,0 +1 @@ +export { AdvertiserInfoStateProvider, useAdvertiserInfoState } from './AdvertiserInfoStateProvider'; diff --git a/packages/p2p-v2/src/routes/AppContent/__tests__/AppContent.spec.tsx b/packages/p2p-v2/src/routes/AppContent/__tests__/AppContent.spec.tsx index fa1179e7157d..10b7ea96312c 100644 --- a/packages/p2p-v2/src/routes/AppContent/__tests__/AppContent.spec.tsx +++ b/packages/p2p-v2/src/routes/AppContent/__tests__/AppContent.spec.tsx @@ -16,6 +16,12 @@ jest.mock('react-router-dom', () => ({ jest.mock('@deriv/api-v2', () => ({ ...jest.requireActual('@deriv/api-v2'), p2p: { + advertiser: { + useGetInfo: jest.fn().mockReturnValue({ + data: {}, + subscribe: jest.fn(), + }), + }, settings: { useGetSettings: jest.fn().mockReturnValue({ data: {}, @@ -27,6 +33,9 @@ jest.mock('@deriv/api-v2', () => ({ data: { currency: 'EUR' }, isLoading: true, }), + useAuthorize: jest.fn().mockReturnValue({ + isSuccess: true, + }), })); jest.mock('@/pages/buy-sell', () => ({ diff --git a/packages/p2p-v2/src/routes/AppContent/index.tsx b/packages/p2p-v2/src/routes/AppContent/index.tsx index df194cc0486b..846bf694f1bf 100644 --- a/packages/p2p-v2/src/routes/AppContent/index.tsx +++ b/packages/p2p-v2/src/routes/AppContent/index.tsx @@ -2,7 +2,8 @@ import React, { useEffect, useState } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; import { CloseHeader } from '@/components'; import { BUY_SELL_URL } from '@/constants'; -import { p2p, useActiveAccount } from '@deriv/api-v2'; +import { AdvertiserInfoStateProvider } from '@/providers/AdvertiserInfoStateProvider'; +import { p2p, useActiveAccount, useAuthorize } from '@deriv/api-v2'; import { Loader, Tab, Tabs } from '@deriv-com/ui'; import Router from '../Router'; import { routes } from '../routes-config'; @@ -13,7 +14,8 @@ const tabRoutesConfiguration = routes.filter(route => route.name !== 'Advertiser const AppContent = () => { const history = useHistory(); const location = useLocation(); - const { data: activeAccountData, isLoading } = useActiveAccount(); + const { data: activeAccountData, isLoading: isLoadingActiveAccount } = useActiveAccount(); + const { isSuccess } = useAuthorize(); const getActiveTab = (pathname: string) => { const match = routes.find(route => pathname.startsWith(route.path)); @@ -21,23 +23,49 @@ const AppContent = () => { }; const [activeTab, setActiveTab] = useState(() => getActiveTab(location.pathname)); - const { subscribe } = p2p.settings.useGetSettings(); + const [hasCreatedAdvertiser, setHasCreatedAdvertiser] = useState(false); + const { subscribe: subscribeP2PSettings } = p2p.settings.useGetSettings(); + const { error, isIdle, isLoading, isSubscribed, subscribe: subscribeAdvertiserInfo } = p2p.advertiser.useGetInfo(); useEffect(() => { - if (activeAccountData) subscribe(); - }, [activeAccountData, subscribe]); + if (activeAccountData) { + subscribeP2PSettings(); + } + }, [activeAccountData, subscribeP2PSettings]); + + useEffect(() => { + if (isSuccess) { + subscribeAdvertiserInfo(); + } + }, [isSuccess, subscribeAdvertiserInfo]); + + // Need this to subscribe to advertiser info after user has created an advertiser. + // setHasCreatedAdvertiser is triggered inside of NicknameModal. + useEffect(() => { + if (isSuccess && hasCreatedAdvertiser) { + subscribeAdvertiserInfo(); + } + }, [hasCreatedAdvertiser, isSuccess, subscribeAdvertiserInfo]); useEffect(() => { setActiveTab(getActiveTab(location.pathname)); }, [location]); - if (isLoading || !activeAccountData) return ; + if (isLoadingActiveAccount || !activeAccountData) return ; // NOTE: Replace this with P2PBlocked component later and a custom hook useIsP2PEnabled, P2P is only available for USD accounts if (activeAccountData?.currency !== 'USD') return

P2P is only available for USD accounts.

; return ( - <> +
{
- +
); }; diff --git a/packages/p2p-v2/src/utils/time.ts b/packages/p2p-v2/src/utils/time.ts index bb22bcc5b562..c9726ba52d2f 100644 --- a/packages/p2p-v2/src/utils/time.ts +++ b/packages/p2p-v2/src/utils/time.ts @@ -29,9 +29,9 @@ export const toMoment = (value?: moment.MomentInput): moment.Moment => { * @param {String} date the date to calculate number of days since * @return {Number} an integer of the number of days */ -export const daysSince = (date: string) => { +export const daysSince = (date: string): number => { const diff = toMoment().startOf('day').diff(toMoment(date).startOf('day'), 'days'); - return !date ? '' : diff; + return !date ? 0 : diff; }; /**