diff --git a/src/components/AdvertiserName/AdvertiserNameBadges.tsx b/src/components/AdvertiserName/AdvertiserNameBadges.tsx index 4fe43feb..b380b9cf 100644 --- a/src/components/AdvertiserName/AdvertiserNameBadges.tsx +++ b/src/components/AdvertiserName/AdvertiserNameBadges.tsx @@ -1,5 +1,7 @@ import { DeepPartial, TAdvertiserStats } from 'types'; import { Badge } from '@/components'; +import { useGetPhoneNumberVerification } from '@/hooks/custom-hooks'; +import { getCurrentRoute } from '@/utils'; import { useTranslations } from '@deriv-com/translations'; import './AdvertiserNameBadges.scss'; @@ -11,9 +13,11 @@ import './AdvertiserNameBadges.scss'; */ const AdvertiserNameBadges = ({ advertiserStats }: { advertiserStats: DeepPartial }) => { const { isAddressVerified, isIdentityVerified, totalOrders } = advertiserStats || {}; + const { isPhoneNumberVerificationEnabled, isPhoneNumberVerified } = useGetPhoneNumberVerification(); const { localize } = useTranslations(); const getStatus = (isVerified?: boolean) => (isVerified ? localize('verified') : localize('not verified')); const getVariant = (isVerified?: boolean) => (isVerified ? 'success' : 'general'); + const isMyProfile = getCurrentRoute() === 'my-profile'; return (
@@ -28,6 +32,13 @@ const AdvertiserNameBadges = ({ advertiserStats }: { advertiserStats: DeepPartia status={getStatus(isAddressVerified)} variant={getVariant(isAddressVerified)} /> + {isPhoneNumberVerificationEnabled && isMyProfile && ( + + )}
); }; diff --git a/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx b/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx index 9188a361..19de24d0 100644 --- a/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx +++ b/src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx @@ -16,6 +16,16 @@ const mockModalManager = { showModal: jest.fn(), }; jest.mock('@/hooks', () => ({ + ...jest.requireActual('@/hooks'), + api: { + settings: { + useSettings: jest.fn(() => ({ pnv_required: false })), + }, + }, + useGetPhoneNumberVerification: jest.fn(() => ({ + isPhoneNumberVerificationEnabled: false, + isPhoneNumberVerified: false, + })), useIsRtl: jest.fn(() => false), useModalManager: jest.fn(() => mockModalManager), })); diff --git a/src/components/AdvertiserName/__tests__/AdvertiserNameBadges.spec.tsx b/src/components/AdvertiserName/__tests__/AdvertiserNameBadges.spec.tsx index 941131b9..fdce1fb9 100644 --- a/src/components/AdvertiserName/__tests__/AdvertiserNameBadges.spec.tsx +++ b/src/components/AdvertiserName/__tests__/AdvertiserNameBadges.spec.tsx @@ -10,9 +10,20 @@ const mockUseAdvertiserStats = { isLoading: false, }; +const mockUseGetPhoneNumberVerification = { + isPhoneNumberVerificationEnabled: false, + isPhoneNumberVerified: false, +}; + jest.mock('@/hooks/custom-hooks', () => ({ ...jest.requireActual('@/hooks/custom-hooks'), useAdvertiserStats: jest.fn(() => mockUseAdvertiserStats), + useGetPhoneNumberVerification: jest.fn(() => mockUseGetPhoneNumberVerification), +})); + +jest.mock('@/utils', () => ({ + ...jest.requireActual('@/utils'), + getCurrentRoute: jest.fn(() => 'my-profile'), })); const mockProps = { @@ -56,4 +67,16 @@ describe('AdvertiserNameBadges', () => { render(); expect(screen.getByText('100+')).toBeInTheDocument(); }); + it('should render mobile badge when phone number verification is enabled and not verified', () => { + mockUseGetPhoneNumberVerification.isPhoneNumberVerificationEnabled = true; + render(); + expect(screen.getByText('Mobile')).toBeInTheDocument(); + expect(screen.getByText('not verified')).toBeInTheDocument(); + }); + it('should render mobile badge with verified status when phone number verification is enabled and verified', () => { + mockUseGetPhoneNumberVerification.isPhoneNumberVerified = true; + render(); + expect(screen.getByText('Mobile')).toBeInTheDocument(); + expect(screen.getAllByText('verified')).toHaveLength(3); + }); }); diff --git a/src/components/AdvertsTableRow/AdvertsTableRow.tsx b/src/components/AdvertsTableRow/AdvertsTableRow.tsx index 65b44fd7..f45e0713 100644 --- a/src/components/AdvertsTableRow/AdvertsTableRow.tsx +++ b/src/components/AdvertsTableRow/AdvertsTableRow.tsx @@ -4,12 +4,13 @@ import { useHistory, useLocation } from 'react-router-dom'; import { TAdvertsTableRowRenderer, TCurrency } from 'types'; import { Badge, BuySellForm, PaymentMethodLabel, StarRating, UserAvatar } from '@/components'; import { ErrorModal, NicknameModal } from '@/components/Modals'; -import { ADVERTISER_URL, BUY_SELL } from '@/constants'; +import { ADVERTISER_URL, BUY_SELL, BUY_SELL_URL } from '@/constants'; import { api } from '@/hooks'; import { useGetBusinessHours, useIsAdvertiser, useIsAdvertiserBarred, + useIsAdvertiserNotVerified, useModalManager, usePoiPoaStatus, } from '@/hooks/custom-hooks'; @@ -35,6 +36,7 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { const { localize } = useTranslations(); const { hasCreatedAdvertiser } = useAdvertiserInfoState(); const { isScheduleAvailable } = useGetBusinessHours(); + const isAdvertiserNotVerified = useIsAdvertiserNotVerified(); const { account_currency: accountCurrency, @@ -91,11 +93,16 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { const redirectToVerification = () => { const searchParams = new URLSearchParams(location.search); - searchParams.set('poi_poa_verified', 'false'); - history.replace({ - pathname: location.pathname, - search: searchParams.toString(), - }); + searchParams.set('verified', 'false'); + + if (!isBuySellPage) { + history.push(`${BUY_SELL_URL}?${searchParams.toString()}`); + } else { + history.replace({ + pathname: location.pathname, + search: searchParams.toString(), + }); + } }; const redirectToAdvertiser = () => { @@ -263,7 +270,7 @@ const AdvertsTableRow = memo((props: TAdvertsTableRowRenderer) => { className='lg:min-w-[7.5rem]' disabled={isAdvertiserBarred || !isScheduleAvailable} onClick={() => { - if (!isAdvertiser && !isPoiPoaVerified) { + if (isAdvertiserNotVerified) { redirectToVerification(); } else { setSelectedAdvertId(advertId); diff --git a/src/components/Checklist/Checklist.scss b/src/components/Checklist/Checklist.scss index 5c08a41b..97ab14d9 100644 --- a/src/components/Checklist/Checklist.scss +++ b/src/components/Checklist/Checklist.scss @@ -41,6 +41,11 @@ cursor: not-allowed; } + &--done { + cursor: default; + background-color: #4bb4b3 !important; + } + &-icon { fill: #fff; } @@ -50,8 +55,14 @@ @include icon-wrapper; &-icon { - fill: #4bb4b3; + fill: #fff; } } + + &-text { + display: flex; + flex-direction: column; + width: 100%; + } } } diff --git a/src/components/Checklist/Checklist.tsx b/src/components/Checklist/Checklist.tsx index 3f6ae03b..ffad2897 100644 --- a/src/components/Checklist/Checklist.tsx +++ b/src/components/Checklist/Checklist.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import { LabelPairedArrowRightLgBoldIcon, LabelPairedCheckMdBoldIcon } from '@deriv/quill-icons'; import { Button, Text, useDevice } from '@deriv-com/ui'; import './Checklist.scss'; @@ -5,6 +6,7 @@ import './Checklist.scss'; type TChecklistItem = { isDisabled?: boolean; onClick?: () => void; + phoneNumber?: string | null; status: string; testId?: string; text: string; @@ -12,32 +14,50 @@ type TChecklistItem = { const Checklist = ({ items }: { items: TChecklistItem[] }) => { const { isMobile } = useDevice(); + + const getTextColor = (isDisabled: boolean | undefined, status: string) => { + if (isDisabled) return 'less-prominent'; + if (status === 'rejected') return 'error'; + return 'general'; + }; + return (
- {items.map(item => ( -
- - {item.text} - - {item.status === 'done' ? ( -
- + {items.map(item => { + const isDone = item.status === 'done'; + + return ( +
+
+ + {item.text} + + {item.phoneNumber && ( + + {item.phoneNumber} + + )}
- ) : (
- ))} +
+ ); + })}
); }; diff --git a/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx b/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx index ab3d5d8c..bab8fbd0 100644 --- a/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx +++ b/src/components/Modals/BlockUnblockUserModal/BlockUnblockUserModal.tsx @@ -84,7 +84,10 @@ const BlockUnblockUserModal = ({ }; const blockUnblockError = errorMessages.find( - error => error.code === ERROR_CODES.PERMISSION_DENIED || error.code === ERROR_CODES.INVALID_ADVERTISER_ID + error => + error.code === ERROR_CODES.PERMISSION_DENIED || + error.code === ERROR_CODES.INVALID_ADVERTISER_ID || + error.code === ERROR_CODES.ADVERTISER_NOT_REGISTERED ); if (blockUnblockError && isModalOpenFor('ErrorModal')) { diff --git a/src/components/Verification/Verification.tsx b/src/components/Verification/Verification.tsx index bb9dc55e..8b9b3c1b 100644 --- a/src/components/Verification/Verification.tsx +++ b/src/components/Verification/Verification.tsx @@ -1,6 +1,6 @@ import { TLocalize } from 'types'; import { Checklist } from '@/components'; -import { usePoiPoaStatus } from '@/hooks/custom-hooks'; +import { useGetPhoneNumberVerification, usePoiPoaStatus } from '@/hooks/custom-hooks'; import { DerivLightIcCashierSendEmailIcon } from '@deriv/quill-icons'; import { Localize, useTranslations } from '@deriv-com/translations'; import { Loader, Text, useDevice } from '@deriv-com/ui'; @@ -14,9 +14,9 @@ const getPoiAction = (status: string | undefined, localize: TLocalize) => { case 'rejected': return localize('Identity verification failed. Please try again.'); case 'verified': - return localize('Identity verification complete.'); + return localize('Identity verified'); default: - return localize('Upload documents to verify your identity.'); + return localize('Your identity'); } }; @@ -32,15 +32,22 @@ const getPoaAction = ( return localize('Address verification failed. Please try again.'); case 'verified': if (isPoaAuthenticatedWithIdv) return localize('Upload documents to verify your address.'); - return localize('Address verification complete.'); + return localize('Address verified'); default: - return localize('Upload documents to verify your address.'); + return localize('Your address'); } }; +const getStatus = (status: string | undefined) => { + if (status === 'verified') return 'done'; + else if (status === 'rejected') return 'rejected'; + return 'action'; +}; + const Verification = () => { const { isMobile } = useDevice(); const { localize } = useTranslations(); + const { isPhoneNumberVerificationEnabled, isPhoneNumberVerified, phoneNumber } = useGetPhoneNumberVerification(); const { data, isLoading } = usePoiPoaStatus(); const { isP2PPoaRequired, @@ -69,13 +76,26 @@ const Verification = () => { }; const checklistItems = [ + ...(isPhoneNumberVerificationEnabled + ? [ + { + onClick: () => { + window.location.href = `${URLConstants.derivAppProduction}/account/personal-details`; + }, + phoneNumber: isPhoneNumberVerified ? phoneNumber : undefined, + status: isPhoneNumberVerified ? 'done' : 'action', + testId: 'dt_verification_phone_number_arrow_button', + text: isPhoneNumberVerified ? localize('Phone number verified') : localize('Your phone number'), + }, + ] + : []), { isDisabled: isPoiPending, onClick: () => { if (!isPoiVerified) redirectToVerification(`${URLConstants.derivAppProduction}/account/proof-of-identity`); }, - status: isPoiVerified ? 'done' : 'action', + status: getStatus(poiStatus), testId: 'dt_verification_poi_arrow_button', text: getPoiAction(poiStatus, localize), }, @@ -87,7 +107,7 @@ const Verification = () => { if (allowPoaRedirection) redirectToVerification(`${URLConstants.derivAppProduction}/account/proof-of-address`); }, - status: allowPoaRedirection ? 'action' : 'done', + status: getStatus(poaStatus), testId: 'dt_verification_poa_arrow_button', text: getPoaAction(isPoaAuthenticatedWithIdv, poaStatus, localize), }, @@ -101,10 +121,10 @@ const Verification = () => {
- + - +
diff --git a/src/components/Verification/__tests__/Verification.spec.tsx b/src/components/Verification/__tests__/Verification.spec.tsx index 6e547af5..2e4b79c0 100644 --- a/src/components/Verification/__tests__/Verification.spec.tsx +++ b/src/components/Verification/__tests__/Verification.spec.tsx @@ -1,10 +1,10 @@ -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { fireEvent, render, screen } from '@testing-library/react'; import Verification from '../Verification'; let mockUsePoiPoaStatusData = { data: { isP2PPoaRequired: 1, + isPoaAuthenticatedWithIdv: false, isPoaPending: false, isPoaVerified: false, isPoiPending: false, @@ -15,8 +15,15 @@ let mockUsePoiPoaStatusData = { isLoading: true, }; +let mockUseGetPhoneNumberVerificationData = { + isPhoneNumberVerificationEnabled: false, + isPhoneNumberVerified: false, + phoneNumber: '1234567890', +}; + jest.mock('@/hooks/custom-hooks', () => ({ ...jest.requireActual('@/hooks/custom-hooks'), + useGetPhoneNumberVerification: jest.fn(() => mockUseGetPhoneNumberVerificationData), usePoiPoaStatus: jest.fn(() => mockUsePoiPoaStatusData), })); @@ -41,25 +48,32 @@ describe('', () => { expect(screen.getByTestId('dt_derivs-loader')).toBeInTheDocument(); }); - it('should ask user to upload their documents if isLoading is false and poi/poa status is none', () => { + it('should not show phone number verification if isPhoneNumberVerificationEnabled is false', () => { + render(); + expect(screen.queryByText('Your phone number')).not.toBeInTheDocument(); + }); + + it('should ask user to upload their documents and add their phone number if isLoading is false and poi/poa status is none and isPhoneNumberVerificationEnabled is true', () => { mockUsePoiPoaStatusData = { ...mockUsePoiPoaStatusData, isLoading: false, }; + mockUseGetPhoneNumberVerificationData = { + ...mockUseGetPhoneNumberVerificationData, + isPhoneNumberVerificationEnabled: true, + }; render(); - expect(screen.getByText('Verify your P2P account')).toBeInTheDocument(); - expect(screen.getByText('Verify your identity and address to use Deriv P2P.')).toBeInTheDocument(); - expect(screen.getByText('Upload documents to verify your identity.')).toBeInTheDocument(); - expect(screen.getByText('Upload documents to verify your address.')).toBeInTheDocument(); + expect(screen.getByText('Let’s get you secured')).toBeInTheDocument(); + expect(screen.getByText('Complete your P2P profile to enjoy secure transactions.')).toBeInTheDocument(); + expect(screen.getByText('Your phone number')).toBeInTheDocument(); + expect(screen.getByText('Your identity')).toBeInTheDocument(); + expect(screen.getByText('Your address')).toBeInTheDocument(); }); - it('should redirect user to proof-of-identity route if user clicks on arrow button', async () => { - mockUsePoiPoaStatusData = { - ...mockUsePoiPoaStatusData, - isLoading: false, - }; + it('should redirect user to account/personal-details route if user clicks on phone number arrow button', () => { + render(); Object.defineProperty(window, 'location', { value: { @@ -68,19 +82,33 @@ describe('', () => { writable: true, }); + const phoneNumberButton = screen.getByTestId('dt_verification_phone_number_arrow_button'); + expect(phoneNumberButton).toBeInTheDocument(); + + fireEvent.click(phoneNumberButton); + + expect(window.location.href).toBe('https://app.deriv.com/account/personal-details'); + }); + + it('should redirect user to proof-of-identity route if user clicks on arrow button', () => { + mockUsePoiPoaStatusData = { + ...mockUsePoiPoaStatusData, + isLoading: false, + }; + render(); const poiButton = screen.getByTestId('dt_verification_poi_arrow_button'); expect(poiButton).toBeInTheDocument(); - await userEvent.click(poiButton); + fireEvent.click(poiButton); expect(window.location.href).toBe( 'https://app.deriv.com/account/proof-of-identity?ext_platform_url=/cashier/p2p&platform=p2p-v2' ); }); - it('should redirect user to proof-of-address route if user clicks on arrow button', async () => { + it('should redirect user to proof-of-address route if user clicks on arrow button', () => { mockUsePoiPoaStatusData = { ...mockUsePoiPoaStatusData, isLoading: false, @@ -91,7 +119,7 @@ describe('', () => { const poaButton = screen.getByTestId('dt_verification_poa_arrow_button'); expect(poaButton).toBeInTheDocument(); - await userEvent.click(poaButton); + fireEvent.click(poaButton); expect(window.location.href).toBe( 'https://app.deriv.com/account/proof-of-address?ext_platform_url=/cashier/p2p&platform=p2p-v2' @@ -104,15 +132,17 @@ describe('', () => { isLoading: false, }; + window.location.search = 'param1=value1¶m2=value2'; + render(); const poiButton = screen.getByTestId('dt_verification_poi_arrow_button'); expect(poiButton).toBeInTheDocument(); - await userEvent.click(poiButton); + fireEvent.click(poiButton); expect(window.location.href).toBe( - 'https://app.deriv.com/account/proof-of-identity?ext_platform_url=/cashier/p2p&platform=p2p-v2' + 'https://app.deriv.com/account/proof-of-identity?ext_platform_url=/cashier/p2p&platform=p2p-v2¶m1=value1¶m2=value2' ); }); @@ -131,8 +161,8 @@ describe('', () => { render(); - const poaButton = screen.getAllByRole('button')[0]; - const poiButton = screen.getAllByRole('button')[1]; + const poaButton = screen.getAllByRole('button')[1]; + const poiButton = screen.getAllByRole('button')[2]; expect(screen.getByText('Identity verification in progress.')).toBeInTheDocument(); expect(screen.getByText('Address verification in progress.')).toBeInTheDocument(); @@ -145,6 +175,8 @@ describe('', () => { ...mockUsePoiPoaStatusData, data: { ...mockUsePoiPoaStatusData.data, + isPoaPending: false, + isPoiPending: false, poaStatus: 'rejected', poiStatus: 'rejected', }, @@ -157,6 +189,18 @@ describe('', () => { expect(screen.getByText('Address verification failed. Please try again.')).toBeInTheDocument(); }); + it('should show verified message and phone number if phone number is verified', () => { + mockUseGetPhoneNumberVerificationData = { + ...mockUseGetPhoneNumberVerificationData, + isPhoneNumberVerified: true, + }; + + render(); + + expect(screen.getByText('Phone number verified')).toBeInTheDocument(); + expect(screen.getByText('1234567890')).toBeInTheDocument(); + }); + it('should show verified message if poi/poa status is verified', () => { mockUsePoiPoaStatusData = { ...mockUsePoiPoaStatusData, @@ -170,7 +214,18 @@ describe('', () => { render(); - expect(screen.getByText('Identity verification complete.')).toBeInTheDocument(); - expect(screen.getByText('Address verification complete.')).toBeInTheDocument(); + expect(screen.getByText('Identity verified')).toBeInTheDocument(); + expect(screen.getByText('Address verified')).toBeInTheDocument(); + }); + + it('should should prompt user to upload documents if isPoaAuthenticatedWithIdv is true', () => { + mockUsePoiPoaStatusData = { + ...mockUsePoiPoaStatusData, + data: { ...mockUsePoiPoaStatusData.data, isPoaAuthenticatedWithIdv: true }, + }; + + render(); + + expect(screen.getByText('Upload documents to verify your address.')).toBeInTheDocument(); }); }); diff --git a/src/constants/api-error-codes.ts b/src/constants/api-error-codes.ts index 5a30fa9c..7afcc6fe 100644 --- a/src/constants/api-error-codes.ts +++ b/src/constants/api-error-codes.ts @@ -9,6 +9,7 @@ export const ERROR_CODES = { ADVERT_SAME_LIMITS: 'AdvertSameLimits', ADVERTISER_ADS_PAUSED: 'advertiser_ads_paused', ADVERTISER_NOT_FOUND: 'AdvertiserNotFound', + ADVERTISER_NOT_REGISTERED: 'AdvertiserNotRegistered', ADVERTISER_SCHEDULE: 'advertiser_schedule', ADVERTISER_SCHEDULE_AVAILABILITY: 'AdvertiserScheduleAvailability', ADVERTISER_TEMP_BAN: 'advertiser_temp_ban', diff --git a/src/hooks/api/settings/p2p-settings/useSettings.ts b/src/hooks/api/settings/p2p-settings/useSettings.ts index 84b51976..af9aacea 100644 --- a/src/hooks/api/settings/p2p-settings/useSettings.ts +++ b/src/hooks/api/settings/p2p-settings/useSettings.ts @@ -17,6 +17,8 @@ type TP2PSettings = isDisabled: boolean; isPaymentMethodsEnabled: boolean; localCurrency?: string; + pnv_required?: boolean; + poa_required?: boolean; rateType: 'fixed' | 'float'; reachedTargetDate: boolean; }) diff --git a/src/hooks/custom-hooks/__tests__/useGetPhoneNumberVerification.spec.ts b/src/hooks/custom-hooks/__tests__/useGetPhoneNumberVerification.spec.ts new file mode 100644 index 00000000..36400898 --- /dev/null +++ b/src/hooks/custom-hooks/__tests__/useGetPhoneNumberVerification.spec.ts @@ -0,0 +1,52 @@ +import { renderHook } from '@testing-library/react'; +import useGetPhoneNumberVerification from '../useGetPhoneNumberVerification'; + +const mockSettings = { + phone: '1234567890', + phone_number_verification: { + verified: 0, + }, +}; + +const mockP2PSettings = { + pnv_required: 0, +}; + +jest.mock('@deriv-com/api-hooks', () => ({ + useGetSettings: jest.fn(() => ({ data: mockSettings })), +})); + +jest.mock('../../api', () => ({ + settings: { + useSettings: jest.fn(() => ({ data: mockP2PSettings })), + }, +})); + +describe('useGetPhoneNumberVerification', () => { + it('should return isPhoneNumberVerificationEnabled false, shouldShowVerification false, the phone number and false if the phone number is not verified', () => { + const { result } = renderHook(() => useGetPhoneNumberVerification()); + + expect(result.current.isPhoneNumberVerificationEnabled).toBe(false); + expect(result.current.shouldShowVerification).toBe(false); + expect(result.current.isPhoneNumberVerified).toBe(false); + expect(result.current.phoneNumber).toBe('1234567890'); + }); + + it('should return isPhoneNumberVerificationEnabled true, the phone number and true if the phone number is verified and pnv_required is true', () => { + mockSettings.phone_number_verification.verified = 1; + mockP2PSettings.pnv_required = 1; + const { result } = renderHook(() => useGetPhoneNumberVerification()); + + expect(result.current.isPhoneNumberVerificationEnabled).toBe(true); + expect(result.current.isPhoneNumberVerified).toBe(true); + expect(result.current.phoneNumber).toBe('1234567890'); + }); + + it('should return shouldShowVerification true if the phone number is not verified and pnv_required is true', () => { + mockSettings.phone_number_verification.verified = 0; + mockP2PSettings.pnv_required = 1; + const { result } = renderHook(() => useGetPhoneNumberVerification()); + + expect(result.current.shouldShowVerification).toBe(true); + }); +}); diff --git a/src/hooks/custom-hooks/__tests__/useIsAdvertiserNotVerified.spec.ts b/src/hooks/custom-hooks/__tests__/useIsAdvertiserNotVerified.spec.ts new file mode 100644 index 00000000..859afd3b --- /dev/null +++ b/src/hooks/custom-hooks/__tests__/useIsAdvertiserNotVerified.spec.ts @@ -0,0 +1,55 @@ +import { renderHook } from '@testing-library/react'; +import useGetPhoneNumberVerification from '../useGetPhoneNumberVerification'; +import useIsAdvertiser from '../useIsAdvertiser'; +import useIsAdvertiserNotVerified from '../useIsAdvertiserNotVerified'; +import usePoiPoaStatus from '../usePoiPoaStatus'; + +jest.mock('../useGetPhoneNumberVerification'); +jest.mock('../useIsAdvertiser'); +jest.mock('../usePoiPoaStatus'); + +const mockUseGetPhoneNumberVerification = useGetPhoneNumberVerification as jest.Mock; +const mockUseIsAdvertiser = useIsAdvertiser as jest.Mock; +const mockUsePoiPoaStatus = usePoiPoaStatus as jest.Mock; + +describe('useIsAdvertiserNotVerified', () => { + it('should return true if user is not an advertiser and POI/POA is not verified', () => { + mockUseGetPhoneNumberVerification.mockReturnValue({ shouldShowVerification: false }); + mockUseIsAdvertiser.mockReturnValue(false); + mockUsePoiPoaStatus.mockReturnValue({ data: { isPoiPoaVerified: false } }); + + const { result } = renderHook(() => useIsAdvertiserNotVerified()); + + expect(result.current).toBe(true); + }); + + it('should return true if user is not an advertiser and should show verification', () => { + mockUseGetPhoneNumberVerification.mockReturnValue({ shouldShowVerification: true }); + mockUseIsAdvertiser.mockReturnValue(false); + mockUsePoiPoaStatus.mockReturnValue({ data: { isPoiPoaVerified: true } }); + + const { result } = renderHook(() => useIsAdvertiserNotVerified()); + + expect(result.current).toBe(true); + }); + + it('should return false if user is an advertiser', () => { + mockUseGetPhoneNumberVerification.mockReturnValue({ shouldShowVerification: true }); + mockUseIsAdvertiser.mockReturnValue(true); + mockUsePoiPoaStatus.mockReturnValue({ data: { isPoiPoaVerified: false } }); + + const { result } = renderHook(() => useIsAdvertiserNotVerified()); + + expect(result.current).toBe(false); + }); + + it('should return false if user is not an advertiser but POI/POA is verified and should not show verification', () => { + mockUseGetPhoneNumberVerification.mockReturnValue({ shouldShowVerification: false }); + mockUseIsAdvertiser.mockReturnValue(false); + mockUsePoiPoaStatus.mockReturnValue({ data: { isPoiPoaVerified: true } }); + + const { result } = renderHook(() => useIsAdvertiserNotVerified()); + + expect(result.current).toBe(false); + }); +}); diff --git a/src/hooks/custom-hooks/__tests__/usePoiPoaStatus.spec.tsx b/src/hooks/custom-hooks/__tests__/usePoiPoaStatus.spec.tsx index 62e81025..10578796 100644 --- a/src/hooks/custom-hooks/__tests__/usePoiPoaStatus.spec.tsx +++ b/src/hooks/custom-hooks/__tests__/usePoiPoaStatus.spec.tsx @@ -23,6 +23,12 @@ jest.mock('@deriv-com/api-hooks', () => ({ }), })); +jest.mock('../../api', () => ({ + settings: { + useSettings: jest.fn(() => ({ data: { poa_required: 1 } })), + }, +})); + const mockValues = { dataUpdatedAt: 0, error: null, @@ -101,7 +107,7 @@ describe('usePoiPoaStatus', () => { const { result } = renderHook(() => usePoiPoaStatus()); expect(result.current.data).toStrictEqual({ - isP2PPoaRequired: 0, + isP2PPoaRequired: true, isPoaAuthenticatedWithIdv: false, isPoaPending: false, isPoaVerified: true, diff --git a/src/hooks/custom-hooks/index.ts b/src/hooks/custom-hooks/index.ts index 5a8d9d85..a2c41ebe 100644 --- a/src/hooks/custom-hooks/index.ts +++ b/src/hooks/custom-hooks/index.ts @@ -8,10 +8,12 @@ export { default as useFetchMore } from './useFetchMore'; export { default as useFloatingRate } from './useFloatingRate'; export { default as useFullScreen } from './useFullScreen'; export { default as useGetBusinessHours } from './useGetBusinessHours.tsx'; +export { default as useGetPhoneNumberVerification } from './useGetPhoneNumberVerification'; export { default as useGrowthbookGetFeatureValue } from './useGrowthbookGetFeatureValue'; export { default as useHandleRouteChange } from './useHandleRouteChange'; export { default as useIsAdvertiser } from './useIsAdvertiser'; export { default as useIsAdvertiserBarred } from './useIsAdvertiserBarred'; +export { default as useIsAdvertiserNotVerified } from './useIsAdvertiserNotVerified.ts'; export { default as useIsP2PBlocked } from './useIsP2PBlocked'; export { default as useIsRtl } from './useIsRtl'; export { default as useLiveChat } from './useLiveChat'; diff --git a/src/hooks/custom-hooks/useGetPhoneNumberVerification.ts b/src/hooks/custom-hooks/useGetPhoneNumberVerification.ts new file mode 100644 index 00000000..ed5d1423 --- /dev/null +++ b/src/hooks/custom-hooks/useGetPhoneNumberVerification.ts @@ -0,0 +1,21 @@ +import { useGetSettings } from '@deriv-com/api-hooks'; +import { api } from '..'; + +/** A custom hook that returns if phone number verification is enabled, + * if the user's phone number is verified, should show verification component, + * and the phone number + * + * */ +const useGetPhoneNumberVerification = () => { + const { data } = useGetSettings(); + const { data: p2pSettings } = api.settings.useSettings(); + + const isPhoneNumberVerificationEnabled = !!p2pSettings?.pnv_required; + const isPhoneNumberVerified = !!data?.phone_number_verification?.verified; + const shouldShowVerification = !isPhoneNumberVerified && isPhoneNumberVerificationEnabled; + const phoneNumber = data?.phone; + + return { isPhoneNumberVerificationEnabled, isPhoneNumberVerified, phoneNumber, shouldShowVerification }; +}; + +export default useGetPhoneNumberVerification; diff --git a/src/hooks/custom-hooks/useIsAdvertiserNotVerified.ts b/src/hooks/custom-hooks/useIsAdvertiserNotVerified.ts new file mode 100644 index 00000000..77709700 --- /dev/null +++ b/src/hooks/custom-hooks/useIsAdvertiserNotVerified.ts @@ -0,0 +1,15 @@ +import useGetPhoneNumberVerification from './useGetPhoneNumberVerification'; +import useIsAdvertiser from './useIsAdvertiser'; +import usePoiPoaStatus from './usePoiPoaStatus'; + +const useIsAdvertiserNotVerified = () => { + const { shouldShowVerification } = useGetPhoneNumberVerification(); + const { data } = usePoiPoaStatus(); + const isPoiPoaVerified = data?.isPoiPoaVerified; + const isAdvertiser = useIsAdvertiser(); + const isAdvertiserNotVerified = !isAdvertiser && (!isPoiPoaVerified || shouldShowVerification); + + return isAdvertiserNotVerified; +}; + +export default useIsAdvertiserNotVerified; diff --git a/src/hooks/custom-hooks/usePoiPoaStatus.ts b/src/hooks/custom-hooks/usePoiPoaStatus.ts index 65a98c5f..1d6cd051 100644 --- a/src/hooks/custom-hooks/usePoiPoaStatus.ts +++ b/src/hooks/custom-hooks/usePoiPoaStatus.ts @@ -1,9 +1,11 @@ import { useMemo } from 'react'; import { useGetAccountStatus } from '@deriv-com/api-hooks'; +import { api } from '..'; /** A custom hook that returns the POA, POI status and if POA is required for P2P */ const usePoiPoaStatus = () => { const { data, ...rest } = useGetAccountStatus(); + const { data: p2pSettings } = api.settings.useSettings(); // create new response for poi/poa statuses const modifiedAccountStatus = useMemo(() => { @@ -11,7 +13,7 @@ const usePoiPoaStatus = () => { const documentStatus = data?.authentication?.document?.status; const identityStatus = data?.authentication?.identity?.status; - const isP2PPoaRequired = data?.p2p_poa_required; + const isP2PPoaRequired = !!p2pSettings?.poa_required; const isPoaAuthenticatedWithIdv = data?.status.includes('poa_authenticated_with_idv') || data?.status.includes('poa_authenticated_with_idv_photo'); @@ -31,7 +33,7 @@ const usePoiPoaStatus = () => { poaStatus: documentStatus, poiStatus: identityStatus, }; - }, [data]); + }, [data, p2pSettings?.poa_required]); return { /** The POI & POA status. */ diff --git a/src/pages/advertiser/screens/AdvertiserAdvertsTable/__tests__/AdvertiserAdvertsTable.spec.tsx b/src/pages/advertiser/screens/AdvertiserAdvertsTable/__tests__/AdvertiserAdvertsTable.spec.tsx index c0ab21bc..6a7e7467 100644 --- a/src/pages/advertiser/screens/AdvertiserAdvertsTable/__tests__/AdvertiserAdvertsTable.spec.tsx +++ b/src/pages/advertiser/screens/AdvertiserAdvertsTable/__tests__/AdvertiserAdvertsTable.spec.tsx @@ -100,6 +100,7 @@ jest.mock('@/hooks/custom-hooks', () => ({ ...jest.requireActual('@/hooks/custom-hooks'), useIsAdvertiser: jest.fn(() => true), useIsAdvertiserBarred: jest.fn(() => false), + useIsAdvertiserNotVerified: jest.fn(() => false), useModalManager: jest.fn(() => mockUseModalManager), usePoiPoaStatus: jest.fn(() => ({ data: { isPoaVerified: true, isPoiVerified: true } })), useQueryString: jest.fn(() => mockUseQueryString), diff --git a/src/pages/buy-sell/screens/BuySell/BuySell.tsx b/src/pages/buy-sell/screens/BuySell/BuySell.tsx index 509dac76..6c416ff9 100644 --- a/src/pages/buy-sell/screens/BuySell/BuySell.tsx +++ b/src/pages/buy-sell/screens/BuySell/BuySell.tsx @@ -13,9 +13,9 @@ const BuySell = () => { const isAdvertiserBarred = useIsAdvertiserBarred(); const history = useHistory(); const location = useLocation(); - const poiPoaVerified = new URLSearchParams(location.search).get('poi_poa_verified'); + const verified = new URLSearchParams(location.search).get('verified'); - if (poiPoaVerified === 'false') { + if (verified === 'false') { return (
', () => { expect(screen.getByText('BuySellTable')).toBeInTheDocument(); }); - it('should render the PageReturn and Verification components if poi_poa_verified search param is false', () => { + it('should render the PageReturn and Verification components if verified search param is false', () => { (mockUseLocation as jest.Mock).mockImplementation(() => ({ - search: '?poi_poa_verified=false', + search: '?verified=false', })); render(); @@ -81,7 +81,7 @@ describe('', () => { it('should call history.replace when PageReturn is clicked', async () => { (mockUseLocation as jest.Mock).mockImplementation(() => ({ - search: '?poi_poa_verified=false', + search: '?verified=false', })); render(); diff --git a/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx b/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx index 5aadb98a..8d9070f8 100644 --- a/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx +++ b/src/pages/buy-sell/screens/BuySellTable/__tests__/BuySellTable.spec.tsx @@ -100,6 +100,7 @@ jest.mock('@/hooks/custom-hooks', () => ({ ...jest.requireActual('@/hooks/custom-hooks'), useIsAdvertiser: jest.fn(() => mockUseIsAdvertiser), useIsAdvertiserBarred: jest.fn().mockReturnValue(false), + useIsAdvertiserNotVerified: jest.fn(() => true), useModalManager: jest.fn(() => mockUseModalManager), usePoiPoaStatus: jest.fn(() => ({ data: { isPoaVerified: true, isPoiVerified: true } })), useQueryString: jest.fn(() => mockUseQueryString), @@ -246,7 +247,7 @@ describe('', () => { expect(mockUseHistory.replace).toHaveBeenCalledWith({ pathname: '/buy-sell', - search: 'poi_poa_verified=false', + search: 'verified=false', }); }); diff --git a/src/pages/my-ads/screens/MyAds/MyAds.tsx b/src/pages/my-ads/screens/MyAds/MyAds.tsx index c247a908..bdaa3a6f 100644 --- a/src/pages/my-ads/screens/MyAds/MyAds.tsx +++ b/src/pages/my-ads/screens/MyAds/MyAds.tsx @@ -1,15 +1,13 @@ import { OutsideBusinessHoursHint, TemporarilyBarredHint, Verification } from '@/components'; -import { useGetBusinessHours, useIsAdvertiser, useIsAdvertiserBarred, usePoiPoaStatus } from '@/hooks/custom-hooks'; +import { useGetBusinessHours, useIsAdvertiserBarred, useIsAdvertiserNotVerified } from '@/hooks/custom-hooks'; import { MyAdsTable } from './MyAdsTable'; const MyAds = () => { - const isAdvertiser = useIsAdvertiser(); const isAdvertiserBarred = useIsAdvertiserBarred(); const { isScheduleAvailable } = useGetBusinessHours(); - const { data } = usePoiPoaStatus(); - const { isPoiPoaVerified } = data || {}; + const isAdvertiserNotVerified = useIsAdvertiserNotVerified(); - if (!isAdvertiser && !isPoiPoaVerified) + if (isAdvertiserNotVerified) return (
; diff --git a/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx b/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx index 047904af..4f18c046 100644 --- a/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx +++ b/src/pages/my-ads/screens/MyAds/__tests__/MyAds.spec.tsx @@ -1,4 +1,4 @@ -import { useGetBusinessHours, useIsAdvertiserBarred, usePoiPoaStatus } from '@/hooks/custom-hooks'; +import { useGetBusinessHours, useIsAdvertiserBarred, useIsAdvertiserNotVerified } from '@/hooks/custom-hooks'; import { render, screen } from '@testing-library/react'; import MyAds from '../MyAds'; @@ -13,15 +13,8 @@ jest.mock('@/hooks/custom-hooks', () => ({ useGetBusinessHours: jest.fn().mockReturnValue({ isScheduleAvailable: true, }), - useIsAdvertiser: jest.fn(() => false), useIsAdvertiserBarred: jest.fn().mockReturnValue(false), - usePoiPoaStatus: jest.fn().mockReturnValue({ - data: { - isPoaVerified: true, - isPoiPoaVerified: true, - isPoiVerified: true, - }, - }), + useIsAdvertiserNotVerified: jest.fn(() => false), })); jest.mock('@deriv-com/ui', () => ({ @@ -36,8 +29,8 @@ jest.mock('../MyAdsTable', () => ({ })); const mockUseGetBusinessHours = useGetBusinessHours as jest.Mock; +const mockUseIsAdvertiserNotVerified = useIsAdvertiserNotVerified as jest.Mock; const mockUseIsAdvertiserBarred = useIsAdvertiserBarred as jest.MockedFunction; -const mockUsePoiPoaStatus = usePoiPoaStatus as jest.MockedFunction; describe('MyAds', () => { it('should render the MyAdsTable component', () => { @@ -64,12 +57,8 @@ describe('MyAds', () => { expect(screen.queryByText('TemporarilyBarredHint')).not.toBeInTheDocument(); }); - it('should render the Verification component if POA/POI is not verified is false', () => { - (mockUsePoiPoaStatus as jest.Mock).mockReturnValue({ - data: { - isPoiPoaVerified: false, - }, - }); + it('should render the Verification component if advertiser is not verified', () => { + mockUseIsAdvertiserNotVerified.mockReturnValue(true); render(); expect(screen.getByText('Verification')).toBeInTheDocument(); }); diff --git a/src/pages/my-profile/screens/MyProfile/MyProfile.tsx b/src/pages/my-profile/screens/MyProfile/MyProfile.tsx index 712e5cf7..81415f1d 100644 --- a/src/pages/my-profile/screens/MyProfile/MyProfile.tsx +++ b/src/pages/my-profile/screens/MyProfile/MyProfile.tsx @@ -4,6 +4,7 @@ import { NicknameModal } from '@/components/Modals'; import { useAdvertiserStats, useIsAdvertiser, + useIsAdvertiserNotVerified, useModalManager, usePoiPoaStatus, useQueryString, @@ -28,6 +29,7 @@ const MyProfile = () => { const { data: advertiserStats, isLoading } = useAdvertiserStats(); const { isPoiPoaVerified } = data || {}; const isAdvertiser = useIsAdvertiser(); + const isAdvertiserNotVerified = useIsAdvertiserNotVerified(); const { hideModal, isModalOpenFor, showModal } = useModalManager({ shouldReinitializeModals: false }); const currentTab = queryString.tab; @@ -48,7 +50,7 @@ const MyProfile = () => { return ; } - if (!isAdvertiser && !isPoiPoaVerified) { + if (isAdvertiserNotVerified) { return (
diff --git a/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx b/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx index dd90719b..26cbfd27 100644 --- a/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx +++ b/src/pages/my-profile/screens/MyProfile/__tests__/MyProfile.spec.tsx @@ -1,4 +1,4 @@ -import { useAdvertiserStats, useIsAdvertiser, usePoiPoaStatus } from '@/hooks/custom-hooks'; +import { useAdvertiserStats, useIsAdvertiser, useIsAdvertiserNotVerified, usePoiPoaStatus } from '@/hooks/custom-hooks'; import { useDevice } from '@deriv-com/ui'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -48,6 +48,7 @@ const mockUseDevice = useDevice as jest.MockedFunction; const mockUsePoiPoaStatus = usePoiPoaStatus as jest.MockedFunction; const mockUseAdvertiserStats = useAdvertiserStats as jest.MockedFunction; const mockUseIsAdvertiser = useIsAdvertiser as jest.MockedFunction; +const mockUseIsAdvertiserNotVerified = useIsAdvertiserNotVerified as jest.Mock; const mockModalManager = { hideModal: jest.fn(), isModalOpenFor: jest.fn().mockReturnValue(false), @@ -63,6 +64,7 @@ jest.mock('@/hooks/custom-hooks', () => ({ isLoading: false, }), useIsAdvertiser: jest.fn().mockReturnValue(true), + useIsAdvertiserNotVerified: jest.fn().mockReturnValue(false), useModalManager: jest.fn(() => mockModalManager), usePoiPoaStatus: jest.fn().mockReturnValue({ data: { @@ -96,7 +98,7 @@ describe('MyProfile', () => { expect(screen.getByTestId('dt_derivs-loader')).toBeInTheDocument(); }); it('should render the verification component if a new user has not completed POI ', () => { - (mockUseIsAdvertiser as jest.Mock).mockReturnValueOnce(false); + (mockUseIsAdvertiserNotVerified as jest.Mock).mockReturnValueOnce(true); (mockUsePoiPoaStatus as jest.Mock).mockReturnValueOnce({ data: { isPoaVerified: true, isPoiVerified: false }, isLoading: false, @@ -106,7 +108,7 @@ describe('MyProfile', () => { expect(screen.getByText('Verification')).toBeInTheDocument(); }); it('should render the verification component if a new user has not completed POA', () => { - (mockUseIsAdvertiser as jest.Mock).mockReturnValueOnce(false); + (mockUseIsAdvertiserNotVerified as jest.Mock).mockReturnValueOnce(true); (mockUsePoiPoaStatus as jest.Mock).mockReturnValueOnce({ data: { isPoaVerified: false, isPoiVerified: true }, isLoading: false, @@ -115,6 +117,11 @@ describe('MyProfile', () => { render(); expect(screen.getByText('Verification')).toBeInTheDocument(); }); + it('should not render the verification component if the user has completed POI and POA and shouldShowVerification is false', () => { + (mockUseIsAdvertiserNotVerified as jest.Mock).mockReturnValueOnce(false); + render(); + expect(screen.queryByText('Verification')).not.toBeInTheDocument(); + }); it('should show the nickname modal if user has completed POI or POA for the first time', () => { (mockUsePoiPoaStatus as jest.Mock).mockReturnValueOnce({ data: { isPoiPoaVerified: true },