Commit 079b6d5c authored by Max Alekseenko's avatar Max Alekseenko

implement referral logic

parent 7c58695e
...@@ -92,6 +92,7 @@ import type { ...@@ -92,6 +92,7 @@ import type {
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { import type {
RewardsConfigResponse, RewardsConfigResponse,
RewardsCheckRefCodeResponse,
RewardsNonceResponse, RewardsNonceResponse,
RewardsCheckUserResponse, RewardsCheckUserResponse,
RewardsLoginResponse, RewardsLoginResponse,
...@@ -335,6 +336,12 @@ export const RESOURCES = { ...@@ -335,6 +336,12 @@ export const RESOURCES = {
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint, endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath, basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
}, },
rewards_check_ref_code: {
path: '/api/v1/auth/code/:code',
pathParams: [ 'code' as const ],
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_nonce: { rewards_nonce: {
path: '/api/v1/auth/nonce', path: '/api/v1/auth/nonce',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint, endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
...@@ -1228,6 +1235,7 @@ Q extends 'address_mud_record' ? AddressMudRecord : ...@@ -1228,6 +1235,7 @@ Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_config' ? RewardsConfigResponse : Q extends 'rewards_config' ? RewardsConfigResponse :
Q extends 'rewards_check_ref_code' ? RewardsCheckRefCodeResponse :
Q extends 'rewards_nonce' ? RewardsNonceResponse : Q extends 'rewards_nonce' ? RewardsNonceResponse :
Q extends 'rewards_check_user' ? RewardsCheckUserResponse : Q extends 'rewards_check_user' ? RewardsCheckUserResponse :
Q extends 'rewards_login' ? RewardsLoginResponse : Q extends 'rewards_login' ? RewardsLoginResponse :
......
...@@ -7,6 +7,10 @@ export type RewardsConfigResponse = { ...@@ -7,6 +7,10 @@ export type RewardsConfigResponse = {
}; };
}; };
export type RewardsCheckRefCodeResponse = {
valid: boolean;
};
export type RewardsNonceResponse = { export type RewardsNonceResponse = {
nonce: string; nonce: string;
}; };
......
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean } from '@chakra-ui/react'; import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean } from '@chakra-ui/react';
import React, { useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
...@@ -14,12 +14,21 @@ const RewardsLoginModal = () => { ...@@ -14,12 +14,21 @@ const RewardsLoginModal = () => {
const { isLoginModalOpen, closeLoginModal } = useRewardsContext(); const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const [ isLoginStep, setIsLoginStep ] = useBoolean(true); const [ isLoginStep, setIsLoginStep ] = useBoolean(true);
const [ isReferral, setIsReferral ] = useBoolean(false);
useEffect(() => { useEffect(() => {
if (!isLoginModalOpen) { if (!isLoginModalOpen) {
setIsLoginStep.on(); setIsLoginStep.on();
setIsReferral.off();
} }
}, [ isLoginModalOpen, setIsLoginStep ]); }, [ isLoginModalOpen, setIsLoginStep, setIsReferral ]);
const goNext = useCallback((isReferral: boolean) => {
if (isReferral) {
setIsReferral.on();
}
setIsLoginStep.off();
}, [ setIsLoginStep, setIsReferral ]);
return ( return (
<Modal <Modal
...@@ -36,8 +45,8 @@ const RewardsLoginModal = () => { ...@@ -36,8 +45,8 @@ const RewardsLoginModal = () => {
<ModalCloseButton top={ 6 } right={ 6 }/> <ModalCloseButton top={ 6 } right={ 6 }/>
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
{ isLoginStep ? { isLoginStep ?
<LoginStepContent goNext={ setIsLoginStep.off } closeModal={ closeLoginModal }/> : <LoginStepContent goNext={ goNext } closeModal={ closeLoginModal }/> :
<CongratsStepContent/> <CongratsStepContent isReferral={ isReferral }/>
} }
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
......
...@@ -10,7 +10,11 @@ import CopyField from '../CopyField'; ...@@ -10,7 +10,11 @@ import CopyField from '../CopyField';
import useReferrals from '../useReferrals'; import useReferrals from '../useReferrals';
import useRewardsConfig from '../useRewardsConfig'; import useRewardsConfig from '../useRewardsConfig';
const CongratsStepContent = () => { type Props = {
isReferral: boolean;
}
const CongratsStepContent = ({ isReferral }: Props) => {
const referralsQuery = useReferrals(); const referralsQuery = useReferrals();
const rewardsConfigQuery = useRewardsConfig(); const rewardsConfigQuery = useRewardsConfig();
...@@ -25,12 +29,41 @@ const CongratsStepContent = () => { ...@@ -25,12 +29,41 @@ const CongratsStepContent = () => {
mb={ 8 } mb={ 8 }
> >
<Flex alignItems="center" pl={ 2 } mb={ 4 }> <Flex alignItems="center" pl={ 2 } mb={ 4 }>
<IconSvg name="merits_colored" boxSize={ 16 }/> <IconSvg name="merits_colored" boxSize="72px" m={ -2 }/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }> <Skeleton isLoaded={ !rewardsConfigQuery.isLoading }>
<Text fontSize="30px" fontWeight="700" color="blue.700"> <Text fontSize="30px" fontWeight="700" color="blue.700" ml={ 1 }>
+{ rewardsConfigQuery.data?.rewards.registration } +{ rewardsConfigQuery.data?.rewards[ isReferral ? 'registration_with_referral' : 'registration' ] }
</Text> </Text>
</Skeleton> </Skeleton>
{ isReferral && (
<Flex alignItems="center" h="56px">
<Box w="1px" h="full" bgColor="whiteAlpha.800" mx={ 8 }/>
<Flex flexDirection="column" justifyContent="space-between">
{ [
{
title: 'Registration',
value: rewardsConfigQuery.data?.rewards.registration,
},
{
title: 'Referral program',
value: Number(rewardsConfigQuery.data?.rewards.registration_with_referral) - Number(rewardsConfigQuery.data?.rewards.registration),
},
].map(({ title, value }) => (
<Flex key={ title } alignItems="center">
<IconSvg name="merits_colored" boxSize={ 8 }/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }>
<Text fontSize="sm" fontWeight="700" color="blue.700">
+{ value }
</Text>
</Skeleton>
<Text fontSize="sm" color="blue.700" ml={ 2 }>
{ title }
</Text>
</Flex>
)) }
</Flex>
</Flex>
) }
</Flex> </Flex>
<Flex <Flex
flexDirection="column" flexDirection="column"
......
import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl } from '@chakra-ui/react'; import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
...@@ -9,7 +10,7 @@ import useWallet from 'ui/snippets/walletMenu/useWallet'; ...@@ -9,7 +10,7 @@ import useWallet from 'ui/snippets/walletMenu/useWallet';
import useLogin from '../useLogin'; import useLogin from '../useLogin';
type Props = { type Props = {
goNext: () => void; goNext: (isReferral: boolean) => void;
closeModal: () => void; closeModal: () => void;
}; };
...@@ -18,22 +19,37 @@ const LoginStepContent = ({ goNext, closeModal }: Props) => { ...@@ -18,22 +19,37 @@ const LoginStepContent = ({ goNext, closeModal }: Props) => {
const { connect, isWalletConnected } = useWallet({ source: 'Merits' }); const { connect, isWalletConnected } = useWallet({ source: 'Merits' });
const [ isSwitchChecked, setIsSwitchChecked ] = useBoolean(false); const [ isSwitchChecked, setIsSwitchChecked ] = useBoolean(false);
const [ isLoading, setIsLoading ] = useBoolean(false); const [ isLoading, setIsLoading ] = useBoolean(false);
const [ refCode, setRefCode ] = useState('');
const [ refCodeError, setRefCodeError ] = useBoolean(false);
const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const login = useLogin(); const login = useLogin();
const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setRefCode(event.target.value);
}, []);
const handleLogin = useCallback(async() => { const handleLogin = useCallback(async() => {
try { try {
setRefCodeError.off();
setIsLoading.on(); setIsLoading.on();
const { isNewUser } = await login(); const { isNewUser, invalidRefCodeError } = await login(refCode);
if (isNewUser) { if (invalidRefCodeError) {
goNext(); setRefCodeError.on();
} else { } else {
closeModal(); if (isNewUser) {
router.push({ pathname: '/account/rewards' }, undefined, { shallow: true }); goNext(Boolean(refCode));
} else {
closeModal();
router.push({ pathname: '/account/rewards' }, undefined, { shallow: true });
}
} }
} catch (error) {} } catch (error) {}
setIsLoading.off(); setIsLoading.off();
}, [ login, goNext, setIsLoading, router, closeModal ]); }, [ login, goNext, setIsLoading, router, closeModal, refCode, setRefCodeError ]);
useEffect(() => {
setRefCodeError.off();
}, [ refCode ]); // eslint-disable-line react-hooks/exhaustive-deps
return ( return (
<> <>
...@@ -59,7 +75,13 @@ const LoginStepContent = ({ goNext, closeModal }: Props) => { ...@@ -59,7 +75,13 @@ const LoginStepContent = ({ goNext, closeModal }: Props) => {
</Flex> </Flex>
{ isSwitchChecked && ( { isSwitchChecked && (
<FormControl variant="floating" id="referral-code" mt={ 3 }> <FormControl variant="floating" id="referral-code" mt={ 3 }>
<Input fontWeight="500" borderRadius="12px !important"/> <Input
fontWeight="500"
borderRadius="12px !important"
value={ refCode }
onChange={ handleRefCodeChange }
isInvalid={ refCodeError }
/>
<InputPlaceholder text="Code"/> <InputPlaceholder text="Code"/>
</FormControl> </FormControl>
) } ) }
......
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useAccount, useSignMessage } from 'wagmi'; import { useAccount, useSignMessage } from 'wagmi';
import type { RewardsNonceResponse, RewardsCheckUserResponse, RewardsLoginResponse } from 'types/api/rewards'; import type {
RewardsNonceResponse, RewardsCheckUserResponse,
RewardsLoginResponse, RewardsCheckRefCodeResponse,
} from 'types/api/rewards';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
...@@ -34,16 +37,22 @@ export default function useLogin() { ...@@ -34,16 +37,22 @@ export default function useLogin() {
const { address } = useAccount(); const { address } = useAccount();
const { signMessageAsync } = useSignMessage(); const { signMessageAsync } = useSignMessage();
return useCallback(async() => { return useCallback(async(refCode: string) => {
try { try {
const [ nonceResponse, userResponse ] = await Promise.all([ const [ nonceResponse, userResponse, checkCodeResponse ] = await Promise.all([
apiFetch<'rewards_nonce', RewardsNonceResponse>('rewards_nonce'), apiFetch<'rewards_nonce', RewardsNonceResponse>('rewards_nonce'),
apiFetch<'rewards_check_user', RewardsCheckUserResponse>('rewards_check_user', { pathParams: { address } }), apiFetch<'rewards_check_user', RewardsCheckUserResponse>('rewards_check_user', { pathParams: { address } }),
refCode ?
apiFetch<'rewards_check_ref_code', RewardsCheckRefCodeResponse>('rewards_check_ref_code', { pathParams: { code: refCode } }) :
Promise.resolve({ valid: true }),
]); ]);
if (!address || !('nonce' in nonceResponse) || !('exists' in userResponse)) { if (!address || !('nonce' in nonceResponse) || !('exists' in userResponse) || !('valid' in checkCodeResponse)) {
throw new Error(); throw new Error();
} }
const message = getMessageToSign(address, nonceResponse.nonce, userResponse.exists); if (!checkCodeResponse.valid) {
return { invalidRefCodeError: true };
}
const message = getMessageToSign(address, nonceResponse.nonce, userResponse.exists, refCode);
const signature = await signMessageAsync({ message }); const signature = await signMessageAsync({ message });
const loginResponse = await apiFetch<'rewards_login', RewardsLoginResponse>('rewards_login', { const loginResponse = await apiFetch<'rewards_login', RewardsLoginResponse>('rewards_login', {
fetchParams: { fetchParams: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment