feat(trading): referral code eligibility

This commit is contained in:
asiaznik
2023-11-22 15:35:09 +01:00
parent 87807d2088
commit 2cef937106
12 changed files with 281 additions and 60 deletions
@@ -32,6 +32,19 @@ const validateCode = (value: string, t: ReturnType<typeof useT>) => {
return true;
};
export const ApplyCodeFormContainer = () => {
const { pubKey } = useVegaWallet();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
return <ApplyCodeForm />;
};
export const ApplyCodeForm = () => {
const t = useT();
const program = useReferralProgram();
@@ -55,9 +68,6 @@ export const ApplyCodeForm = () => {
} = useForm();
const [params] = useSearchParams();
const { data: referee } = useReferral({ pubKey, role: 'referee' });
const { data: referrer } = useReferral({ pubKey, role: 'referrer' });
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined,
@@ -144,11 +154,6 @@ export const ApplyCodeForm = () => {
}
}, [navigate, status]);
// go to main page if the current pubkey is already a referrer or referee
if (referee || referrer) {
return <Navigate to={Routes.REFERRALS} />;
}
// show "code applied" message when successfully applied
if (status === 'successful') {
return (
@@ -243,13 +248,13 @@ export const ApplyCodeForm = () => {
<Loader />
</div>
) : null}
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData ? (
<div className="mt-10">
<h2 className="mb-5 text-2xl">
{t(
'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.',
{ count: nextBenefitTierEpochsValue }
)}
<h2 className="text-2xl mb-5">
{t('referralApplyPreviewMessage', {
count: nextBenefitTierEpochsValue,
})}
</h2>
<Statistics data={previewData} program={program} as="referee" />
</div>
@@ -11,6 +11,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) {
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -0,0 +1,10 @@
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
@@ -9,7 +9,7 @@ export type ReferralSetStatsQueryVariables = Types.Exact<{
}>;
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string } } | null> } };
export type ReferralSetStatsQuery = { __typename?: 'Query', referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, partyId: string, discountFactor: string, rewardFactor: string, epochNotionalTakerVolume: string, referralSetRunningNotionalTakerVolume: string, rewardsMultiplier: string, rewardsFactorMultiplier: string, referrerTakerVolume: string, wasEligible: boolean } } | null> } };
export const ReferralSetStatsDocument = gql`
@@ -26,6 +26,7 @@ export const ReferralSetStatsDocument = gql`
rewardsMultiplier
rewardsFactorMultiplier
referrerTakerVolume
wasEligible
}
}
}
@@ -0,0 +1,53 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type StakeAvailableQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type StakeAvailableQuery = { __typename?: 'Query', party?: { __typename?: 'Party', stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } | null, networkParameter?: { __typename?: 'NetworkParameter', value: string } | null };
export const StakeAvailableDocument = gql`
query StakeAvailable($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
/**
* __useStakeAvailableQuery__
*
* To run a query within a React component, call `useStakeAvailableQuery` and pass it any options that fit your needs.
* When your component renders, `useStakeAvailableQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useStakeAvailableQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function useStakeAvailableQuery(baseOptions: Apollo.QueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export function useStakeAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StakeAvailableQuery, StakeAvailableQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<StakeAvailableQuery, StakeAvailableQueryVariables>(StakeAvailableDocument, options);
}
export type StakeAvailableQueryHookResult = ReturnType<typeof useStakeAvailableQuery>;
export type StakeAvailableLazyQueryHookResult = ReturnType<typeof useStakeAvailableLazyQuery>;
export type StakeAvailableQueryResult = Apollo.QueryResult<StakeAvailableQuery, StakeAvailableQueryVariables>;
@@ -0,0 +1,93 @@
import {
Intent,
type Toast,
useToasts,
ToastHeading,
Button,
} from '@vegaprotocol/ui-toolkit';
import { useReferral } from './use-referral';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
const useNonEligibleReferralSet = () => {
const { pubKey } = useVegaWallet();
const { data, loading, refetch } = useReferral({ pubKey, role: 'referee' });
const {
data: epochData,
loading: epochLoading,
refetch: epochRefetch,
} = useCurrentEpochInfoQuery();
useEffect(() => {
const interval = setInterval(() => {
refetch();
epochRefetch();
}, REFETCH_INTERVAL);
return () => {
clearInterval(interval);
};
}, [epochRefetch, refetch]);
return { data, epoch: epochData?.epoch.id, loading: loading || epochLoading };
};
export const useReferralToasts = () => {
const navigate = useNavigate();
const t = useT();
const [setToast, hasToast, updateToast] = useToasts((store) => [
store.setToast,
store.hasToast,
store.update,
]);
const { data, epoch, loading } = useNonEligibleReferralSet();
useEffect(() => {
if (
data &&
epoch &&
!loading &&
!data.isEligible &&
!hasToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch)
) {
const nonEligibleReferralToast: Toast = {
id: NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch,
intent: Intent.Warning,
content: (
<>
<ToastHeading>{t('Referral code no longer valid')}</ToastHeading>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements.'
)}
</p>
<p className="mt-2">
<Button
data-testid="toast-apply-code"
size="xs"
onClick={() => {
navigate(Routes.REFERRALS_APPLY_CODE);
}}
>
{t('Apply a new code')}
</Button>
</p>
</>
),
onClose: () =>
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
}),
};
setToast(nonEligibleReferralToast);
}
}, [data, epoch, hasToast, loading, navigate, setToast, t, updateToast]);
};
@@ -4,6 +4,7 @@ import { useRefereesQuery } from './__generated__/Referees';
import compact from 'lodash/compact';
import type { ReferralSetsQueryVariables } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
@@ -62,6 +63,8 @@ export const useReferral = (args: UseReferralArgs) => {
? referralData.referralSets.edges[0]?.node
: undefined;
const { isEligible } = useStakeAvailable(referralSet?.referrer);
const {
data: refereesData,
loading: refereesLoading,
@@ -103,6 +106,7 @@ export const useReferral = (args: UseReferralArgs) => {
referee: referee,
referrerId: referralSet.referrer,
createdAt: referralSet.createdAt,
isEligible,
referees,
}
: undefined;
@@ -1,34 +1,36 @@
import { gql, useQuery } from '@apollo/client';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useStakeAvailableQuery } from './__generated__/StakeAvailable';
const STAKE_QUERY = gql`
query CreateCode($partyId: ID!) {
party(id: $partyId) {
stakingSummary {
currentStakeAvailable
}
}
networkParameter(key: "referralProgram.minStakedVegaTokens") {
value
}
}
`;
export const useStakeAvailable = () => {
const { pubKey } = useVegaWallet();
const { data } = useQuery(STAKE_QUERY, {
variables: { partyId: pubKey || '' },
skip: !pubKey,
/**
* Gets the current stake available for given public key and required stake for
* the referral program.
*
* (Uses currently connected public key if left empty)
*/
export const useStakeAvailable = (pubKey?: string) => {
const { pubKey: currentPubKey } = useVegaWallet();
const partyId = pubKey || currentPubKey;
const { data } = useStakeAvailableQuery({
variables: { partyId: partyId || '' },
skip: !partyId,
// TODO: remove when network params available
errorPolicy: 'ignore',
});
const stakeAvailable = data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined;
// const requiredStake = data
// ? BigInt(data.networkParameter?.value || '0')
// : undefined;
const requiredStake = BigInt(10);
return {
stakeAvailable: data
? BigInt(data.party?.stakingSummary.currentStakeAvailable || '0')
: undefined,
requiredStake: data
? BigInt(data.networkParameter?.value || '0')
: undefined,
stakeAvailable,
requiredStake,
isEligible:
stakeAvailable != null &&
requiredStake != null &&
stakeAvailable >= requiredStake,
};
};
@@ -32,6 +32,7 @@ import maxBy from 'lodash/maxBy';
import { DocsLinks } from '@vegaprotocol/environment';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
import { ApplyCodeForm } from './apply-code-form';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -50,11 +51,21 @@ export const ReferralStatistics = () => {
});
if (referee?.code) {
return <Statistics data={referee} program={program} as="referee" />;
return (
<>
<Statistics data={referee} program={program} as="referee" />;
{!referee.isEligible && <ApplyCodeForm />}
</>
);
}
if (referrer?.code) {
return <Statistics data={referrer} program={program} as="referrer" />;
return (
<>
<Statistics data={referrer} program={program} as="referrer" />;
<RefereesTable data={referrer} program={program} />
</>
);
}
return <CreateCodeContainer />;
@@ -174,7 +185,7 @@ export const Statistics = ({
const { benefitTiers } = useReferralProgram();
const { stakeAvailable } = useStakeAvailable();
const { stakeAvailable, isEligible } = useStakeAvailable();
const { details } = program;
const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
@@ -200,12 +211,24 @@ export const Statistics = ({
{baseCommissionValue * 100}%
</StatTile>
);
const stakingMultiplierTile = (
<StatTile
title={t('Staking multiplier')}
description={t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(stakeAvailable?.toString() || 0, 18),
})}
description={
<span
className={classNames({
'text-vega-red': !isEligible,
})}
>
{t('{{amount}} $VEGA staked', {
amount: addDecimalsFormatNumber(
stakeAvailable?.toString() || 0,
18
),
})}
</span>
}
>
{multiplier || t('None')}
</StatTile>
@@ -333,28 +356,53 @@ export const Statistics = ({
</>
);
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
const eligibilityWarning = as === 'referee' && !isEligible && (
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3">
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
);
return (
<>
{/* Stats tiles */}
<div className="relative mx-auto mb-20">
<div
className={classNames(
'grid grid-cols-1 grid-rows-1 gap-5 mx-auto mb-20'
)}
className={classNames('grid grid-cols-1 grid-rows-1 gap-5', {
'opacity-20 pointer-events-none': as === 'referee' && !isEligible,
})}
>
{as === 'referrer' && referrerTiles}
{as === 'referee' && refereeTiles}
</div>
{eligibilityWarning}
</div>
);
};
export const RefereesTable = ({
data,
program,
}: {
data: NonNullable<ReturnType<typeof useReferral>['data']>;
program: ReturnType<typeof useReferralProgram>;
}) => {
const t = useT();
const [collapsed, setCollapsed] = useState(false);
const tableRef = useRef<HTMLTableElement>(null);
const { details } = program;
useLayoutEffect(() => {
if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) {
setCollapsed(true);
}
}, []);
return (
<>
{/* Referees (only for referrer view) */}
{as === 'referrer' && data.referees.length > 0 && (
{data.referees.length > 0 && (
<div className="mt-20 mb-20">
<h2 className="mb-5 text-2xl">{t('Referees')}</h2>
<div
+2 -2
View File
@@ -18,7 +18,7 @@ import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
import { ApplyCodeForm } from '../client-pages/referrals/apply-code-form';
import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-form';
import { CreateCodeContainer } from '../client-pages/referrals/create-code-form';
import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-boundary';
import { compact } from 'lodash';
@@ -79,7 +79,7 @@ export const routerConfig: RouteObject[] = compact([
},
{
path: AppRoutes.REFERRALS_APPLY_CODE,
element: <ApplyCodeForm />,
element: <ApplyCodeFormContainer />,
},
],
},
+2
View File
@@ -5,6 +5,7 @@ import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
import { useReferralToasts } from '../client-pages/referrals/hooks/use-referral-toasts';
export const ToastsManager = () => {
useProposalToasts();
@@ -14,6 +15,7 @@ export const ToastsManager = () => {
useReadyToWithdrawalToasts({
withdrawalsLink: Links.PORTFOLIO(),
});
useReferralToasts();
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
+2
View File
@@ -203,6 +203,8 @@
"Ready to trade": "Ready to trade",
"Ready to trade with real funds? <0>Switch to Mainnet</0>": "Ready to trade with real funds? <0>Switch to Mainnet</0>",
"Redeem rewards": "Redeem rewards",
"referralApplyPreviewMessage": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epoch.",
"referralApplyPreviewMessage_plural": "You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.",
"Referral benefits": "Referral benefits",
"Referral discount": "Referral discount",
"referral-statistics-commission": "Commission earned in <0>qUSD</0> (last {{count}} epochs)",