Commit 85ec04fd authored by Max Alekseenko's avatar Max Alekseenko

post-review changes

parent b472070b
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(),
......
......@@ -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 |
| --- | --- | --- | --- | --- | --- | --- |
......
......@@ -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>('');
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
const updateCountdown = () => {
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();
let interval: ReturnType<typeof setTimeout>; // eslint-disable-line prefer-const
const updateCountdown = (target: number) => {
const now = new Date().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>
</>
......
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