Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ameerul / P2PS-464 [FE] - Mandatory Mobile Number Verification for Deriv P2P (New User) #400

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/components/AdvertiserName/AdvertiserNameBadges.tsx
Original file line number Diff line number Diff line change
@@ -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<TAdvertiserStats> }) => {
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 (
<div className='advertiser-name-badges' data-testid='dt_advertiser_name_badges'>
@@ -28,6 +32,13 @@ const AdvertiserNameBadges = ({ advertiserStats }: { advertiserStats: DeepPartia
status={getStatus(isAddressVerified)}
variant={getVariant(isAddressVerified)}
/>
{isPhoneNumberVerificationEnabled && isMyProfile && (
<Badge
label={localize('Mobile')}
status={getStatus(isPhoneNumberVerified)}
variant={getVariant(isPhoneNumberVerified)}
/>
)}
</div>
);
};
10 changes: 10 additions & 0 deletions src/components/AdvertiserName/__tests__/AdvertiserName.spec.tsx
Original file line number Diff line number Diff line change
@@ -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),
}));
Original file line number Diff line number Diff line change
@@ -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(<AdvertiserNameBadges {...mockProps} />);
expect(screen.getByText('100+')).toBeInTheDocument();
});
it('should render mobile badge when phone number verification is enabled and not verified', () => {
mockUseGetPhoneNumberVerification.isPhoneNumberVerificationEnabled = true;
render(<AdvertiserNameBadges {...mockProps} />);
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(<AdvertiserNameBadges {...mockProps} />);
expect(screen.getByText('Mobile')).toBeInTheDocument();
expect(screen.getAllByText('verified')).toHaveLength(3);
});
});
21 changes: 14 additions & 7 deletions src/components/AdvertsTableRow/AdvertsTableRow.tsx
Original file line number Diff line number Diff line change
@@ -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');
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated the param name to make it more general since now we're also checking for PNV if enabled

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a question. why do we keep this in the URL params? I mean it can be easily manipulated by the user.
can we add a hook and check this in component level using that hook? 🤔


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);
13 changes: 12 additions & 1 deletion src/components/Checklist/Checklist.scss
Original file line number Diff line number Diff line change
@@ -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%;
}
}
}
56 changes: 38 additions & 18 deletions src/components/Checklist/Checklist.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,63 @@
import clsx from 'clsx';
import { LabelPairedArrowRightLgBoldIcon, LabelPairedCheckMdBoldIcon } from '@deriv/quill-icons';
import { Button, Text, useDevice } from '@deriv-com/ui';
import './Checklist.scss';

type TChecklistItem = {
isDisabled?: boolean;
onClick?: () => void;
phoneNumber?: string | null;
status: string;
testId?: string;
text: string;
};

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 (
<div className='checklist'>
{items.map(item => (
<div className='checklist__item' key={item.text}>
<Text color={item.isDisabled ? 'less-prominent' : 'general'} size={isMobile ? 'md' : 'sm'}>
{item.text}
</Text>
{item.status === 'done' ? (
<div className='checklist__item-checkmark'>
<LabelPairedCheckMdBoldIcon className='checklist__item-checkmark-icon' />
{items.map(item => {
const isDone = item.status === 'done';

return (
<div className='checklist__item' key={item.text}>
<div className='checklist__item-text'>
<Text color={getTextColor(item.isDisabled, item.status)} size={isMobile ? 'md' : 'sm'}>
{item.text}
</Text>
{item.phoneNumber && (
<Text color='less-prominent' size={isMobile ? 'sm' : 'xs'}>
{item.phoneNumber}
</Text>
)}
</div>
) : (
<Button
className='checklist__item-button'
className={clsx('checklist__item-button', {
'checklist__item-button--done': isDone,
})}
disabled={item.isDisabled}
icon={
<LabelPairedArrowRightLgBoldIcon
className='checklist__item-button-icon'
{...(item.testId && { 'data-testid': item.testId })}
/>
isDone ? (
<LabelPairedCheckMdBoldIcon className='checklist__item-checkmark-icon' />
) : (
<LabelPairedArrowRightLgBoldIcon
className='checklist__item-button-icon'
{...(item.testId && { 'data-testid': item.testId })}
/>
)
}
onClick={item.onClick}
onClick={isDone ? undefined : item.onClick}
/>
)}
</div>
))}
</div>
);
})}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check for this edge case when user is not verified

Screenshot 2024-11-08 at 3 58 53 PM

);

if (blockUnblockError && isModalOpenFor('ErrorModal')) {
38 changes: 29 additions & 9 deletions src/components/Verification/Verification.tsx
Original file line number Diff line number Diff line change
@@ -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`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we do the redirection based on the environment.. if test link or staging, redirect to staging-app.deriv. else app.deriv ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm i dont think its necessary tbh, because either way we can still test the PNV using just prod link. Also even if we use staging and so forth, QA still need to set up the endpoint again

},
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 = () => {
<div className='verification'>
<DerivLightIcCashierSendEmailIcon className='verification__icon' height={128} width={128} />
<Text className='verification__text' size={isMobile ? 'lg' : 'md'} weight='bold'>
<Localize i18n_default_text='Verify your P2P account' />
<Localize i18n_default_text='Let’s get you secured' />
</Text>
<Text align='center' className='verification__text' size={isMobile ? 'lg' : 'sm'}>
<Localize i18n_default_text='Verify your identity and address to use Deriv P2P.' />
<Localize i18n_default_text='Complete your P2P profile to enjoy secure transactions.' />
</Text>
<Checklist items={checklistItems} />
</div>
Loading
Loading