Commit 67acc9a4 authored by Max Alekseenko's avatar Max Alekseenko

implement login

parent fb5726b7
...@@ -90,6 +90,7 @@ import type { ...@@ -90,6 +90,7 @@ import type {
OptimismL2BatchBlocks, OptimismL2BatchBlocks,
} from 'types/api/optimisticL2'; } from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { RewardsNonceResponse, RewardsCheckUserResponse, RewardsLoginResponse } from 'types/api/rewards';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search'; import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium'; import type { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium';
import type { HomeStats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
...@@ -319,6 +320,24 @@ export const RESOURCES = { ...@@ -319,6 +320,24 @@ export const RESOURCES = {
basePath: marketplaceApi?.basePath, basePath: marketplaceApi?.basePath,
}, },
// REWARDS SERVICE
rewards_nonce: {
path: '/api/v1/auth/nonce',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_check_user: {
path: '/api/v1/auth/user/:address',
pathParams: [ 'address' as const ],
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
rewards_login: {
path: '/api/v1/auth/login',
endpoint: getFeaturePayload(config.features.rewards)?.api.endpoint,
basePath: getFeaturePayload(config.features.rewards)?.api.basePath,
},
// BLOCKS, TXS // BLOCKS, TXS
blocks: { blocks: {
path: '/api/v2/blocks', path: '/api/v2/blocks',
...@@ -1169,6 +1188,9 @@ Q extends 'address_mud_records' ? AddressMudRecords : ...@@ -1169,6 +1188,9 @@ Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord : Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_nonce' ? RewardsNonceResponse :
Q extends 'rewards_check_user' ? RewardsCheckUserResponse :
Q extends 'rewards_login' ? RewardsLoginResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
...@@ -5,6 +5,7 @@ import isBrowser from './isBrowser'; ...@@ -5,6 +5,7 @@ import isBrowser from './isBrowser';
export enum NAMES { export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed', NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key', API_TOKEN='_explorer_key',
REWARDS_API_TOKEN='rewards_api_token',
INVALID_SESSION='invalid_session', INVALID_SESSION='invalid_session',
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed', CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
......
export type RewardsNonceResponse = {
nonce: string;
};
export type RewardsCheckUserResponse = {
exists: boolean;
};
export type RewardsLoginResponse = {
created: boolean;
token: string;
};
...@@ -36,7 +36,7 @@ const RewardsLoginModal = () => { ...@@ -36,7 +36,7 @@ const RewardsLoginModal = () => {
<ModalCloseButton top={ 6 } right={ 6 }/> <ModalCloseButton top={ 6 } right={ 6 }/>
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
{ isLoginStep ? { isLoginStep ?
<LoginStepContent goNext={ setIsLoginStep.off }/> : <LoginStepContent goNext={ setIsLoginStep.off } closeModal={ closeLoginModal }/> :
<CongratsStepContent/> <CongratsStepContent/>
} }
</ModalBody> </ModalBody>
......
import { Text, Box, Flex, useColorModeValue, Button } from '@chakra-ui/react'; import { Text, Box, Flex, useColorModeValue, Button } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { route } from 'nextjs-routes';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import AvailableSoonLabel from '../AvailableSoonLabel'; import AvailableSoonLabel from '../AvailableSoonLabel';
...@@ -69,7 +71,7 @@ const CongratsStepContent = () => { ...@@ -69,7 +71,7 @@ const CongratsStepContent = () => {
Explore your current merits balance, find activities to boost your merits, Explore your current merits balance, find activities to boost your merits,
and view your capybara NFT badge collection on the dashboard and view your capybara NFT badge collection on the dashboard
</Text> </Text>
<Button mt={ 3 }> <Button mt={ 3 } as="a" href={ route({ pathname: '/account/rewards' }) }>
Open Open
</Button> </Button>
</Flex> </Flex>
......
import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl } from '@chakra-ui/react'; import { Text, Button, useColorModeValue, Image, Box, Flex, Switch, useBoolean, Input, FormControl } from '@chakra-ui/react';
import React from 'react'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react';
import InputPlaceholder from 'ui/shared/InputPlaceholder'; import InputPlaceholder from 'ui/shared/InputPlaceholder';
import LinkExternal from 'ui/shared/links/LinkExternal'; import LinkExternal from 'ui/shared/links/LinkExternal';
import useWallet from 'ui/snippets/walletMenu/useWallet'; import useWallet from 'ui/snippets/walletMenu/useWallet';
const LoginStepContent = ({ goNext }: { goNext: () => void }) => { import useLogin from '../useLogin';
type Props = {
goNext: () => void;
closeModal: () => void;
};
const LoginStepContent = ({ goNext, closeModal }: Props) => {
const router = useRouter();
const { connect, isWalletConnected } = useWallet({ source: 'Merits' }); const { connect, isWalletConnected } = useWallet({ source: 'Merits' });
const [ isSwitchChecked, setIsSwitchChecked ] = useBoolean(false); const [ isSwitchChecked, setIsSwitchChecked ] = useBoolean(false);
const [ isLoading, setIsLoading ] = useBoolean(false);
const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const dividerColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const login = useLogin();
const handleLogin = useCallback(async() => {
try {
setIsLoading.on();
const { isNewUser } = await login();
if (isNewUser) {
goNext();
} else {
closeModal();
router.push({ pathname: '/account/rewards' }, undefined, { shallow: true });
}
} catch (error) {}
setIsLoading.off();
}, [ login, goNext, setIsLoading, router, closeModal ]);
return ( return (
<> <>
...@@ -46,7 +71,8 @@ const LoginStepContent = ({ goNext }: { goNext: () => void }) => { ...@@ -46,7 +71,8 @@ const LoginStepContent = ({ goNext }: { goNext: () => void }) => {
w="full" w="full"
mt={ isWalletConnected ? 6 : 0 } mt={ isWalletConnected ? 6 : 0 }
mb={ 4 } mb={ 4 }
onClick={ isWalletConnected ? goNext : connect } onClick={ isWalletConnected ? handleLogin : connect }
isLoading={ isLoading }
> >
{ isWalletConnected ? 'Get started' : 'Connect wallet' } { isWalletConnected ? 'Get started' : 'Connect wallet' }
</Button> </Button>
......
import { useCallback } from 'react';
import { useAccount, useSignMessage } from 'wagmi';
import type { RewardsNonceResponse, RewardsCheckUserResponse, RewardsLoginResponse } from 'types/api/rewards';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch';
import * as cookies from 'lib/cookies';
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*/ 'blockscout.com' } wants you to sign in with your Ethereum account:`,
address,
'',
body,
'',
`URI: ${ /*window.location.origin*/ 'https://blockscout.com' }`,
'Version: 1',
`Chain ID: ${ config.chain.id }`,
`Nonce: ${ nonce }`,
`Issued At: ${ new Date().toISOString() }`,
].join('\n');
}
export default function useLogin() {
const apiFetch = useApiFetch();
const toast = useToast();
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
return useCallback(async() => {
try {
const [ nonceResponse, userResponse ] = await Promise.all([
apiFetch<'rewards_nonce', RewardsNonceResponse>('rewards_nonce'),
apiFetch<'rewards_check_user', RewardsCheckUserResponse>('rewards_check_user', { pathParams: { address } }),
]);
if (!address || !('nonce' in nonceResponse) || !('exists' in userResponse)) {
throw new Error();
}
const message = getMessageToSign(address, nonceResponse.nonce, userResponse.exists);
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;
}
cookies.set(cookies.NAMES.REWARDS_API_TOKEN, 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 ]);
}
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