Compare commits

...
14 changed files with 713 additions and 66 deletions
@@ -10,7 +10,7 @@ import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
@@ -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,14 +68,29 @@ 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,
});
/**
* Validates the set a user tries to apply to.
*/
const validateSet = useCallback(() => {
if (
codeField &&
!previewLoading &&
previewData &&
!previewData.isEligible
) {
return t('The code is no longer valid.');
}
if (codeField && !previewLoading && !previewData) {
return t('The code is invalid');
}
return true;
}, [codeField, previewData, previewLoading, t]);
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
@@ -144,11 +172,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 (
@@ -205,7 +228,10 @@ export const ApplyCodeForm = () => {
return (
<>
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8">
<div
data-testid="referral-apply-code-form"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
>
<h3 className="calt mb-4 text-center text-2xl">
{t('Apply a referral code')}
</h3>
@@ -224,7 +250,11 @@ export const ApplyCodeForm = () => {
hasError={Boolean(errors.code)}
{...register('code', {
required: t('You have to provide a code to apply it.'),
validate: (value) => validateCode(value, t),
validate: (value) => {
const err = validateCode(value, t);
if (err !== true) return err;
return validateSet();
},
})}
placeholder="Enter a code"
className="bg-vega-clight-900 dark:bg-vega-cdark-700 mb-2"
@@ -238,18 +268,18 @@ export const ApplyCodeForm = () => {
</InputError>
)}
</div>
{previewLoading && !previewData ? (
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
<div className="mt-10">
<Loader />
</div>
) : null}
{previewData ? (
{/* TODO: Re-check plural forms once i18n is updated */}
{previewData && previewData.isEligible ? (
<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>
@@ -39,7 +39,10 @@ export const CreateCodeForm = () => {
const { pubKey, isReadOnly } = useVegaWallet();
return (
<div className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg">
<div
data-testid="referral-create-code-form"
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
>
<h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')}
</h3>
@@ -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,111 @@
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 { matchPath, useLocation, 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 { pathname } = useLocation();
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={() => {
const matched = matchPath(
Routes.REFERRALS_APPLY_CODE,
pathname
);
if (!matched) navigate(Routes.REFERRALS_APPLY_CODE);
updateToast(NON_ELIGIBLE_REFERRAL_SET_TOAST_ID + epoch, {
hidden: true,
});
}}
>
{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,
pathname,
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,35 @@
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;
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,
};
};
@@ -0,0 +1,374 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
type ReferralProgramQuery,
} from './hooks/__generated__/CurrentReferralProgram';
import {
ReferralSetsDocument,
type ReferralSetsQueryVariables,
type ReferralSetsQuery,
} from './hooks/__generated__/ReferralSets';
import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
type RefereesQueryVariables,
type RefereesQuery,
} from './hooks/__generated__/Referees';
import { MemoryRouter } from 'react-router-dom';
const MOCK_PUBKEY =
'1234567890123456789012345678901234567890123456789012345678901234';
const MOCK_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '1',
},
},
};
const MOCK_NON_ELIGIBILE_STAKE_AVAILABLE: StakeAvailableQuery = {
networkParameter: {
__typename: 'NetworkParameter',
value: '1',
},
party: {
__typename: 'Party',
stakingSummary: {
__typename: 'StakingSummary',
currentStakeAvailable: '0',
},
},
};
const MOCK_REFERRAL_PROGRAM: ReferralProgramQuery = {
currentReferralProgram: {
__typename: 'CurrentReferralProgram',
benefitTiers: [
{
__typename: 'BenefitTier',
minimumEpochs: 1,
minimumRunningNotionalTakerVolume: '0',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
{
__typename: 'BenefitTier',
minimumEpochs: 2,
minimumRunningNotionalTakerVolume: '10',
referralDiscountFactor: '0.02',
referralRewardFactor: '0.02',
},
],
endOfProgramTimestamp: '202411012023-11-26T05:58:24.045158Z',
id: '123',
stakingTiers: [
{
__typename: 'StakingTier',
minimumStakedTokens: '100',
referralRewardMultiplier: '1',
},
{
__typename: 'StakingTier',
minimumStakedTokens: '1000',
referralRewardMultiplier: '2',
},
],
version: 2,
windowLength: 3,
endedAt: null,
},
};
const MOCK_REFERRER_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer: MOCK_PUBKEY,
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFERREE_SET: ReferralSetsQuery = {
referralSets: {
__typename: 'ReferralSetConnection',
edges: [
{
__typename: 'ReferralSetEdge',
node: {
__typename: 'ReferralSet',
createdAt: '2023-11-26T05:58:24.045158Z',
id: '3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
referrer:
'1111111111111111111111111111111111111111111111111111111111111111',
updatedAt: '2023-11-26T05:58:24.045158Z',
},
},
],
},
};
const MOCK_REFEREES: RefereesQuery = {
referralSetReferees: {
__typename: 'ReferralSetRefereeConnection',
edges: [
{
node: {
atEpoch: 1,
joinedAt: '2023-11-21T14:17:09.257235Z',
refereeId:
'0987654321098765432109876543210987654321098765432109876543219876',
referralSetId:
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
totalRefereeGeneratedRewards: '1234',
totalRefereeNotionalTakerVolume: '5678',
__typename: 'ReferralSetReferee',
},
},
],
},
};
const programMock: MockedResponse<ReferralProgramQuery> = {
request: {
query: ReferralProgramDocument,
},
result: { data: MOCK_REFERRAL_PROGRAM },
};
const referralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERRER_SET,
},
};
const noReferralSetAsReferrerMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referrer: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const referralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: MOCK_REFERREE_SET,
},
};
const noReferralSetAsRefereeMock: MockedResponse<
ReferralSetsQuery,
ReferralSetsQueryVariables
> = {
request: {
query: ReferralSetsDocument,
variables: {
referee: MOCK_PUBKEY,
},
},
result: {
data: { referralSets: { edges: [] } },
},
};
const stakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_STAKE_AVAILABLE,
},
};
const nonEligibleStakeAvailableMock: MockedResponse<
StakeAvailableQuery,
StakeAvailableQueryVariables
> = {
request: {
query: StakeAvailableDocument,
variables: {
partyId: MOCK_PUBKEY,
},
},
result: {
data: MOCK_NON_ELIGIBILE_STAKE_AVAILABLE,
},
};
const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
request: {
query: RefereesDocument,
variables: {
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
aggregationEpochs:
MOCK_REFERRAL_PROGRAM.currentReferralProgram?.windowLength,
},
},
result: {
data: MOCK_REFEREES,
},
};
jest.mock('@vegaprotocol/wallet', () => {
return {
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: () => {
const ctx: Partial<VegaWalletContextShape> = {
pubKey: MOCK_PUBKEY,
};
return ctx;
},
};
});
describe('ReferralStatistics', () => {
it('displays create code when no data has been found for given pubkey', () => {
const { queryByTestId } = render(
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
);
expect(queryByTestId('referral-create-code-form')).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render(
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
});
});
});
@@ -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,60 @@ 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
data-testid="referral-eligibility-warning"
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
data-testid="referral-statistics"
data-as={as}
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)",