From 2cef937106fc2473e2912afda3bb4939a5f4ede2 Mon Sep 17 00:00:00 2001 From: asiaznik Date: Tue, 21 Nov 2023 17:26:59 +0100 Subject: [PATCH] feat(trading): referral code eligibility --- .../referrals/apply-code-form.tsx | 31 ++++--- .../referrals/hooks/ReferralSetStats.graphql | 1 + .../referrals/hooks/StakeAvailable.graphql | 10 ++ .../hooks/__generated__/ReferralSetStats.ts | 3 +- .../hooks/__generated__/StakeAvailable.ts | 53 +++++++++++ .../referrals/hooks/use-referral-toasts.tsx | 93 +++++++++++++++++++ .../referrals/hooks/use-referral.ts | 4 + .../referrals/hooks/use-stake-available.ts | 52 ++++++----- .../referrals/referral-statistics.tsx | 86 +++++++++++++---- apps/trading/pages/client-router.tsx | 4 +- apps/trading/pages/toasts-manager.tsx | 2 + libs/i18n/src/locales/en/trading.json | 2 + 12 files changed, 281 insertions(+), 60 deletions(-) create mode 100644 apps/trading/client-pages/referrals/hooks/StakeAvailable.graphql create mode 100644 apps/trading/client-pages/referrals/hooks/__generated__/StakeAvailable.ts create mode 100644 apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx diff --git a/apps/trading/client-pages/referrals/apply-code-form.tsx b/apps/trading/client-pages/referrals/apply-code-form.tsx index 23fea1116..2f9be981e 100644 --- a/apps/trading/client-pages/referrals/apply-code-form.tsx +++ b/apps/trading/client-pages/referrals/apply-code-form.tsx @@ -32,6 +32,19 @@ const validateCode = (value: string, t: ReturnType) => { 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 ; + } + + return ; +}; + 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 ; - } - // show "code applied" message when successfully applied if (status === 'successful') { return ( @@ -243,13 +248,13 @@ export const ApplyCodeForm = () => { ) : null} + {/* TODO: Re-check plural forms once i18n is updated */} {previewData ? (
-

- {t( - 'You are joining the group shown, but will not have access to benefits until you have completed at least {{count}} epochs.', - { count: nextBenefitTierEpochsValue } - )} +

+ {t('referralApplyPreviewMessage', { + count: nextBenefitTierEpochsValue, + })}

diff --git a/apps/trading/client-pages/referrals/hooks/ReferralSetStats.graphql b/apps/trading/client-pages/referrals/hooks/ReferralSetStats.graphql index 7d0fba752..d13d28d90 100644 --- a/apps/trading/client-pages/referrals/hooks/ReferralSetStats.graphql +++ b/apps/trading/client-pages/referrals/hooks/ReferralSetStats.graphql @@ -11,6 +11,7 @@ query ReferralSetStats($code: ID!, $epoch: Int) { rewardsMultiplier rewardsFactorMultiplier referrerTakerVolume + wasEligible } } } diff --git a/apps/trading/client-pages/referrals/hooks/StakeAvailable.graphql b/apps/trading/client-pages/referrals/hooks/StakeAvailable.graphql new file mode 100644 index 000000000..5ccbb4b30 --- /dev/null +++ b/apps/trading/client-pages/referrals/hooks/StakeAvailable.graphql @@ -0,0 +1,10 @@ +query StakeAvailable($partyId: ID!) { + party(id: $partyId) { + stakingSummary { + currentStakeAvailable + } + } + networkParameter(key: "referralProgram.minStakedVegaTokens") { + value + } +} diff --git a/apps/trading/client-pages/referrals/hooks/__generated__/ReferralSetStats.ts b/apps/trading/client-pages/referrals/hooks/__generated__/ReferralSetStats.ts index 7647ede92..d60b1fef4 100644 --- a/apps/trading/client-pages/referrals/hooks/__generated__/ReferralSetStats.ts +++ b/apps/trading/client-pages/referrals/hooks/__generated__/ReferralSetStats.ts @@ -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 } } } diff --git a/apps/trading/client-pages/referrals/hooks/__generated__/StakeAvailable.ts b/apps/trading/client-pages/referrals/hooks/__generated__/StakeAvailable.ts new file mode 100644 index 000000000..7acc953c2 --- /dev/null +++ b/apps/trading/client-pages/referrals/hooks/__generated__/StakeAvailable.ts @@ -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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(StakeAvailableDocument, options); + } +export function useStakeAvailableLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(StakeAvailableDocument, options); + } +export type StakeAvailableQueryHookResult = ReturnType; +export type StakeAvailableLazyQueryHookResult = ReturnType; +export type StakeAvailableQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx b/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx new file mode 100644 index 000000000..6d52f5725 --- /dev/null +++ b/apps/trading/client-pages/referrals/hooks/use-referral-toasts.tsx @@ -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: ( + <> + {t('Referral code no longer valid')} +

+ {t( + 'Your referral code is no longer valid as the referrer no longer meets the minimum requirements.' + )} +

+

+ +

+ + ), + onClose: () => + updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, { + hidden: true, + }), + }; + setToast(nonEligibleReferralToast); + } + }, [data, epoch, hasToast, loading, navigate, setToast, t, updateToast]); +}; diff --git a/apps/trading/client-pages/referrals/hooks/use-referral.ts b/apps/trading/client-pages/referrals/hooks/use-referral.ts index 67f471bad..8265bafce 100644 --- a/apps/trading/client-pages/referrals/hooks/use-referral.ts +++ b/apps/trading/client-pages/referrals/hooks/use-referral.ts @@ -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; diff --git a/apps/trading/client-pages/referrals/hooks/use-stake-available.ts b/apps/trading/client-pages/referrals/hooks/use-stake-available.ts index af0b8836c..993ddeb6f 100644 --- a/apps/trading/client-pages/referrals/hooks/use-stake-available.ts +++ b/apps/trading/client-pages/referrals/hooks/use-stake-available.ts @@ -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, }; }; diff --git a/apps/trading/client-pages/referrals/referral-statistics.tsx b/apps/trading/client-pages/referrals/referral-statistics.tsx index 5830812e2..b7f47a72d 100644 --- a/apps/trading/client-pages/referrals/referral-statistics.tsx +++ b/apps/trading/client-pages/referrals/referral-statistics.tsx @@ -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 ; + return ( + <> + ; + {!referee.isEligible && } + + ); } if (referrer?.code) { - return ; + return ( + <> + ; + + + ); } return ; @@ -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}% ); + const stakingMultiplierTile = ( + {t('{{amount}} $VEGA staked', { + amount: addDecimalsFormatNumber( + stakeAvailable?.toString() || 0, + 18 + ), + })} + + } > {multiplier || t('None')} @@ -333,28 +356,53 @@ export const Statistics = ({ ); - const [collapsed, setCollapsed] = useState(false); - const tableRef = useRef(null); - useLayoutEffect(() => { - if ((tableRef.current?.getBoundingClientRect().height || 0) > 384) { - setCollapsed(true); - } - }, []); + const eligibilityWarning = as === 'referee' && !isEligible && ( +
+

{t('Referral code no longer valid')}

+

+ {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.' + )} +

+
+ ); return ( - <> - {/* Stats tiles */} +
{as === 'referrer' && referrerTiles} {as === 'referee' && refereeTiles}
+ {eligibilityWarning} +
+ ); +}; + +export const RefereesTable = ({ + data, + program, +}: { + data: NonNullable['data']>; + program: ReturnType; +}) => { + const t = useT(); + const [collapsed, setCollapsed] = useState(false); + const tableRef = useRef(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 && (

{t('Referees')}

, + element: , }, ], }, diff --git a/apps/trading/pages/toasts-manager.tsx b/apps/trading/pages/toasts-manager.tsx index 5e424a121..25ad557cc 100644 --- a/apps/trading/pages/toasts-manager.tsx +++ b/apps/trading/pages/toasts-manager.tsx @@ -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 ; diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index e62fcdbbd..d593bcc0a 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -203,6 +203,8 @@ "Ready to trade": "Ready to trade", "Ready to trade with real funds? <0>Switch to Mainnet": "Ready to trade with real funds? <0>Switch to Mainnet", "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 (last {{count}} epochs)",