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
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
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
\ No newline at end of file
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
......@@ -45,7 +45,7 @@ type TRewardsContext = {
openLoginModal: () => void;
closeLoginModal: () => 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>;
};
......@@ -70,7 +70,7 @@ const initialState = {
openLoginModal: () => {},
closeLoginModal: () => {},
saveApiToken: () => {},
login: async() => ({}),
login: async() => ({ isNewUser: false, reward: null }),
claim: async() => {},
};
......@@ -216,10 +216,14 @@ export function RewardsContextProvider({ children }: Props) {
apiFetch('rewards_nonce') as Promise<RewardsNonceResponse>,
refCode ?
apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<RewardsCheckRefCodeResponse> :
Promise.resolve({ valid: true }),
Promise.resolve({ valid: true, reward: null }),
]);
if (!checkCodeResponse.valid) {
return { invalidRefCodeError: true };
return {
invalidRefCodeError: true,
isNewUser: false,
reward: null,
};
}
const message = getMessageToSign(address, nonceResponse.nonce, checkUserQuery.data?.exists, refCode);
const signature = await signMessageAsync({ message });
......@@ -234,7 +238,10 @@ export function RewardsContextProvider({ children }: Props) {
},
}) as RewardsLoginResponse;
saveApiToken(loginResponse.token);
return { isNewUser: loginResponse.created };
return {
isNewUser: loginResponse.created,
reward: checkCodeResponse.reward,
};
} catch (_error) {
errorToast(_error);
throw _error;
......
......@@ -12,6 +12,8 @@ export type RewardsConfigResponse = {
export type RewardsCheckRefCodeResponse = {
valid: boolean;
is_custom: boolean;
reward: string | null;
};
export type RewardsNonceResponse = {
......
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react';
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { Screen } from 'ui/snippets/auth/types';
......@@ -25,24 +25,25 @@ const RewardsLoginModal = () => {
const isMobile = useIsMobile();
const { isLoginModalOpen, closeLoginModal } = useRewardsContext();
const [ isLoginStep, setIsLoginStep ] = useBoolean(true);
const [ isReferral, setIsReferral ] = useBoolean(false);
const [ authModalInitialScreen, setAuthModalInitialScreen ] = React.useState<Screen>();
const [ isLoginStep, setIsLoginStep ] = useState(true);
const [ isReferral, setIsReferral ] = useState(false);
const [ customReferralReward, setCustomReferralReward ] = useState<string | null>(null);
const [ authModalInitialScreen, setAuthModalInitialScreen ] = useState<Screen>();
const authModal = useDisclosure();
useEffect(() => {
if (!isLoginModalOpen) {
setIsLoginStep.on();
setIsReferral.off();
setIsLoginStep(true);
setIsReferral(false);
setCustomReferralReward(null);
}
}, [ isLoginModalOpen, setIsLoginStep, setIsReferral ]);
}, [ isLoginModalOpen ]);
const goNext = useCallback((isReferral: boolean) => {
if (isReferral) {
setIsReferral.on();
}
setIsLoginStep.off();
}, [ setIsLoginStep, setIsReferral ]);
const goNext = useCallback((isReferral: boolean, reward: string | null) => {
setIsReferral(isReferral);
setCustomReferralReward(reward);
setIsLoginStep(false);
}, []);
const handleAuthModalOpen = useCallback((isAuth: boolean, trySharedLogin?: boolean) => {
setAuthModalInitialScreen({ type: 'connect_wallet', isAuth, loginToRewards: trySharedLogin });
......@@ -74,7 +75,7 @@ const RewardsLoginModal = () => {
<ModalBody mb={ 0 }>
{ isLoginStep ?
<LoginStepContent goNext={ goNext } openAuthModal={ handleAuthModalOpen } closeModal={ closeLoginModal }/> :
<CongratsStepContent isReferral={ isReferral }/>
<CongratsStepContent isReferral={ isReferral } customReferralReward={ customReferralReward }/>
}
</ModalBody>
</ModalContent>
......
......@@ -12,14 +12,17 @@ import RewardsReadOnlyInputWithCopy from '../../RewardsReadOnlyInputWithCopy';
type Props = {
isReferral: boolean;
customReferralReward: string | null;
};
const CongratsStepContent = ({ isReferral }: Props) => {
const CongratsStepContent = ({ isReferral, customReferralReward }: Props) => {
const { referralsQuery, rewardsConfigQuery } = useRewardsContext();
const registrationReward = rewardsConfigQuery.data?.rewards.registration;
const registrationWithReferralReward = rewardsConfigQuery.data?.rewards.registration_with_referral;
const referralReward = Number(registrationWithReferralReward) - Number(registrationReward);
const registrationReward = Number(rewardsConfigQuery.data?.rewards.registration);
const registrationWithReferralReward = customReferralReward ?
Number(customReferralReward) + registrationReward :
Number(rewardsConfigQuery.data?.rewards.registration_with_referral);
const referralReward = registrationWithReferralReward - registrationReward;
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
......@@ -41,7 +44,7 @@ const CongratsStepContent = ({ isReferral }: Props) => {
<MeritsIcon boxSize={{ base: isReferral ? 8 : 12, md: 12 }} mr={{ base: isReferral ? 1 : 2, md: 2 }}/>
<Skeleton isLoaded={ !rewardsConfigQuery.isLoading }>
<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>
</Skeleton>
{ isReferral && (
......
......@@ -13,7 +13,7 @@ import LinkExternal from 'ui/shared/links/LinkExternal';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
type Props = {
goNext: (isReferral: boolean) => void;
goNext: (isReferral: boolean, reward: string | null) => void;
closeModal: () => void;
openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void;
};
......@@ -23,9 +23,9 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const { connect, isConnected, address } = useWallet({ source: 'Merits' });
const savedRefCode = cookies.get(cookies.NAMES.REWARDS_REFERRAL_CODE);
const [ isRefCodeUsed, setIsRefCodeUsed ] = useBoolean(Boolean(savedRefCode));
const [ isLoading, setIsLoading ] = useBoolean(false);
const [ isLoading, setIsLoading ] = useState(false);
const [ refCode, setRefCode ] = useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false);
const [ refCodeError, setRefCodeError ] = useState(false);
const { login, checkUserQuery, rewardsConfigQuery } = useRewardsContext();
const profileQuery = useProfileQuery();
......@@ -51,30 +51,27 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const loginToRewardsProgram = useCallback(async() => {
try {
setRefCodeError.off();
setIsLoading.on();
const { isNewUser, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : '');
setRefCodeError(false);
setIsLoading(true);
const { isNewUser, reward, invalidRefCodeError } = await login(isSignUp && isRefCodeUsed ? refCode : '');
if (invalidRefCodeError) {
setRefCodeError.on();
setRefCodeError(true);
} else {
if (isNewUser) {
goNext(isRefCodeUsed);
goNext(isRefCodeUsed, reward);
} else {
closeModal();
router.push({ pathname: '/account/merits' }, undefined, { shallow: true });
}
}
} catch (error) {}
setIsLoading.off();
}, [ login, goNext, setIsLoading, router, closeModal, refCode, setRefCodeError, isRefCodeUsed, isSignUp ]);
setIsLoading(false);
}, [ login, goNext, router, closeModal, refCode, isRefCodeUsed, isSignUp ]);
useEffect(() => {
if (isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6) {
setRefCodeError.on();
} else {
setRefCodeError.off();
}
}, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps
const isInvalid = isSignUp && isRefCodeUsed && refCode.length > 0 && refCode.length !== 6 && refCode.length !== 12;
setRefCodeError(isInvalid);
}, [ refCode, isRefCodeUsed, isSignUp ]);
const handleButtonClick = React.useCallback(() => {
if (canTrySharedLogin) {
......@@ -145,7 +142,10 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
<FormInputPlaceholder text="Code"/>
</FormControl>
<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>
</>
) }
......
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