mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
post-review changes
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import type { Feature } from './types';
|
||||
|
||||
import { getEnvValue } from '../utils';
|
||||
import account from './account';
|
||||
import blockchainInteraction from './blockchainInteraction';
|
||||
|
||||
const apiHost = getEnvValue('NEXT_PUBLIC_REWARDS_SERVICE_API_HOST');
|
||||
|
||||
const title = 'Rewards service integration';
|
||||
|
||||
const config: Feature<{ api: { endpoint: string; basePath: string } }> = (() => {
|
||||
if (apiHost) {
|
||||
if (apiHost && account.isEnabled && blockchainInteraction.isEnabled) {
|
||||
return Object.freeze({
|
||||
title,
|
||||
isEnabled: true,
|
||||
|
||||
@@ -837,6 +837,7 @@ const schema = yup
|
||||
|
||||
return isUndefined || valueSchema.isValidSync(data);
|
||||
}),
|
||||
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST: yup.string().test(urlTest),
|
||||
|
||||
// 6. External services envs
|
||||
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID: yup.string(),
|
||||
@@ -844,7 +845,6 @@ const schema = yup
|
||||
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID: yup.string(),
|
||||
NEXT_PUBLIC_MIXPANEL_PROJECT_TOKEN: yup.string(),
|
||||
NEXT_PUBLIC_GROWTH_BOOK_CLIENT_KEY: yup.string(),
|
||||
NEXT_PUBLIC_REWARDS_SERVICE_API_HOST: yup.string().test(urlTest),
|
||||
|
||||
// Misc
|
||||
NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(),
|
||||
|
||||
+1
-1
@@ -796,7 +796,7 @@ The feature enables a "Save with GasHawk" button next to the "Gas used" value on
|
||||
|
||||
### Rewards service API
|
||||
|
||||
This feature enables Blockscout Merits program.
|
||||
This feature enables Blockscout Merits program. It requires that the [My account](ENVS.md#my-account) and [Blockchain interaction](ENVS.md#blockchain-interaction-writing-to-contract-etc) features are also enabled.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
|
||||
+37
-27
@@ -3,7 +3,7 @@ import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useAccount, useSignMessage } from 'wagmi';
|
||||
import { useSignMessage } from 'wagmi';
|
||||
|
||||
import type {
|
||||
RewardsUserBalancesResponse, RewardsUserDailyCheckResponse,
|
||||
@@ -17,19 +17,25 @@ import config from 'configs/app';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import useApiFetch from 'lib/api/useApiFetch';
|
||||
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
|
||||
import { YEAR } from 'lib/consts';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import decodeJWT from 'lib/decodeJWT';
|
||||
import getErrorMessage from 'lib/errors/getErrorMessage';
|
||||
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
|
||||
import useToast from 'lib/hooks/useToast';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
import removeQueryParam from 'lib/router/removeQueryParam';
|
||||
import useAccount from 'lib/web3/useAccount';
|
||||
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
|
||||
|
||||
type ContextQueryResult<Response> = Pick<UseQueryResult<Response, ResourceError<unknown>>, 'data' | 'isLoading' | 'refetch' | 'isPending' | 'isFetching'>;
|
||||
|
||||
type TRewardsContext = {
|
||||
balancesQuery: UseQueryResult<RewardsUserBalancesResponse, ResourceError<unknown>>;
|
||||
dailyRewardQuery: UseQueryResult<RewardsUserDailyCheckResponse, ResourceError<unknown>>;
|
||||
referralsQuery: UseQueryResult<RewardsUserReferralsResponse, ResourceError<unknown>>;
|
||||
rewardsConfigQuery: UseQueryResult<RewardsConfigResponse, ResourceError<unknown>>;
|
||||
checkUserQuery: UseQueryResult<RewardsCheckUserResponse, ResourceError<unknown>>;
|
||||
balancesQuery: ContextQueryResult<RewardsUserBalancesResponse>;
|
||||
dailyRewardQuery: ContextQueryResult<RewardsUserDailyCheckResponse>;
|
||||
referralsQuery: ContextQueryResult<RewardsUserReferralsResponse>;
|
||||
rewardsConfigQuery: ContextQueryResult<RewardsConfigResponse>;
|
||||
checkUserQuery: ContextQueryResult<RewardsCheckUserResponse>;
|
||||
apiToken: string | undefined;
|
||||
isInitialized: boolean;
|
||||
isLoginModalOpen: boolean;
|
||||
@@ -39,15 +45,20 @@ type TRewardsContext = {
|
||||
claim: () => Promise<void>;
|
||||
}
|
||||
|
||||
const createDefaultQueryResult = <TData, TError>() =>
|
||||
({ data: undefined, isLoading: false, refetch: () => {} } as UseQueryResult<TData, TError>);
|
||||
const defaultQueryResult = {
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
isFetching: false,
|
||||
refetch: () => Promise.resolve({} as never),
|
||||
};
|
||||
|
||||
const RewardsContext = createContext<TRewardsContext>({
|
||||
balancesQuery: createDefaultQueryResult<RewardsUserBalancesResponse, ResourceError<unknown>>(),
|
||||
dailyRewardQuery: createDefaultQueryResult<RewardsUserDailyCheckResponse, ResourceError<unknown>>(),
|
||||
referralsQuery: createDefaultQueryResult<RewardsUserReferralsResponse, ResourceError<unknown>>(),
|
||||
rewardsConfigQuery: createDefaultQueryResult<RewardsConfigResponse, ResourceError<unknown>>(),
|
||||
checkUserQuery: createDefaultQueryResult<RewardsCheckUserResponse, ResourceError<unknown>>(),
|
||||
balancesQuery: defaultQueryResult,
|
||||
dailyRewardQuery: defaultQueryResult,
|
||||
referralsQuery: defaultQueryResult,
|
||||
rewardsConfigQuery: defaultQueryResult,
|
||||
checkUserQuery: defaultQueryResult,
|
||||
apiToken: undefined,
|
||||
isInitialized: false,
|
||||
isLoginModalOpen: false,
|
||||
@@ -74,7 +85,7 @@ function getMessageToSign(address: string, nonce: string, isLogin?: boolean, ref
|
||||
`Chain ID: ${ config.chain.id }`,
|
||||
`Nonce: ${ nonce }`,
|
||||
`Issued At: ${ new Date().toISOString() }`,
|
||||
`Expiration Time: ${ new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString() }`,
|
||||
`Expiration Time: ${ new Date(Date.now() + YEAR).toISOString() }`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -117,7 +128,11 @@ export function RewardsContextProvider({ children }: Props) {
|
||||
|
||||
// Save the API token to cookies and state
|
||||
const saveApiToken = useCallback((token: string | undefined) => {
|
||||
cookies.set(cookies.NAMES.REWARDS_API_TOKEN, token || '');
|
||||
if (token) {
|
||||
cookies.set(cookies.NAMES.REWARDS_API_TOKEN, token);
|
||||
} else {
|
||||
cookies.remove(cookies.NAMES.REWARDS_API_TOKEN);
|
||||
}
|
||||
setApiToken(token);
|
||||
}, []);
|
||||
|
||||
@@ -168,11 +183,12 @@ export function RewardsContextProvider({ children }: Props) {
|
||||
}
|
||||
}, [ router, apiToken, isInitialized, setIsLoginModalOpen ]);
|
||||
|
||||
const errorToast = useCallback((error: ResourceError<{ message: string }>) => {
|
||||
const errorToast = useCallback((error: unknown) => {
|
||||
const apiError = getErrorObjPayload<{ message: string }>(error);
|
||||
toast({
|
||||
position: 'top-right',
|
||||
title: 'Error',
|
||||
description: error?.payload?.message || 'Something went wrong. Try again later.',
|
||||
description: apiError?.message || getErrorMessage(error) || 'Something went wrong. Try again later.',
|
||||
status: 'error',
|
||||
variant: 'subtle',
|
||||
isClosable: true,
|
||||
@@ -182,15 +198,15 @@ export function RewardsContextProvider({ children }: Props) {
|
||||
// Login to the rewards program
|
||||
const login = useCallback(async(refCode: string) => {
|
||||
try {
|
||||
if (!address) {
|
||||
throw new Error();
|
||||
}
|
||||
const [ nonceResponse, checkCodeResponse ] = await Promise.all([
|
||||
apiFetch('rewards_nonce') as Promise<RewardsNonceResponse>,
|
||||
refCode ?
|
||||
apiFetch('rewards_check_ref_code', { pathParams: { code: refCode } }) as Promise<RewardsCheckRefCodeResponse> :
|
||||
Promise.resolve({ valid: true }),
|
||||
]);
|
||||
if (!address || !('nonce' in nonceResponse) || !('valid' in checkCodeResponse)) {
|
||||
throw new Error();
|
||||
}
|
||||
if (!checkCodeResponse.valid) {
|
||||
return { invalidRefCodeError: true };
|
||||
}
|
||||
@@ -206,9 +222,6 @@ export function RewardsContextProvider({ children }: Props) {
|
||||
},
|
||||
},
|
||||
}) as RewardsLoginResponse;
|
||||
if (!('created' in loginResponse)) {
|
||||
throw loginResponse;
|
||||
}
|
||||
saveApiToken(loginResponse.token);
|
||||
return { isNewUser: loginResponse.created };
|
||||
} catch (_error) {
|
||||
@@ -220,15 +233,12 @@ export function RewardsContextProvider({ children }: Props) {
|
||||
// Claim daily reward
|
||||
const claim = useCallback(async() => {
|
||||
try {
|
||||
const claimResponse = await apiFetch('rewards_user_daily_claim', {
|
||||
await apiFetch('rewards_user_daily_claim', {
|
||||
fetchParams: {
|
||||
method: 'POST',
|
||||
...fetchParams,
|
||||
},
|
||||
}) as RewardsUserDailyClaimResponse;
|
||||
if (!('daily_reward' in claimResponse)) {
|
||||
throw claimResponse;
|
||||
}
|
||||
} catch (_error) {
|
||||
errorToast(_error as ResourceError<{ message: string }>);
|
||||
throw _error;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Button, Flex, Skeleton, useBoolean, Image } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
|
||||
import { SECOND } from 'lib/consts';
|
||||
import { useRewardsContext } from 'lib/contexts/rewards';
|
||||
import { apos } from 'lib/html-entities';
|
||||
import splitSecondsInPeriods from 'ui/blockCountdown/splitSecondsInPeriods';
|
||||
@@ -10,6 +11,7 @@ import RewardsDashboardCard from 'ui/rewards/RewardsDashboardCard';
|
||||
import RewardsDashboardCardValue from 'ui/rewards/RewardsDashboardCardValue';
|
||||
import LinkExternal from 'ui/shared/links/LinkExternal';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
|
||||
|
||||
const RewardsDashboard = () => {
|
||||
const router = useRouter();
|
||||
@@ -20,9 +22,13 @@ const RewardsDashboard = () => {
|
||||
const [ isClaiming, setIsClaiming ] = useBoolean(false);
|
||||
const [ timeLeft, setTimeLeft ] = React.useState<string>('');
|
||||
|
||||
if (isInitialized && !apiToken) {
|
||||
router.replace({ pathname: '/' }, undefined, { shallow: true });
|
||||
}
|
||||
useRedirectForInvalidAuthToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized && !apiToken) {
|
||||
router.replace({ pathname: '/' }, undefined, { shallow: true });
|
||||
}
|
||||
}, [ isInitialized, apiToken, router ]);
|
||||
|
||||
const dailyRewardValue = Number(dailyRewardQuery.data?.daily_reward || 0) + Number(dailyRewardQuery.data?.pending_referral_rewards || 0);
|
||||
|
||||
@@ -30,8 +36,10 @@ const RewardsDashboard = () => {
|
||||
setIsClaiming.on();
|
||||
try {
|
||||
await claim();
|
||||
balancesQuery.refetch();
|
||||
dailyRewardQuery.refetch();
|
||||
await Promise.all([
|
||||
balancesQuery.refetch(),
|
||||
dailyRewardQuery.refetch(),
|
||||
]);
|
||||
} catch (error) {}
|
||||
setIsClaiming.off();
|
||||
}, [ claim, setIsClaiming, balancesQuery, dailyRewardQuery ]);
|
||||
@@ -41,17 +49,18 @@ const RewardsDashboard = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
let interval: NodeJS.Timeout; // eslint-disable-line
|
||||
// format the date to be compatible with the Date constructor
|
||||
const formattedDate = dailyRewardQuery.data.reset_at.replace(' ', 'T').replace(' UTC', 'Z');
|
||||
const target = new Date(formattedDate).getTime();
|
||||
|
||||
const updateCountdown = () => {
|
||||
let interval: ReturnType<typeof setTimeout>; // eslint-disable-line prefer-const
|
||||
|
||||
const updateCountdown = (target: number) => {
|
||||
const now = new Date().getTime();
|
||||
// format the date to be compatible with the Date constructor
|
||||
const formattedDate = dailyRewardQuery.data.reset_at.replace(' ', 'T').replace(' UTC', 'Z');
|
||||
const target = new Date(formattedDate).getTime();
|
||||
const difference = target - now;
|
||||
|
||||
if (difference > 0) {
|
||||
const { hours, minutes, seconds } = splitSecondsInPeriods(Math.floor(difference / 1000));
|
||||
const { hours, minutes, seconds } = splitSecondsInPeriods(Math.floor(difference / SECOND));
|
||||
setTimeLeft(`${ hours }:${ minutes }:${ seconds }`);
|
||||
} else {
|
||||
setTimeLeft('00:00:00');
|
||||
@@ -60,11 +69,11 @@ const RewardsDashboard = () => {
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
updateCountdown(target);
|
||||
|
||||
interval = setInterval(() => {
|
||||
updateCountdown();
|
||||
}, 1000);
|
||||
updateCountdown(target);
|
||||
}, SECOND);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [ dailyRewardQuery ]);
|
||||
@@ -78,7 +87,10 @@ const RewardsDashboard = () => {
|
||||
secondRow={ (
|
||||
<span>
|
||||
The Blockscout Merits Program is just getting started! Learn more about the details,
|
||||
features, and future plans in our <LinkExternal href="">blog post</LinkExternal>.
|
||||
features, and future plans in our{ ' ' }
|
||||
<LinkExternal href="https://www.blog.blockscout.com/blockscout-merits-rewarding-block-explorer-skills">
|
||||
blog post
|
||||
</LinkExternal>.
|
||||
</span>
|
||||
) }
|
||||
/>
|
||||
@@ -108,7 +120,7 @@ const RewardsDashboard = () => {
|
||||
hint={ (
|
||||
<>
|
||||
Total number of merits earned from all activities.{ ' ' }
|
||||
<LinkExternal href="https://docs.blockscout.com/using-blockscout/my-account/merits">
|
||||
<LinkExternal href="https://docs.blockscout.com/using-blockscout/merits">
|
||||
More info on merits
|
||||
</LinkExternal>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user