import { Input, InputError, Loader, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import type { FieldValues } from 'react-hook-form'; 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 { useCallback, useEffect, useRef, useState } from 'react'; import { RainbowButton } from './buttons'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useIsInReferralSet, useReferral } from './hooks/use-referral'; import { Routes } from '../../lib/links'; import { useTransactionEventSubscription } from '@vegaprotocol/web3'; import { Statistics, useStats } from './referral-statistics'; import { useReferralProgram } from './hooks/use-referral-program'; import { ns, useT } from '../../lib/use-t'; import { useFundsAvailable } from './hooks/use-funds-available'; import { ViewType, useSidebar } from '../../components/sidebar'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { QUSDTooltip } from './qusd-tooltip'; import { Trans } from 'react-i18next'; const RELOAD_DELAY = 3000; const SPAM_PROTECTION_ERR = 'SPAM_PROTECTION_ERR'; const SpamProtectionErr = ({ requiredFunds, }: { requiredFunds?: string | number | bigint; }) => { if (!requiredFunds) return null; // eslint-disable-next-line react/jsx-no-undef return ( ]} ns={ns} /> ); }; const validateCode = (value: string, t: ReturnType) => { const number = +`0x${value}`; if (!value || value.length !== 64) { return t('Code must be 64 characters in length'); } else if (Number.isNaN(number)) { return t('Code must be be valid hex'); } return true; }; export const ApplyCodeFormContainer = ({ onSuccess, }: { onSuccess?: () => void; }) => { const { pubKey } = useVegaWallet(); const isInReferralSet = useIsInReferralSet(pubKey); // Navigate to the index page when already in the referral set. if (isInReferralSet) { return ; } return ; }; export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => { const t = useT(); const program = useReferralProgram(); const navigate = useNavigate(); const openWalletDialog = useVegaWalletDialogStore( (store) => store.openVegaWalletDialog ); const [status, setStatus] = useState< 'requested' | 'no-funds' | 'successful' | null >(null); const txHash = useRef(null); const { isReadOnly, pubKey, sendTx } = useVegaWallet(); const { isEligible, requiredFunds } = useFundsAvailable(); const currentRouteId = useGetCurrentRouteId(); const setViews = useSidebar((s) => s.setViews); const { register, handleSubmit, formState: { errors }, setValue, setError, watch, } = useForm(); const [params] = useSearchParams(); const codeField = watch('code'); const { data: previewData, loading: previewLoading } = useReferral({ code: validateCode(codeField, t) ? codeField : undefined, }); /** * Validates if a connected party can apply a code (min funds span protection) */ const validateFundsAvailable = useCallback(() => { if (requiredFunds && !isEligible) { const err = SPAM_PROTECTION_ERR; return err; } return true; }, [isEligible, requiredFunds]); /** * 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); }, [params, setValue]); useEffect(() => { const err = validateFundsAvailable(); if (err !== true) { setStatus('no-funds'); } else { setStatus(null); } }, [isEligible, validateFundsAvailable]); const onSubmit = ({ code }: FieldValues) => { if (isReadOnly || !pubKey || !code || code.length === 0) { return; } setStatus('requested'); sendTx(pubKey, { applyReferralCode: { id: code as string, }, }) .then((res) => { if (!res) { setError('code', { type: 'required', message: t('The transaction could not be sent'), }); } if (res) { txHash.current = res.transactionHash.toLowerCase(); } }) .catch((err) => { if (err.message.includes('user rejected')) { setStatus(null); } else { setStatus(null); setError('code', { type: 'required', message: err instanceof Error ? err.message : t('Your code has been rejected'), }); } }); }; useTransactionEventSubscription({ variables: { partyId: pubKey || '' }, skip: !pubKey, fetchPolicy: 'no-cache', onData: ({ data: result }) => result.data?.busEvents?.forEach((event) => { if (event.event.__typename === 'TransactionResult') { const hash = event.event.hash.toLowerCase(); if (txHash.current && txHash.current === hash) { const err = event.event.error; const status = event.event.status; if (err) { setStatus(null); setError('code', { type: 'required', message: err, }); } if (status && !err) { setStatus('successful'); } } } }), }); const { epochsValue, nextBenefitTierValue } = useStats({ program }); // go to main page when successfully applied useEffect(() => { if (status === 'successful') { setTimeout(() => { if (onSuccess) onSuccess(); navigate(Routes.REFERRALS); }, RELOAD_DELAY); } }, [navigate, onSuccess, status]); // show "code applied" message when successfully applied if (status === 'successful') { return (

{' '} {t('Code applied')}

); } const getButtonProps = () => { if (!pubKey) { return { disabled: false, children: t('Connect wallet'), type: 'button' as ButtonHTMLAttributes['type'], onClick: ((event) => { event.preventDefault(); openWalletDialog(); }) as MouseEventHandler, }; } if (isReadOnly) { return { disabled: true, children: t('Apply a code'), type: 'submit' as ButtonHTMLAttributes['type'], }; } if (status === 'no-funds') { return { disabled: false, children: t('Deposit funds'), type: 'button' as ButtonHTMLAttributes['type'], onClick: ((event) => { event.preventDefault(); setViews({ type: ViewType.Deposit }, currentRouteId); }) as MouseEventHandler, }; } if (status === 'requested') { return { disabled: true, children: t('Confirm in wallet...'), type: 'submit' as ButtonHTMLAttributes['type'], }; } return { disabled: false, children: t('Apply a code'), type: 'submit' as ButtonHTMLAttributes['type'], }; }; const nextBenefitTierEpochsValue = nextBenefitTierValue ? nextBenefitTierValue.epochs - epochsValue : 0; return ( <>

{t('Apply a referral code')}

{t( 'Apply a referral code to access the discount benefits of the current program.' )}

{status === 'no-funds' ? ( ) : ( errors.code && ( {errors.code.message === SPAM_PROTECTION_ERR ? ( ) : ( errors.code.message?.toString() )} ) )}
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
) : null} {/* TODO: Re-check plural forms once i18n is updated */} {previewData && previewData.isEligible ? (

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

) : null} ); };