Commit 7b45a38a authored by Max Alekseenko's avatar Max Alekseenko Committed by GitHub

Support custom ref codes (Merits) (#2631)

support custom ref codes
parent 35135fff
...@@ -68,4 +68,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-prod-2.blockscout.com ...@@ -68,4 +68,4 @@ NEXT_PUBLIC_STATS_API_HOST=https://stats-sepolia.k8s-prod-2.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
\ No newline at end of file
...@@ -45,7 +45,7 @@ type TRewardsContext = { ...@@ -45,7 +45,7 @@ type TRewardsContext = {
openLoginModal: () => void; openLoginModal: () => void;
closeLoginModal: () => void; closeLoginModal: () => void;
saveApiToken: (token: string | undefined) => void; saveApiToken: (token: string | undefined) => void;
login: (refCode: string) => Promise<{ isNewUser?: boolean; invalidRefCodeError?: boolean }>; login: (refCode: string) => Promise<{ isNewUser: boolean; reward: string | null; invalidRefCodeError?: boolean }>;
claim: () => Promise<void>; claim: () => Promise<void>;
}; };
...@@ -70,7 +70,7 @@ const initialState = { ...@@ -70,7 +70,7 @@ const initialState = {
openLoginModal: () => {}, openLoginModal: () => {},
closeLoginModal: () => {}, closeLoginModal: () => {},
saveApiToken: () => {}, saveApiToken: () => {},
login: async() => ({}), login: async() => ({ isNewUser: false, reward: null }),
claim: async() => {}, claim: async() => {},
}; };
...@@ -216,10 +216,14 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -216,10 +216,14 @@ export function RewardsContextProvider({ children }: Props) {
apiFetch('rewards_nonce') as Promise<RewardsNonceResponse>, apiFetch('rewards_nonce') as Promise<RewardsNonceResponse>,
refCode ? refCode ?
apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<RewardsCheckRefCodeResponse> : apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<RewardsCheckRefCodeResponse> :
Promise.resolve({ valid: true }), Promise.resolve({ valid: true, reward: null }),
]); ]);
if (!checkCodeResponse.valid) { if (!checkCodeResponse.valid) {
return { invalidRefCodeError: true }; return {
invalidRefCodeError: true,
isNewUser: false,
reward: null,
};
} }
const message = getMessageToSign(address, nonceResponse.nonce, checkUserQuery.data?.exists, refCode); const message = getMessageToSign(address, nonceResponse.nonce, checkUserQuery.data?.exists, refCode);
const signature = await signMessageAsync({ message }); const signature = await signMessageAsync({ message });
...@@ -234,7 +238,10 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -234,7 +238,10 @@ export function RewardsContextProvider({ children }: Props) {
}, },
}) as RewardsLoginResponse; }) as RewardsLoginResponse;
saveApiToken(loginResponse.token); saveApiToken(loginResponse.token);
return { isNewUser: loginResponse.created }; return {
isNewUser: loginResponse.created,
reward: checkCodeResponse.reward,
};
} catch (_error) { } catch (_error) {
errorToast(_error); errorToast(_error);
throw _error; throw _error;
......
...@@ -12,6 +12,8 @@ export type RewardsConfigResponse = { ...@@ -12,6 +12,8 @@ export type RewardsConfigResponse = {
export type RewardsCheckRefCodeResponse = { export type RewardsCheckRefCodeResponse = {
valid: boolean; valid: boolean;
is_custom: boolean;
reward: string | null;
}; };
export type RewardsNonceResponse = { export type RewardsNonceResponse = {
......
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean, useDisclosure } from '@chakra-ui/react'; import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { Screen } from 'ui/snippets/auth/types'; import type { Screen } from 'ui/snippets/auth/types';
...@@ -25,24 +25,25 @@ const RewardsLoginModal = () => { ...@@ -25,24 +25,25 @@ const RewardsLoginModal = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { isLoginModalOpen, closeLoginModal } = useRewardsContext(); const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const [ isLoginStep, setIsLoginStep ] = useBoolean(true); const [ isLoginStep, setIsLoginStep ] = useState(true);
const [ isReferral, setIsReferral ] = useBoolean(false); const [ isReferral, setIsReferral ] = useState(false);
const [ authModalInitialScreen, setAuthModalInitialScreen ] = React.useState<Screen>(); const [ customReferralReward, setCustomReferralReward ] = useState<string | null>(null);
const [ authModalInitialScreen, setAuthModalInitialScreen ] = useState<Screen>();
const authModal = useDisclosure(); const authModal = useDisclosure();
useEffect(() => { useEffect(() => {
if (!isLoginModalOpen) { if (!isLoginModalOpen) {
setIsLoginStep.on(); setIsLoginStep(true);
setIsReferral.off(); setIsReferral(false);
setCustomReferralReward(null);
} }
}, [ isLoginModalOpen, setIsLoginStep, setIsReferral ]); }, [ isLoginModalOpen ]);
const goNext = useCallback((isReferral: boolean) => { const goNext = useCallback((isReferral: boolean, reward: string | null) => {
if (isReferral) { setIsReferral(isReferral);
setIsReferral.on(); setCustomReferralReward(reward);
} setIsLoginStep(false);
setIsLoginStep.off(); }, []);
}, [ setIsLoginStep, setIsReferral ]);
const handleAuthModalOpen = useCallback((isAuth: boolean, trySharedLogin?: boolean) => { const handleAuthModalOpen = useCallback((isAuth: boolean, trySharedLogin?: boolean) => {
setAuthModalInitialScreen({ type: 'connect_wallet', isAuth, loginToRewards: trySharedLogin }); setAuthModalInitialScreen({ type: 'connect_wallet', isAuth, loginToRewards: trySharedLogin });
...@@ -74,7 +75,7 @@ const RewardsLoginModal = () => { ...@@ -74,7 +75,7 @@ const RewardsLoginModal = () => {
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
{ isLoginStep ? { isLoginStep ?
<LoginStepContent goNext={ goNext } openAuthModal={ handleAuthModalOpen } closeModal={ closeLoginModal }/> : <LoginStepContent goNext={ goNext } openAuthModal={ handleAuthModalOpen } closeModal={ closeLoginModal }/> :
<CongratsStepContent isReferral={ isReferral }/> <CongratsStepContent isReferral={ isReferral } customReferralReward={ customReferralReward }/>
} }
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
......
...@@ -12,14 +12,17 @@ import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy'; ...@@ -12,14 +12,17 @@ import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy';
type Props = { type Props = {
isReferral: boolean; isReferral: boolean;
customReferralReward: string | null;
}; };
const CongratsStepContent = ({ isReferral }: Props) => { const CongratsStepContent = ({ isReferral, customReferralReward }: Props) => {
const { referralsQuery, rewardsConfigQuery } = useRewardsContext(); const { referralsQuery, rewardsConfigQuery } = useRewardsContext();
const registrationReward = rewardsConfigQuery.data?.rewards.registration; const registrationReward = Number(rewardsConfigQuery.data?.rewards.registration);
const registrationWithReferralReward = rewardsConfigQuery.data?.rewards.registration_with_referral; const registrationWithReferralReward = customReferralReward ?
const referralReward = Number(registrationWithReferralReward) - Number(registrationReward); Number(customReferralReward) + registrationReward :
Number(rewardsConfigQuery.data?.rewards.registration_with_referral);
const referralReward = registrationWithReferralReward - registrationReward;
const refLink = referralsQuery.data?.link || 'N/A'; const refLink = referralsQuery.data?.link || 'N/A';
const shareText = `I joined the @blockscout Merits Program and got my first ${ registrationReward || 'N/A' } #Merits! Use this link for a sign-up bonus and start earning rewards with @blockscout block explorer.\n\n${ refLink }`; // eslint-disable-line max-len const shareText = `I joined the @blockscout Merits Program and got my first ${ registrationReward || 'N/A' } #Merits! Use this link for a sign-up bonus and start earning rewards with @blockscout block explorer.\n\n${ refLink }`; // eslint-disable-line max-len
...@@ -41,7 +44,7 @@ const CongratsStepContent = ({ isReferral }: Props) => { ...@@ -41,7 +44,7 @@ const CongratsStepContent = ({ isReferral }: Props) => {
<MeritsIcon boxSize={{ base: isReferral ? 8 : 12, md: 12 }} mr={{ base: isReferral ? 1 : 2, md: 2 }}/> <MeritsIcon boxSize={{ base: isReferral ? 8 : 12, md: 12 }} mr={{ base: isReferral ? 1 : 2, md: 2 }}/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }> <Skeleton isLoaded={ !rewardsConfigQuery.isLoading }>
<Text fontSize={{ base: isReferral ? '24px' : '30px', md: '30px' }} fontWeight="700" color={ textColor }> <Text fontSize={{ base: isReferral ? '24px' : '30px', md: '30px' }} fontWeight="700" color={ textColor }>
+{ rewardsConfigQuery.data?.rewards[ isReferral ? 'registration_with_referral' : 'registration' ] || 'N/A' } +{ (isReferral ? registrationWithReferralReward : registrationReward) || 'N/A' }
</Text> </Text>
</Skeleton> </Skeleton>
{ isReferral && ( { isReferral && (
......
...@@ -13,7 +13,7 @@ import LinkExternal from 'ui/shared/links/LinkExternal'; ...@@ -13,7 +13,7 @@ import LinkExternal from 'ui/shared/links/LinkExternal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
type Props = { type Props = {
goNext: (isReferral: boolean) => void; goNext: (isReferral: boolean, reward: string | null) => void;
closeModal: () => void; closeModal: () => void;
openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void; openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void;
}; };
...@@ -23,9 +23,9 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -23,9 +23,9 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const { connect, isConnected, address } = useWallet({ source: 'Merits' }); const { connect, isConnected, address } = useWallet({ source: 'Merits' });
const savedRefCode = cookies.get(cookies.NAMES.REWARDS_REFERRAL_CODE); const savedRefCode = cookies.get(cookies.NAMES.REWARDS_REFERRAL_CODE);
const [ isRefCodeUsed, setIsRefCodeUsed ] = useBoolean(Boolean(savedRefCode)); const [ isRefCodeUsed, setIsRefCodeUsed ] = useBoolean(Boolean(savedRefCode));
const [ isLoading, setIsLoading ] = useBoolean(false); const [ isLoading, setIsLoading ] = useState(false);
const [ refCode, setRefCode ] = useState(savedRefCode || ''); const [ refCode, setRefCode ] = useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false); const [ refCodeError, setRefCodeError ] = useState(false);
const { login, checkUserQuery, rewardsConfigQuery } = useRewardsContext(); const { login, checkUserQuery, rewardsConfigQuery } = useRewardsContext();
const profileQuery = useProfileQuery(); const profileQuery = useProfileQuery();
...@@ -51,30 +51,27 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -51,30 +51,27 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const loginToRewardsProgram = useCallback(async() => { const loginToRewardsProgram = useCallback(async() => {
try { try {
setRefCodeError.off(); setRefCodeError(false);
setIsLoading.on(); setIsLoading(true);
const { isNewUser, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : ''); const { isNewUser, reward, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : '');
if (invalidRefCodeError) { if (invalidRefCodeError) {
setRefCodeError.on(); setRefCodeError(true);
} else { } else {
if (isNewUser) { if (isNewUser) {
goNext(isRefCodeUsed); goNext(isRefCodeUsed, reward);
} else { } else {
closeModal(); closeModal();
router.push({ pathname: '/account/merits' }, undefined, { shallow: true }); router.push({ pathname: '/account/merits' }, undefined, { shallow: true });
} }
} }
} catch (error) {} } catch (error) {}
setIsLoading.off(); setIsLoading(false);
}, [ login, goNext, setIsLoading, router, closeModal, refCode, setRefCodeError, isRefCodeUsed, isSignUp ]); }, [ login, goNext, router, closeModal, refCode, isRefCodeUsed, isSignUp ]);
useEffect(() => { useEffect(() => {
if (isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6) { const isInvalid = isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6 && refCode.length !== 12;
setRefCodeError.on(); setRefCodeError(isInvalid);
} else { }, [ refCode, isRefCodeUsed, isSignUp ]);
setRefCodeError.off();
}
}, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps
const handleButtonClick = React.useCallback(() => { const handleButtonClick = React.useCallback(() => {
if (canTrySharedLogin) { if (canTrySharedLogin) {
...@@ -145,7 +142,10 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -145,7 +142,10 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
<FormInputPlaceholder text="Code"/> <FormInputPlaceholder text="Code"/>
</FormControl> </FormControl>
<Text fontSize="sm" variant="secondary" mt={ 1 } color={ refCodeError ? 'red.500' : undefined }> <Text fontSize="sm" variant="secondary" mt={ 1 } color={ refCodeError ? 'red.500' : undefined }>
{ refCodeError ? 'Incorrect code or format' : 'The code should be in format XXXXXX' } { refCodeError ?
'Incorrect code or format (6 or 12 characters)' :
'The code should be in format XXXXXX'
}
</Text> </Text>
</> </>
) } ) }
......
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