Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom ref codes (Merits) #2631

Merged
merged 1 commit into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion configs/envs/.env.eth_sepolia
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
17 changes: 12 additions & 5 deletions lib/contexts/rewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
};

Expand All @@ -70,7 +70,7 @@ const initialState = {
openLoginModal: () => {},
closeLoginModal: () => {},
saveApiToken: () => {},
login: async() => ({}),
login: async() => ({ isNewUser: false, reward: null }),
claim: async() => {},
};

Expand Down Expand Up @@ -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 });
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions types/api/rewards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export type RewardsConfigResponse = {

export type RewardsCheckRefCodeResponse = {
valid: boolean;
is_custom: boolean;
reward: string | null;
};

export type RewardsNonceResponse = {
Expand Down
31 changes: 16 additions & 15 deletions ui/rewards/login/RewardsLoginModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
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';

Expand All @@ -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 });
Expand Down Expand Up @@ -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>
Expand Down
13 changes: 8 additions & 5 deletions ui/rewards/login/steps/CongratsStepContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 && (
Expand Down
34 changes: 17 additions & 17 deletions ui/rewards/login/steps/LoginStepContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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();

Expand All @@ -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) {
Expand Down Expand Up @@ -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>
</>
) }
Expand Down
Loading