Commit c290cbc6 authored by Max Alekseenko's avatar Max Alekseenko

move login to context

parent 2fde0bba
import { useBoolean } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React, { createContext, useContext, useEffect, useMemo, useCallback } from 'react';
import { useAccount, useSignMessage } from 'wagmi';
import type { RewardsUserBalancesResponse, RewardsUserDailyCheckResponse } from 'types/api/rewards';
import type {
RewardsUserBalancesResponse, RewardsUserDailyCheckResponse,
RewardsNonceResponse, RewardsCheckUserResponse,
RewardsLoginResponse, RewardsCheckRefCodeResponse,
} from 'types/api/rewards';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
import useToast from 'lib/hooks/useToast';
import getQueryParamString from 'lib/router/getQueryParamString';
import removeQueryParam from 'lib/router/removeQueryParam';
......@@ -25,7 +33,7 @@ type TRewardsContext = {
isDailyRewardLoading: boolean;
refetchDailyReward: () => void;
apiToken: string | undefined;
saveApiToken: (token: string) => void;
login: (refCode: string) => Promise<{ isNewUser?: boolean; invalidRefCodeError?: boolean }>;
}
const RewardsContext = createContext<TRewardsContext>({
......@@ -39,11 +47,35 @@ const RewardsContext = createContext<TRewardsContext>({
isDailyRewardLoading: false,
refetchDailyReward: () => {},
apiToken: undefined,
saveApiToken: () => {},
login: async() => ({}),
});
function getMessageToSign(address: string, nonce: string, isLogin?: boolean, refCode?: string) {
const signInText = 'Sign-In for the Blockscout points program.';
const signUpText = 'Sign-Up for the Blockscout points program. I accept Terms of Service: https://points.blockscout.com/tos. I love capybaras.';
const referralText = refCode ? ` Referral code: ${ refCode }` : '';
const body = isLogin ? signInText : signUpText + referralText;
return [
`${ window.location.hostname } wants you to sign in with your Ethereum account:`,
address,
'',
body,
'',
`URI: ${ window.location.origin }`,
'Version: 1',
`Chain ID: ${ config.chain.id }`,
`Nonce: ${ nonce }`,
`Issued At: ${ new Date().toISOString() }`,
`Expiration Time: ${ new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString() }`,
].join('\n');
}
export function RewardsContextProvider({ children }: Props) {
const router = useRouter();
const apiFetch = useApiFetch();
const toast = useToast();
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
const [ isLoginModalOpen, setIsLoginModalOpen ] = useBoolean(false);
const [ isInitialized, setIsInitialized ] = useBoolean(false);
const [ apiToken, setApiToken ] = React.useState<string | undefined>();
......@@ -91,6 +123,51 @@ export function RewardsContextProvider({ children }: Props) {
}
}, [ router, apiToken, isInitialized, setIsLoginModalOpen ]);
const login = useCallback(async(refCode: string) => {
try {
const [ nonceResponse, userResponse, checkCodeResponse ] = await Promise.all([
apiFetch<'rewards_nonce', RewardsNonceResponse>('rewards_nonce'),
apiFetch<'rewards_check_user', RewardsCheckUserResponse>('rewards_check_user', { pathParams: { address } }),
refCode ?
apiFetch<'rewards_check_ref_code', RewardsCheckRefCodeResponse>('rewards_check_ref_code', { pathParams: { code: refCode } }) :
Promise.resolve({ valid: true }),
]);
if (!address || !('nonce' in nonceResponse) || !('exists' in userResponse) || !('valid' in checkCodeResponse)) {
throw new Error();
}
if (!checkCodeResponse.valid) {
return { invalidRefCodeError: true };
}
const message = getMessageToSign(address, nonceResponse.nonce, userResponse.exists, refCode);
const signature = await signMessageAsync({ message });
const loginResponse = await apiFetch<'rewards_login', RewardsLoginResponse>('rewards_login', {
fetchParams: {
method: 'POST',
body: {
nonce: nonceResponse.nonce,
message,
signature,
},
},
});
if (!('created' in loginResponse)) {
throw loginResponse;
}
saveApiToken(loginResponse.token);
return { isNewUser: loginResponse.created };
} catch (_error) {
toast({
position: 'top-right',
title: 'Error',
description: (_error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
throw _error;
}
}, [ apiFetch, address, signMessageAsync, toast, saveApiToken ]);
const value = useMemo(() => ({
isLoginModalOpen,
openLoginModal: setIsLoginModalOpen.on,
......@@ -102,8 +179,8 @@ export function RewardsContextProvider({ children }: Props) {
isDailyRewardLoading: dailyRewardQuery.isLoading,
refetchDailyReward: dailyRewardQuery.refetch,
apiToken,
saveApiToken,
}), [ isLoginModalOpen, setIsLoginModalOpen, balancesQuery, dailyRewardQuery, apiToken, saveApiToken ]);
login,
}), [ isLoginModalOpen, setIsLoginModalOpen, balancesQuery, dailyRewardQuery, apiToken, login ]);
return (
<RewardsContext.Provider value={ value }>
......
......@@ -3,13 +3,12 @@ import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState, useEffect } from 'react';
import { useRewardsContext } from 'lib/contexts/rewards';
import * as cookies from 'lib/cookies';
import useWallet from 'lib/web3/useWallet';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import LinkExternal from 'ui/shared/links/LinkExternal';
import useLogin from '../useLogin';
type Props = {
goNext: (isReferral: boolean) => void;
closeModal: () => void;
......@@ -24,7 +23,7 @@ const LoginStepContent = ({ goNext, closeModal }: Props) => {
const [ refCode, setRefCode ] = useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false);
const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const login = useLogin();
const { login } = useRewardsContext();
const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setRefCode(event.target.value);
......
import { useCallback } from 'react';
import { useAccount, useSignMessage } from 'wagmi';
import type {
RewardsNonceResponse, RewardsCheckUserResponse,
RewardsLoginResponse, RewardsCheckRefCodeResponse,
} from 'types/api/rewards';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import { useRewardsContext } from 'lib/contexts/rewards';
import useToast from 'lib/hooks/useToast';
function getMessageToSign(address: string, nonce: string, isLogin?: boolean, refCode?: string) {
const signInText = 'Sign-In for the Blockscout points program.';
const signUpText = 'Sign-Up for the Blockscout points program. I accept Terms of Service: https://points.blockscout.com/tos. I love capybaras.';
const referralText = refCode ? ` Referral code: ${ refCode }` : '';
const body = isLogin ? signInText : signUpText + referralText;
return [
`${ window.location.hostname } wants you to sign in with your Ethereum account:`,
address,
'',
body,
'',
`URI: ${ window.location.origin }`,
'Version: 1',
`Chain ID: ${ config.chain.id }`,
`Nonce: ${ nonce }`,
`Issued At: ${ new Date().toISOString() }`,
`Expiration Time: ${ new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString() }`,
].join('\n');
}
export default function useLogin() {
const apiFetch = useApiFetch();
const toast = useToast();
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
const { saveApiToken } = useRewardsContext();
return useCallback(async(refCode: string) => {
try {
const [ nonceResponse, userResponse, checkCodeResponse ] = await Promise.all([
apiFetch<'rewards_nonce', RewardsNonceResponse>('rewards_nonce'),
apiFetch<'rewards_check_user', RewardsCheckUserResponse>('rewards_check_user', { pathParams: { address } }),
refCode ?
apiFetch<'rewards_check_ref_code', RewardsCheckRefCodeResponse>('rewards_check_ref_code', { pathParams: { code: refCode } }) :
Promise.resolve({ valid: true }),
]);
if (!address || !('nonce' in nonceResponse) || !('exists' in userResponse) || !('valid' in checkCodeResponse)) {
throw new Error();
}
if (!checkCodeResponse.valid) {
return { invalidRefCodeError: true };
}
const message = getMessageToSign(address, nonceResponse.nonce, userResponse.exists, refCode);
const signature = await signMessageAsync({ message });
const loginResponse = await apiFetch<'rewards_login', RewardsLoginResponse>('rewards_login', {
fetchParams: {
method: 'POST',
body: {
nonce: nonceResponse.nonce,
message,
signature,
},
},
});
if (!('created' in loginResponse)) {
throw loginResponse;
}
saveApiToken(loginResponse.token);
return { isNewUser: loginResponse.created };
} catch (_error) {
toast({
position: 'top-right',
title: 'Error',
description: (_error as ResourceError<{ message: string }>)?.payload?.message || 'Something went wrong. Try again later.',
status: 'error',
variant: 'subtle',
isClosable: true,
});
throw _error;
}
}, [ apiFetch, address, signMessageAsync, toast, saveApiToken ]);
}
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