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

implement login

parent fb5726b7
......@@ -90,6 +90,7 @@ import type {
OptimismL2BatchBlocks,
} from 'types/api/optimisticL2';
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 { ShibariumWithdrawalsResponse, ShibariumDepositsResponse } from 'types/api/shibarium';
import type { HomeStats } from 'types/api/stats';
......@@ -319,6 +320,24 @@ export const RESOURCES = {
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: {
path: '/api/v2/blocks',
......@@ -1169,6 +1188,9 @@ Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_nonce' ? RewardsNonceResponse :
Q extends 'rewards_check_user' ? RewardsCheckUserResponse :
Q extends 'rewards_login' ? RewardsLoginResponse :
never;
/* eslint-enable @typescript-eslint/indent */
......
......@@ -5,6 +5,7 @@ import isBrowser from './isBrowser';
export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed',
API_TOKEN='_explorer_key',
REWARDS_API_TOKEN='rewards_api_token',
INVALID_SESSION='invalid_session',
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
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 = () => {
<ModalCloseButton top={ 6 } right={ 6 }/>
<ModalBody mb={ 0 }>
{ isLoginStep ?
<LoginStepContent goNext={ setIsLoginStep.off }/> :
<LoginStepContent goNext={ setIsLoginStep.off } closeModal={ closeLoginModal }/> :
<CongratsStepContent/>
}
</ModalBody>
......
import { Text, Box, Flex, useColorModeValue, Button } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import IconSvg from 'ui/shared/IconSvg';
import AvailableSoonLabel from '../AvailableSoonLabel';
......@@ -69,7 +71,7 @@ const CongratsStepContent = () => {
Explore your current merits balance, find activities to boost your merits,
and view your capybara NFT badge collection on the dashboard
</Text>
<Button mt={ 3 }>
<Button mt={ 3 } as="a" href={ route({ pathname: '/account/rewards' }) }>
Open
</Button>
</Flex>
......
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 LinkExternal from 'ui/shared/links/LinkExternal';
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 [ isSwitchChecked, setIsSwitchChecked ] = useBoolean(false);
const [ isLoading, setIsLoading ] = useBoolean(false);
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 (
<>
......@@ -46,7 +71,8 @@ const LoginStepContent = ({ goNext }: { goNext: () => void }) => {
w="full"
mt={ isWalletConnected ? 6 : 0 }
mb={ 4 }
onClick={ isWalletConnected ? goNext : connect }
onClick={ isWalletConnected ? handleLogin : connect }
isLoading={ isLoading }
>
{ isWalletConnected ? 'Get started' : 'Connect wallet' }
</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