Commit adc32416 authored by tom goriunov's avatar tom goriunov Committed by GitHub

UX changes for BS / Merits login (#2541)

* UX changes for BS / Merits login

Fixes #2535

* fix ts

* refetch user balance after unified login

* fixes
parent f9804e57
...@@ -44,6 +44,7 @@ type TRewardsContext = { ...@@ -44,6 +44,7 @@ type TRewardsContext = {
isLoginModalOpen: boolean; isLoginModalOpen: boolean;
openLoginModal: () => void; openLoginModal: () => void;
closeLoginModal: () => void; closeLoginModal: () => void;
saveApiToken: (token: string | undefined) => void;
login: (refCode: string) => Promise<{ isNewUser?: boolean; invalidRefCodeError?: boolean }>; login: (refCode: string) => Promise<{ isNewUser?: boolean; invalidRefCodeError?: boolean }>;
claim: () => Promise<void>; claim: () => Promise<void>;
}; };
...@@ -68,6 +69,7 @@ const initialState = { ...@@ -68,6 +69,7 @@ const initialState = {
isLoginModalOpen: false, isLoginModalOpen: false,
openLoginModal: () => {}, openLoginModal: () => {},
closeLoginModal: () => {}, closeLoginModal: () => {},
saveApiToken: () => {},
login: async() => ({}), login: async() => ({}),
claim: async() => {}, claim: async() => {},
}; };
...@@ -265,6 +267,7 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -265,6 +267,7 @@ export function RewardsContextProvider({ children }: Props) {
rewardsConfigQuery, rewardsConfigQuery,
checkUserQuery, checkUserQuery,
apiToken, apiToken,
saveApiToken,
isInitialized, isInitialized,
isLoginModalOpen, isLoginModalOpen,
openLoginModal: setIsLoginModalOpen.on, openLoginModal: setIsLoginModalOpen.on,
...@@ -274,7 +277,7 @@ export function RewardsContextProvider({ children }: Props) { ...@@ -274,7 +277,7 @@ export function RewardsContextProvider({ children }: Props) {
}; };
}, [ }, [
isLoginModalOpen, setIsLoginModalOpen, balancesQuery, dailyRewardQuery, checkUserQuery, isLoginModalOpen, setIsLoginModalOpen, balancesQuery, dailyRewardQuery, checkUserQuery,
apiToken, login, claim, referralsQuery, rewardsConfigQuery, isInitialized, apiToken, login, claim, referralsQuery, rewardsConfigQuery, isInitialized, saveApiToken,
]); ]);
return ( return (
......
...@@ -7,4 +7,7 @@ export const base: RewardsConfigResponse = { ...@@ -7,4 +7,7 @@ export const base: RewardsConfigResponse = {
daily_claim: '10', daily_claim: '10',
referral_share: '0.1', referral_share: '0.1',
}, },
auth: {
shared_siwe_login: true,
},
}; };
...@@ -5,6 +5,9 @@ export type RewardsConfigResponse = { ...@@ -5,6 +5,9 @@ export type RewardsConfigResponse = {
daily_claim: string; daily_claim: string;
referral_share: string; referral_share: string;
}; };
auth: {
shared_siwe_login: boolean;
};
}; };
export type RewardsCheckRefCodeResponse = { export type RewardsCheckRefCodeResponse = {
...@@ -13,6 +16,7 @@ export type RewardsCheckRefCodeResponse = { ...@@ -13,6 +16,7 @@ export type RewardsCheckRefCodeResponse = {
export type RewardsNonceResponse = { export type RewardsNonceResponse = {
nonce: string; nonce: string;
merits_login_nonce?: string;
}; };
export type RewardsCheckUserResponse = { export type RewardsCheckUserResponse = {
......
...@@ -31,7 +31,7 @@ const MyProfile = () => { ...@@ -31,7 +31,7 @@ const MyProfile = () => {
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const handleAddWalletClick = React.useCallback(() => { const handleAddWalletClick = React.useCallback(() => {
setAuthInitialScreen({ type: 'connect_wallet', isAuth: true }); setAuthInitialScreen({ type: 'connect_wallet', isAuth: true, loginToRewards: true });
authModal.onOpen(); authModal.onOpen();
}, [ authModal ]); }, [ authModal ]);
......
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean, useDisclosure } from '@chakra-ui/react'; import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalCloseButton, ModalBody, useBoolean, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import type { Screen } from 'ui/snippets/auth/types';
import { useRewardsContext } from 'lib/contexts/rewards'; import { useRewardsContext } from 'lib/contexts/rewards';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useWallet from 'lib/web3/useWallet'; import useWallet from 'lib/web3/useWallet';
...@@ -25,7 +27,7 @@ const RewardsLoginModal = () => { ...@@ -25,7 +27,7 @@ const RewardsLoginModal = () => {
const [ isLoginStep, setIsLoginStep ] = useBoolean(true); const [ isLoginStep, setIsLoginStep ] = useBoolean(true);
const [ isReferral, setIsReferral ] = useBoolean(false); const [ isReferral, setIsReferral ] = useBoolean(false);
const [ isAuth, setIsAuth ] = useBoolean(false); const [ authModalInitialScreen, setAuthModalInitialScreen ] = React.useState<Screen>();
const authModal = useDisclosure(); const authModal = useDisclosure();
useEffect(() => { useEffect(() => {
...@@ -42,15 +44,18 @@ const RewardsLoginModal = () => { ...@@ -42,15 +44,18 @@ const RewardsLoginModal = () => {
setIsLoginStep.off(); setIsLoginStep.off();
}, [ setIsLoginStep, setIsReferral ]); }, [ setIsLoginStep, setIsReferral ]);
const handleAuthModalOpen = useCallback((isAuth: boolean) => { const handleAuthModalOpen = useCallback((isAuth: boolean, trySharedLogin?: boolean) => {
setIsAuth[isAuth ? 'on' : 'off'](); setAuthModalInitialScreen({ type: 'connect_wallet', isAuth, loginToRewards: trySharedLogin });
authModal.onOpen(); authModal.onOpen();
}, [ authModal, setIsAuth ]); }, [ authModal, setAuthModalInitialScreen ]);
const handleAuthModalClose = useCallback(() => { const handleAuthModalClose = useCallback((isSuccess?: boolean, rewardsApiToken?: string) => {
setIsAuth.off(); if (isSuccess && rewardsApiToken) {
closeLoginModal();
}
setAuthModalInitialScreen(undefined);
authModal.onClose(); authModal.onClose();
}, [ authModal, setIsAuth ]); }, [ authModal, setAuthModalInitialScreen, closeLoginModal ]);
return ( return (
<> <>
...@@ -74,10 +79,10 @@ const RewardsLoginModal = () => { ...@@ -74,10 +79,10 @@ const RewardsLoginModal = () => {
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
{ authModal.isOpen && ( { authModal.isOpen && authModalInitialScreen && (
<AuthModal <AuthModal
onClose={ handleAuthModalClose } onClose={ handleAuthModalClose }
initialScreen={{ type: 'connect_wallet', isAuth }} initialScreen={ authModalInitialScreen }
mixpanelConfig={ MIXPANEL_CONFIG } mixpanelConfig={ MIXPANEL_CONFIG }
closeOnError closeOnError
/> />
......
...@@ -15,7 +15,7 @@ import useProfileQuery from 'ui/snippets/auth/useProfileQuery'; ...@@ -15,7 +15,7 @@ import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
type Props = { type Props = {
goNext: (isReferral: boolean) => void; goNext: (isReferral: boolean) => void;
closeModal: () => void; closeModal: () => void;
openAuthModal: (isAuth: boolean) => void; openAuthModal: (isAuth: boolean, trySharedLogin?: boolean) => void;
}; };
const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
...@@ -26,7 +26,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -26,7 +26,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
const [ isLoading, setIsLoading ] = useBoolean(false); const [ isLoading, setIsLoading ] = useBoolean(false);
const [ refCode, setRefCode ] = useState(savedRefCode || ''); const [ refCode, setRefCode ] = useState(savedRefCode || '');
const [ refCodeError, setRefCodeError ] = useBoolean(false); const [ refCodeError, setRefCodeError ] = useBoolean(false);
const { login, checkUserQuery } = useRewardsContext(); const { login, checkUserQuery, rewardsConfigQuery } = useRewardsContext();
const profileQuery = useProfileQuery(); const profileQuery = useProfileQuery();
const isAddressMismatch = useMemo(() => const isAddressMismatch = useMemo(() =>
...@@ -43,6 +43,8 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -43,6 +43,8 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists, isConnected && !isAddressMismatch && !checkUserQuery.isFetching && !checkUserQuery.data?.exists,
[ isConnected, isAddressMismatch, checkUserQuery ]); [ isConnected, isAddressMismatch, checkUserQuery ]);
const canTrySharedLogin = rewardsConfigQuery.data?.auth.shared_siwe_login && checkUserQuery.data?.exists !== false && !isLoggedIntoAccountWithWallet;
const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleRefCodeChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setRefCode(event.target.value); setRefCode(event.target.value);
}, []); }, []);
...@@ -56,7 +58,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -56,7 +58,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
setRefCodeError.on(); setRefCodeError.on();
} else { } else {
if (isNewUser) { if (isNewUser) {
goNext(Boolean(refCode)); goNext(isRefCodeUsed);
} else { } else {
closeModal(); closeModal();
router.push({ pathname: '/account/rewards' }, undefined, { shallow: true }); router.push({ pathname: '/account/rewards' }, undefined, { shallow: true });
...@@ -74,15 +76,27 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -74,15 +76,27 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
} }
}, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps }, [ refCode, isRefCodeUsed, isSignUp ]); // eslint-disable-line react-hooks/exhaustive-deps
const handleLogin = useCallback(async() => { const handleButtonClick = React.useCallback(() => {
if (canTrySharedLogin) {
return openAuthModal(Boolean(profileQuery.data?.email), true);
}
if (!isConnected) {
return connect();
}
if (isLoggedIntoAccountWithWallet) { if (isLoggedIntoAccountWithWallet) {
loginToRewardsProgram(); return loginToRewardsProgram();
} else {
openAuthModal(Boolean(profileQuery.data?.email));
} }
}, [ loginToRewardsProgram, openAuthModal, isLoggedIntoAccountWithWallet, profileQuery ]);
return openAuthModal(Boolean(profileQuery.data?.email));
}, [ loginToRewardsProgram, openAuthModal, profileQuery, connect, isConnected, isLoggedIntoAccountWithWallet, canTrySharedLogin ]);
const buttonText = useMemo(() => { const buttonText = useMemo(() => {
if (canTrySharedLogin) {
return 'Continue with wallet';
}
if (!isConnected) { if (!isConnected) {
return 'Connect wallet'; return 'Connect wallet';
} }
...@@ -90,7 +104,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -90,7 +104,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
return isSignUp ? 'Get started' : 'Continue'; return isSignUp ? 'Get started' : 'Continue';
} }
return profileQuery.data?.email ? 'Add wallet to account' : 'Log in to account'; return profileQuery.data?.email ? 'Add wallet to account' : 'Log in to account';
}, [ isConnected, isLoggedIntoAccountWithWallet, profileQuery.data, isSignUp ]); }, [ canTrySharedLogin, isConnected, isLoggedIntoAccountWithWallet, profileQuery.data?.email, isSignUp ]);
return ( return (
<> <>
...@@ -148,7 +162,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -148,7 +162,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
w="full" w="full"
whiteSpace="normal" whiteSpace="normal"
mb={ 4 } mb={ 4 }
onClick={ isConnected ? handleLogin : connect } onClick={ handleButtonClick }
isLoading={ isLoading || profileQuery.isLoading || checkUserQuery.isFetching } isLoading={ isLoading || profileQuery.isLoading || checkUserQuery.isFetching }
loadingText={ isLoading ? 'Sign message in your wallet' : undefined } loadingText={ isLoading ? 'Sign message in your wallet' : undefined }
isDisabled={ isAddressMismatch || refCodeError } isDisabled={ isAddressMismatch || refCodeError }
......
...@@ -7,6 +7,7 @@ import type { Screen, ScreenSuccess } from './types'; ...@@ -7,6 +7,7 @@ import type { Screen, ScreenSuccess } from './types';
import config from 'configs/app'; import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import { useRewardsContext } from 'lib/contexts/rewards';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken'; import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import * as mixpanel from 'lib/mixpanel'; import * as mixpanel from 'lib/mixpanel';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
...@@ -22,7 +23,7 @@ const feature = config.features.account; ...@@ -22,7 +23,7 @@ const feature = config.features.account;
interface Props { interface Props {
initialScreen: Screen; initialScreen: Screen;
onClose: (isSuccess?: boolean) => void; onClose: (isSuccess?: boolean, rewardsApiToken?: string) => void;
mixpanelConfig?: { mixpanelConfig?: {
wallet_connect?: { wallet_connect?: {
source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source']; source: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
...@@ -37,6 +38,9 @@ interface Props { ...@@ -37,6 +38,9 @@ interface Props {
const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Props) => { const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Props) => {
const [ steps, setSteps ] = React.useState<Array<Screen>>([ initialScreen ]); const [ steps, setSteps ] = React.useState<Array<Screen>>([ initialScreen ]);
const [ isSuccess, setIsSuccess ] = React.useState(false); const [ isSuccess, setIsSuccess ] = React.useState(false);
const [ rewardsApiToken, setRewardsApiToken ] = React.useState<string | undefined>(undefined);
const { saveApiToken } = useRewardsContext();
const router = useRouter(); const router = useRouter();
const csrfQuery = useGetCsrfToken(); const csrfQuery = useGetCsrfToken();
...@@ -87,12 +91,18 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro ...@@ -87,12 +91,18 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
queryClient.setQueryData(getResourceKey('user_info'), () => screen.profile); queryClient.setQueryData(getResourceKey('user_info'), () => screen.profile);
await csrfQuery.refetch(); await csrfQuery.refetch();
if ('rewardsToken' in screen && screen.rewardsToken) {
setRewardsApiToken(screen.rewardsToken);
saveApiToken(screen.rewardsToken);
}
onNextStep(screen); onNextStep(screen);
}, [ initialScreen, mixpanelConfig?.account_link_info.source, onNextStep, csrfQuery, queryClient ]); }, [ initialScreen, mixpanelConfig?.account_link_info.source, onNextStep, csrfQuery, queryClient, saveApiToken ]);
const onModalClose = React.useCallback(() => { const onModalClose = React.useCallback(() => {
onClose(isSuccess); onClose(isSuccess, rewardsApiToken);
}, [ isSuccess, onClose ]); }, [ isSuccess, rewardsApiToken, onClose ]);
const header = (() => { const header = (() => {
const currentStep = steps[steps.length - 1]; const currentStep = steps[steps.length - 1];
...@@ -122,6 +132,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro ...@@ -122,6 +132,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
onSuccess={ onAuthSuccess } onSuccess={ onAuthSuccess }
onError={ onReset } onError={ onReset }
isAuth={ currentStep.isAuth } isAuth={ currentStep.isAuth }
loginToRewards={ currentStep.loginToRewards }
source={ mixpanelConfig?.wallet_connect?.source } source={ mixpanelConfig?.wallet_connect?.source }
/> />
); );
...@@ -153,6 +164,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro ...@@ -153,6 +164,7 @@ const AuthModal = ({ initialScreen, onClose, mixpanelConfig, closeOnError }: Pro
onClose={ onModalClose } onClose={ onModalClose }
isAuth={ currentStep.isAuth } isAuth={ currentStep.isAuth }
profile={ currentStep.profile } profile={ currentStep.profile }
rewardsToken={ currentStep.rewardsToken }
/> />
); );
} }
......
...@@ -15,14 +15,15 @@ interface Props { ...@@ -15,14 +15,15 @@ interface Props {
onError: (isAuth?: boolean) => void; onError: (isAuth?: boolean) => void;
isAuth?: boolean; isAuth?: boolean;
source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source']; source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
loginToRewards?: boolean;
} }
const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Props) => { const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source, loginToRewards }: Props) => {
const isStartedRef = React.useRef(false); const isStartedRef = React.useRef(false);
const recaptcha = useReCaptcha(); const recaptcha = useReCaptcha();
const handleSignInSuccess = React.useCallback(({ address, profile }: { address: string; profile: UserInfo }) => { const handleSignInSuccess = React.useCallback(({ address, profile, rewardsToken }: { address: string; profile: UserInfo; rewardsToken?: string }) => {
onSuccess({ type: 'success_wallet', address, isAuth, profile }); onSuccess({ type: 'success_wallet', address, isAuth, profile, rewardsToken });
}, [ onSuccess, isAuth ]); }, [ onSuccess, isAuth ]);
const handleSignInError = React.useCallback(() => { const handleSignInError = React.useCallback(() => {
...@@ -35,6 +36,7 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Pr ...@@ -35,6 +36,7 @@ const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth, source }: Pr
source, source,
isAuth, isAuth,
executeRecaptchaAsync: recaptcha.executeAsync, executeRecaptchaAsync: recaptcha.executeAsync,
loginToRewards,
}); });
React.useEffect(() => { React.useEffect(() => {
......
...@@ -24,7 +24,7 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => { ...@@ -24,7 +24,7 @@ const AuthModalScreenSelectMethod = ({ onSelectMethod }: Props) => {
Action: 'Wallet', Action: 'Wallet',
Source: 'Options selector', Source: 'Options selector',
}); });
onSelectMethod({ type: 'connect_wallet' }); onSelectMethod({ type: 'connect_wallet', loginToRewards: true });
}, [ onSelectMethod ]); }, [ onSelectMethod ]);
return ( return (
......
...@@ -16,7 +16,7 @@ interface Props { ...@@ -16,7 +16,7 @@ interface Props {
const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, onClose, isAuth, profile }: Props) => { const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, onClose, isAuth, profile }: Props) => {
const handleConnectWalletClick = React.useCallback(() => { const handleConnectWalletClick = React.useCallback(() => {
onConnectWallet({ type: 'connect_wallet', isAuth: true }); onConnectWallet({ type: 'connect_wallet', isAuth: true, loginToRewards: true });
}, [ onConnectWallet ]); }, [ onConnectWallet ]);
if (isAuth) { if (isAuth) {
......
...@@ -14,9 +14,10 @@ interface Props { ...@@ -14,9 +14,10 @@ interface Props {
onClose: () => void; onClose: () => void;
isAuth?: boolean; isAuth?: boolean;
profile: UserInfo | undefined; profile: UserInfo | undefined;
rewardsToken?: string;
} }
const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, profile }: Props) => { const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, profile, rewardsToken }: Props) => {
const handleAddEmailClick = React.useCallback(() => { const handleAddEmailClick = React.useCallback(() => {
onAddEmail({ type: 'email', isAuth: true }); onAddEmail({ type: 'email', isAuth: true });
}, [ onAddEmail ]); }, [ onAddEmail ]);
...@@ -45,7 +46,8 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, pr ...@@ -45,7 +46,8 @@ const AuthModalScreenSuccessWallet = ({ address, onAddEmail, onClose, isAuth, pr
<Text> <Text>
Wallet{ ' ' } Wallet{ ' ' }
<chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' } <chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' }
has been successfully used to log in to your Blockscout account. has been successfully used to log in to your Blockscout account
{ Boolean(rewardsToken) && ` and Merits Program` }.
</Text> </Text>
{ !profile?.email ? ( { !profile?.email ? (
<> <>
......
...@@ -10,12 +10,14 @@ export type ScreenSuccess = { ...@@ -10,12 +10,14 @@ export type ScreenSuccess = {
address: string; address: string;
profile: UserInfo; profile: UserInfo;
isAuth?: boolean; isAuth?: boolean;
rewardsToken?: string;
}; };
export type Screen = { export type Screen = {
type: 'select_method'; type: 'select_method';
} | { } | {
type: 'connect_wallet'; type: 'connect_wallet';
isAuth?: boolean; isAuth?: boolean;
loginToRewards?: boolean;
} | { } | {
type: 'email'; type: 'email';
isAuth?: boolean; isAuth?: boolean;
......
...@@ -2,9 +2,12 @@ import React from 'react'; ...@@ -2,9 +2,12 @@ import React from 'react';
import { useSignMessage } from 'wagmi'; import { useSignMessage } from 'wagmi';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import type { RewardsCheckUserResponse, RewardsConfigResponse, RewardsLoginResponse, RewardsNonceResponse } from 'types/api/rewards';
import config from 'configs/app'; import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import { YEAR } from 'lib/consts';
import * as cookies from 'lib/cookies';
import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObj from 'lib/errors/getErrorObj'; import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
...@@ -12,15 +15,38 @@ import useToast from 'lib/hooks/useToast'; ...@@ -12,15 +15,38 @@ import useToast from 'lib/hooks/useToast';
import type * as mixpanel from 'lib/mixpanel'; import type * as mixpanel from 'lib/mixpanel';
import useWeb3Wallet from 'lib/web3/useWallet'; import useWeb3Wallet from 'lib/web3/useWallet';
function composeMessage(address: string, nonceBlockscout: string, nonceRewards: string) {
const feature = config.features.rewards;
const urlObj = window.location.hostname === 'localhost' && feature.isEnabled ?
new URL(feature.api.endpoint) :
window.location;
return [
`${ urlObj.hostname } wants you to sign in with your Ethereum account:`,
address,
'',
`Sign in/up to Blockscout Account V2 & Blockscout Merits program. Merits nonce: ${ nonceRewards }.`,
'',
`URI: ${ urlObj.origin }`,
'Version: 1',
`Chain ID: ${ config.chain.id }`,
`Nonce: ${ nonceBlockscout }`,
`Issued At: ${ new Date().toISOString() }`,
`Expiration Time: ${ new Date(Date.now() + YEAR).toISOString() }`,
].join('\n');
}
interface Props { interface Props {
onSuccess?: ({ address, profile }: { address: string; profile: UserInfo }) => void; onSuccess?: ({ address, profile, rewardsToken }: { address: string; profile: UserInfo; rewardsToken?: string }) => void;
onError?: () => void; onError?: () => void;
source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source']; source?: mixpanel.EventPayload<mixpanel.EventTypes.WALLET_CONNECT>['Source'];
isAuth?: boolean; isAuth?: boolean;
loginToRewards?: boolean;
executeRecaptchaAsync: () => Promise<string | null>; executeRecaptchaAsync: () => Promise<string | null>;
} }
function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, executeRecaptchaAsync }: Props) { function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, loginToRewards, executeRecaptchaAsync }: Props) {
const [ isPending, setIsPending ] = React.useState(false); const [ isPending, setIsPending ] = React.useState(false);
const isConnectingWalletRef = React.useRef(false); const isConnectingWalletRef = React.useRef(false);
...@@ -29,27 +55,83 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, exe ...@@ -29,27 +55,83 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, exe
const web3Wallet = useWeb3Wallet({ source }); const web3Wallet = useWeb3Wallet({ source });
const { signMessageAsync } = useSignMessage(); const { signMessageAsync } = useSignMessage();
const getSiweMessage = React.useCallback(async(address: string) => {
try {
if (!loginToRewards) {
throw new Error('Login to rewards is not enabled');
}
if (cookies.get(cookies.NAMES.REWARDS_API_TOKEN)) {
throw new Error('User already has logged in to rewards');
}
const rewardsConfig = await apiFetch('rewards_config') as RewardsConfigResponse;
if (!rewardsConfig.auth.shared_siwe_login) {
throw new Error('Shared SIWE login is not enabled');
}
const rewardsCheckUser = await apiFetch('rewards_check_user', { pathParams: { address } }) as RewardsCheckUserResponse;
if (!rewardsCheckUser.exists) {
throw new Error('Rewards user does not exist');
}
const nonceConfig = await apiFetch(
'rewards_nonce',
{ queryParams: { blockscout_login_address: address, blockscout_login_chain_id: config.chain.id } },
) as RewardsNonceResponse;
if (!nonceConfig.merits_login_nonce || !nonceConfig.nonce) {
throw new Error('Cannot get merits login nonce');
}
return {
message: composeMessage(address, nonceConfig.nonce, nonceConfig.merits_login_nonce),
authNonce: nonceConfig.nonce,
rewardsNonce: nonceConfig.merits_login_nonce,
type: 'shared',
};
} catch (error) {
const response = await apiFetch('auth_siwe_message', { queryParams: { address } }) as { siwe_message: string };
return {
message: response.siwe_message,
type: 'single',
};
}
}, [ apiFetch, loginToRewards ]);
const proceedToAuth = React.useCallback(async(address: string) => { const proceedToAuth = React.useCallback(async(address: string) => {
try { try {
const siweMessage = await apiFetch('auth_siwe_message', { queryParams: { address } }) as { siwe_message: string }; const siweMessage = await getSiweMessage(address);
const signature = await signMessageAsync({ message: siweMessage.siwe_message }); const signature = await signMessageAsync({ message: siweMessage.message });
const recaptchaToken = await executeRecaptchaAsync(); const recaptchaToken = await executeRecaptchaAsync();
if (!recaptchaToken) { if (!recaptchaToken) {
throw new Error('ReCaptcha is not solved'); throw new Error('ReCaptcha is not solved');
} }
const resource = isAuth ? 'auth_link_address' : 'auth_siwe_verify'; const authResource = isAuth ? 'auth_link_address' : 'auth_siwe_verify';
const response = await apiFetch<typeof resource, UserInfo, unknown>(resource, { const authResponse = await apiFetch<typeof authResource, UserInfo, unknown>(authResource, {
fetchParams: { fetchParams: {
method: 'POST', method: 'POST',
body: { message: siweMessage.siwe_message, signature, recaptcha_response: recaptchaToken }, body: { message: siweMessage.message, signature, recaptcha_response: recaptchaToken },
}, },
}); });
if (!('name' in response)) {
const rewardsLoginResponse = siweMessage.type === 'shared' ?
await apiFetch('rewards_login', {
fetchParams: {
method: 'POST',
body: {
nonce: siweMessage.authNonce,
message: siweMessage.message,
signature,
},
},
}) as RewardsLoginResponse : undefined;
if (!('name' in authResponse)) {
throw Error('Something went wrong'); throw Error('Something went wrong');
} }
onSuccess?.({ address, profile: response }); onSuccess?.({ address, profile: authResponse, rewardsToken: rewardsLoginResponse?.token });
} catch (error) { } catch (error) {
const errorObj = getErrorObj(error); const errorObj = getErrorObj(error);
const apiErrorMessage = getErrorObjPayload<{ message: string }>(error)?.message; const apiErrorMessage = getErrorObjPayload<{ message: string }>(error)?.message;
...@@ -63,7 +145,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, exe ...@@ -63,7 +145,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth, exe
} finally { } finally {
setIsPending(false); setIsPending(false);
} }
}, [ apiFetch, isAuth, onError, onSuccess, signMessageAsync, toast, executeRecaptchaAsync ]); }, [ getSiweMessage, signMessageAsync, executeRecaptchaAsync, isAuth, apiFetch, onSuccess, onError, toast ]);
const start = React.useCallback(() => { const start = React.useCallback(() => {
setIsPending(true); setIsPending(true);
......
...@@ -41,7 +41,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) => ...@@ -41,7 +41,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
} }
if (router.pathname === '/apps/[id]' && config.features.blockchainInteraction.isEnabled) { if (router.pathname === '/apps/[id]' && config.features.blockchainInteraction.isEnabled) {
setAuthInitialScreen({ type: 'connect_wallet' }); setAuthInitialScreen({ type: 'connect_wallet', loginToRewards: true });
} }
authModal.onOpen(); authModal.onOpen();
...@@ -53,7 +53,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) => ...@@ -53,7 +53,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
}, [ authModal ]); }, [ authModal ]);
const handleAddAddressClick = React.useCallback(() => { const handleAddAddressClick = React.useCallback(() => {
setAuthInitialScreen({ type: 'connect_wallet', isAuth: true }); setAuthInitialScreen({ type: 'connect_wallet', isAuth: true, loginToRewards: true });
authModal.onOpen(); authModal.onOpen();
}, [ authModal ]); }, [ authModal ]);
......
...@@ -35,7 +35,7 @@ const UserProfileMobile = () => { ...@@ -35,7 +35,7 @@ const UserProfileMobile = () => {
} }
if (router.pathname === '/apps/[id]' && config.features.blockchainInteraction.isEnabled) { if (router.pathname === '/apps/[id]' && config.features.blockchainInteraction.isEnabled) {
setAuthInitialScreen({ type: 'connect_wallet' }); setAuthInitialScreen({ type: 'connect_wallet', loginToRewards: true });
} }
authModal.onOpen(); authModal.onOpen();
...@@ -47,7 +47,7 @@ const UserProfileMobile = () => { ...@@ -47,7 +47,7 @@ const UserProfileMobile = () => {
}, [ authModal ]); }, [ authModal ]);
const handleAddAddressClick = React.useCallback(() => { const handleAddAddressClick = React.useCallback(() => {
setAuthInitialScreen({ type: 'connect_wallet', isAuth: true }); setAuthInitialScreen({ type: 'connect_wallet', isAuth: true, loginToRewards: true });
authModal.onOpen(); authModal.onOpen();
}, [ authModal ]); }, [ authModal ]);
......
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