Commit 1970de13 authored by tom's avatar tom

workflow to link wallet or email to account

parent 12e07e0e
import useApiQuery from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
// TODO @tom2drum move to auth
export default function useFetchProfileInfo() {
return useApiQuery('user_info', {
queryOptions: {
......
......@@ -2,14 +2,18 @@ import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies';
import useFetchProfileInfo from './useFetchProfileInfo';
// TODO @tom2drum move to auth
export default function useHasAccount() {
const appProps = useAppContext();
const profileQuery = useFetchProfileInfo();
if (!config.features.account.isEnabled) {
return false;
}
const cookiesString = appProps.cookies;
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString) || profileQuery.data);
return hasAuth;
}
import { Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay } from '@chakra-ui/react';
import React from 'react';
import type { Screen } from './types';
import type { Screen, ScreenSuccess } from './types';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import IconSvg from 'ui/shared/IconSvg';
import AuthModalScreenConnectWallet from './screens/AuthModalScreenConnectWallet';
import AuthModalScreenEmail from './screens/AuthModalScreenEmail';
import AuthModalScreenOtpCode from './screens/AuthModalScreenOtpCode';
import AuthModalScreenSelectMethod from './screens/AuthModalScreenSelectMethod';
import AuthModalScreenSuccessCreatedEmail from './screens/AuthModalScreenSuccessCreatedEmail';
import AuthModalScreenSuccessCreatedWallet from './screens/AuthModalScreenSuccessCreatedWallet';
import AuthModalScreenSuccessEmail from './screens/AuthModalScreenSuccessEmail';
import AuthModalScreenSuccessWallet from './screens/AuthModalScreenSuccessWallet';
interface Props {
initialScreen: Screen;
......@@ -19,6 +20,7 @@ interface Props {
const AuthModal = ({ initialScreen, onClose }: Props) => {
const [ steps, setSteps ] = React.useState<Array<Screen>>([ initialScreen ]);
const profileQuery = useFetchProfileInfo();
const onNextStep = React.useCallback((screen: Screen) => {
setSteps((prev) => [ ...prev, screen ]);
......@@ -32,19 +34,27 @@ const AuthModal = ({ initialScreen, onClose }: Props) => {
setSteps([ initialScreen ]);
}, [ initialScreen ]);
const onAuthSuccess = React.useCallback(async(screen: ScreenSuccess) => {
const { data } = await profileQuery.refetch();
if (data) {
onNextStep({ ...screen, profile: data });
}
// TODO @tom2drum handle error case
}, [ onNextStep, profileQuery ]);
const header = (() => {
const currentStep = steps[steps.length - 1];
switch (currentStep.type) {
case 'select_method':
return 'Select a way to connect';
case 'connect_wallet':
return 'Continue with wallet';
return currentStep.isAuth ? 'Add wallet' : 'Continue with wallet';
case 'email':
return currentStep.isAccountExists ? 'Add email' : 'Continue with email';
return currentStep.isAuth ? 'Add email' : 'Continue with email';
case 'otp_code':
return 'Confirmation code';
case 'success_created_email':
case 'success_created_wallet':
case 'success_email':
case 'success_wallet':
return 'Congrats!';
}
})();
......@@ -55,15 +65,29 @@ const AuthModal = ({ initialScreen, onClose }: Props) => {
case 'select_method':
return <AuthModalScreenSelectMethod onSelectMethod={ onNextStep }/>;
case 'connect_wallet':
return <AuthModalScreenConnectWallet onSuccess={ onNextStep } onError={ onReset }/>;
return <AuthModalScreenConnectWallet onSuccess={ onAuthSuccess } onError={ onReset } isAuth={ currentStep.isAuth }/>;
case 'email':
return <AuthModalScreenEmail onSubmit={ onNextStep }/>;
return <AuthModalScreenEmail onSubmit={ onNextStep } isAuth={ currentStep.isAuth }/>;
case 'otp_code':
return <AuthModalScreenOtpCode email={ currentStep.email } onSubmit={ onNextStep }/>;
case 'success_created_email':
return <AuthModalScreenSuccessCreatedEmail/>;
case 'success_created_wallet':
return <AuthModalScreenSuccessCreatedWallet address={ currentStep.address } onAddEmail={ onNextStep }/>;
return <AuthModalScreenOtpCode email={ currentStep.email } onSuccess={ onAuthSuccess } isAuth={ currentStep.isAuth }/>;
case 'success_email':
return (
<AuthModalScreenSuccessEmail
email={ currentStep.email }
onConnectWallet={ onNextStep }
isAuth={ currentStep.isAuth }
profile={ currentStep.profile }
/>
);
case 'success_wallet':
return (
<AuthModalScreenSuccessWallet
address={ currentStep.address }
onAddEmail={ onNextStep }
isAuth={ currentStep.isAuth }
profile={ currentStep.profile }
/>
);
}
})();
......
import { Center, Spinner } from '@chakra-ui/react';
import React from 'react';
import type { Screen } from '../types';
import type { ScreenSuccess } from '../types';
import useSignInWithWallet from '../useSignInWithWallet';
interface Props {
onSuccess: (screen: Screen) => void;
onSuccess: (screen: ScreenSuccess) => void;
onError: () => void;
isAuth?: boolean;
}
const AuthModalScreenConnectWallet = ({ onSuccess, onError }: Props) => {
const AuthModalScreenConnectWallet = ({ onSuccess, onError, isAuth }: Props) => {
const isStartedRef = React.useRef(false);
const handleSignInSuccess = React.useCallback(({ address }: { address: string }) => {
onSuccess({ type: 'success_created_wallet', address });
}, [ onSuccess ]);
onSuccess({ type: 'success_wallet', address, isAuth });
}, [ onSuccess, isAuth ]);
const handleSignInError = React.useCallback(() => {
onError();
......
......@@ -14,9 +14,10 @@ import AuthModalFieldEmail from '../fields/AuthModalFieldEmail';
interface Props {
onSubmit: (screen: Screen) => void;
isAuth?: boolean;
}
const AuthModalScreenEmail = ({ onSubmit }: Props) => {
const AuthModalScreenEmail = ({ onSubmit, isAuth }: Props) => {
const apiFetch = useApiFetch();
const toast = useToast();
......@@ -39,7 +40,7 @@ const AuthModalScreenEmail = ({ onSubmit }: Props) => {
},
})
.then(() => {
onSubmit({ type: 'otp_code', email: formData.email });
onSubmit({ type: 'otp_code', email: formData.email, isAuth });
})
.catch((error) => {
toast({
......@@ -48,7 +49,7 @@ const AuthModalScreenEmail = ({ onSubmit }: Props) => {
description: getErrorMessage(error) || 'Something went wrong',
});
});
}, [ apiFetch, onSubmit, toast ]);
}, [ apiFetch, onSubmit, toast, isAuth ]);
return (
<FormProvider { ...formApi }>
......
......@@ -3,7 +3,7 @@ import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import type { OtpCodeFormFields, Screen } from '../types';
import type { OtpCodeFormFields, ScreenSuccess } from '../types';
import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage';
......@@ -14,10 +14,11 @@ import AuthModalFieldOtpCode from '../fields/AuthModalFieldOtpCode';
interface Props {
email: string;
onSubmit: (screen: Screen) => void;
onSuccess: (screen: ScreenSuccess) => void;
isAuth?: boolean;
}
const AuthModalScreenOtpCode = ({ email, onSubmit }: Props) => {
const AuthModalScreenOtpCode = ({ email, onSuccess, isAuth }: Props) => {
const apiFetch = useApiFetch();
const toast = useToast();
......@@ -40,7 +41,7 @@ const AuthModalScreenOtpCode = ({ email, onSubmit }: Props) => {
},
})
.then(() => {
onSubmit({ type: 'success_created_email' });
onSuccess({ type: 'success_email', email, isAuth });
})
.catch((error) => {
// TODO @tom2drum handle incorrect code error
......@@ -50,7 +51,7 @@ const AuthModalScreenOtpCode = ({ email, onSubmit }: Props) => {
description: getErrorMessage(error) || 'Something went wrong',
});
});
}, [ apiFetch, email, onSubmit, toast ]);
}, [ apiFetch, email, onSuccess, toast, isAuth ]);
const handleResendCodeClick = React.useCallback(() => {
return apiFetch('auth_send_otp', {
......
import { Box, Text, Button } from '@chakra-ui/react';
import React from 'react';
const AuthModalScreenSuccessCreatedEmail = () => {
return (
<Box>
<Text>Your account was successfully created!</Text>
<Text mt={ 6 }>Connect a web3 wallet to safely interact with smart contracts and dapps inside Blockscout.</Text>
<Button mt={ 6 }>Connect wallet</Button>
</Box>
);
};
export default React.memo(AuthModalScreenSuccessCreatedEmail);
import { chakra, Box, Text, Button } from '@chakra-ui/react';
import React from 'react';
import type { Screen } from '../types';
import type { UserInfo } from 'types/api/account';
interface Props {
email: string;
onConnectWallet: (screen: Screen) => void;
isAuth?: boolean;
profile: UserInfo | undefined;
}
const AuthModalScreenSuccessEmail = ({ email, onConnectWallet, isAuth, profile }: Props) => {
const handleConnectWalletClick = React.useCallback(() => {
onConnectWallet({ type: 'connect_wallet', isAuth: true });
}, [ onConnectWallet ]);
if (isAuth) {
return (
<Text>
Your account was linked to{ ' ' }
<chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' }
email. Use for the next login.
</Text>
);
}
return (
<Box>
<Text>
<chakra.span fontWeight="700">{ email }</chakra.span>{ ' ' }
email has been successfully used to log in to your Blockscout account.
</Text>
{ !profile?.address_hash && (
<>
<Text mt={ 6 }>Add your web3 wallet to safely interact with smart contracts and dapps inside Blockscout.</Text>
<Button mt={ 6 } onClick={ handleConnectWalletClick }>Connect wallet</Button>
</>
) }
</Box>
);
};
export default React.memo(AuthModalScreenSuccessEmail);
......@@ -2,30 +2,47 @@ import { chakra, Box, Text, Button } from '@chakra-ui/react';
import React from 'react';
import type { Screen } from '../types';
import type { UserInfo } from 'types/api/account';
import shortenString from 'lib/shortenString';
interface Props {
address: string;
onAddEmail: (screen: Screen) => void;
isAuth?: boolean;
profile: UserInfo | undefined;
}
const AuthModalScreenSuccessCreatedWallet = ({ address, onAddEmail }: Props) => {
const AuthModalScreenSuccessWallet = ({ address, onAddEmail, isAuth, profile }: Props) => {
const handleAddEmailClick = React.useCallback(() => {
onAddEmail({ type: 'email', isAccountExists: true });
onAddEmail({ type: 'email', isAuth: true });
}, [ onAddEmail ]);
if (isAuth) {
return (
<Box>
<Text>
Your account was linked to{ ' ' }
<chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' }
wallet. Use for the next login.
</Text>
);
}
return (
<Box>
<Text>
Wallet{ ' ' }
<chakra.span fontWeight="700">{ shortenString(address) }</chakra.span>{ ' ' }
has been successfully used to log in to your Blockscout account.
</Text>
{ !profile?.email && (
<>
<Text mt={ 6 }>Add your email to receive notifications about addresses in your watch list.</Text>
<Button mt={ 6 } onClick={ handleAddEmailClick }>Add email</Button>
</>
) }
</Box>
);
};
export default React.memo(AuthModalScreenSuccessCreatedWallet);
export default React.memo(AuthModalScreenSuccessWallet);
import type { UserInfo } from 'types/api/account';
export type ScreenSuccess = {
type: 'success_email';
email: string;
profile?: UserInfo;
isAuth?: boolean;
} | {
type: 'success_wallet';
address: string;
profile?: UserInfo;
isAuth?: boolean;
}
export type Screen = {
type: 'select_method';
} | {
type: 'connect_wallet';
isAuth?: boolean;
} | {
type: 'email';
isAccountExists?: boolean;
isAuth?: boolean;
} | {
type: 'otp_code';
email: string;
} | {
type: 'success_created_email';
} | {
type: 'success_created_wallet';
address: string;
}
isAuth?: boolean;
} | ScreenSuccess;
export interface EmailFormFields {
email: string;
......
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Route } from 'nextjs-routes';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
const PROTECTED_ROUTES: Array<Route['pathname']> = [
......@@ -17,17 +19,21 @@ const PROTECTED_ROUTES: Array<Route['pathname']> = [
export default function useLogout() {
const router = useRouter();
const queryClient = useQueryClient();
return React.useCallback(async() => {
cookies.remove(cookies.NAMES.API_TOKEN);
queryClient.resetQueries({
queryKey: getResourceKey('user_info'),
exact: true,
});
if (
PROTECTED_ROUTES.includes(router.pathname) ||
(router.pathname === '/txs' && router.query.tab === 'watchlist')
) {
window.location.assign('/');
} else {
window.location.reload();
router.push({ pathname: '/' }, undefined, { shallow: true });
}
}, [ router ]);
}, [ queryClient, router ]);
}
import { Box, Divider, Flex, Text, VStack } from '@chakra-ui/react';
import { Box, Divider, Flex, Link, Text, VStack } from '@chakra-ui/react';
import React from 'react';
import type { NavLink } from './types';
......@@ -45,10 +45,11 @@ const navLinks: Array<NavLink> = [
interface Props {
data: UserInfo;
onClose?: () => void;
onClose: () => void;
onAddEmail: () => void;
}
const UserProfileContent = ({ data, onClose }: Props) => {
const UserProfileContent = ({ data, onClose, onAddEmail }: Props) => {
const { isAutoConnectDisabled } = useMarketplaceContext();
const logout = useLogout();
......@@ -63,7 +64,10 @@ const UserProfileContent = ({ data, onClose }: Props) => {
icon="profile"
onClick={ onClose }
/>
{ data?.email && <Text variant="secondary" fontSize="sm">{ getUserHandle(data.email) }</Text> }
{ data?.email ?
<Text variant="secondary" fontSize="sm">{ getUserHandle(data.email) }</Text> :
<Link onClick={ onAddEmail } color="text_secondary" fontSize="sm" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add email</Link>
}
</Flex>
{ config.features.blockchainInteraction.isEnabled ? <UserProfileContentWallet onClose={ onClose }/> : <Divider/> }
......
......@@ -2,6 +2,8 @@ import { PopoverBody, PopoverContent, PopoverTrigger, useDisclosure, type Button
import { useRouter } from 'next/router';
import React from 'react';
import type { Screen } from 'ui/snippets/auth/types';
import config from 'configs/app';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import Popover from 'ui/shared/chakra/Popover';
......@@ -17,6 +19,9 @@ interface Props {
}
const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) => {
const [ authInitialScreen, setAuthInitialScreen ] = React.useState<Screen>({
type: config.features.blockchainInteraction.isEnabled ? 'select_method' : 'email',
});
const router = useRouter();
const authModal = useDisclosure();
......@@ -39,6 +44,11 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
authModal.onOpen();
}, [ profileQuery.data, router.pathname, authModal, profileMenu, signInWithWallet ]);
const handleAddEmailClick = React.useCallback(() => {
setAuthInitialScreen({ type: 'email', isAuth: true });
authModal.onOpen();
}, [ authModal ]);
return (
<>
<Popover openDelay={ 300 } placement="bottom-end" isLazy isOpen={ profileMenu.isOpen } onClose={ profileMenu.onClose }>
......@@ -54,7 +64,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
{ profileQuery.data && (
<PopoverContent maxW="280px" minW="220px" w="min-content">
<PopoverBody>
<UserProfileContent data={ profileQuery.data } onClose={ profileMenu.onClose }/>
<UserProfileContent data={ profileQuery.data } onClose={ profileMenu.onClose } onAddEmail={ handleAddEmailClick }/>
</PopoverBody>
</PopoverContent>
) }
......@@ -62,7 +72,7 @@ const UserProfileDesktop = ({ buttonSize, buttonVariant = 'header' }: Props) =>
{ authModal.isOpen && (
<AuthModal
onClose={ authModal.onClose }
initialScreen={{ type: config.features.blockchainInteraction.isEnabled ? 'select_method' : 'email' }}
initialScreen={ authInitialScreen }
/>
) }
</>
......
......@@ -2,6 +2,8 @@ import { Drawer, DrawerBody, DrawerContent, DrawerOverlay, useDisclosure } from
import { useRouter } from 'next/router';
import React from 'react';
import type { Screen } from 'ui/snippets/auth/types';
import config from 'configs/app';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import AuthModal from 'ui/snippets/auth/AuthModal';
......@@ -11,6 +13,9 @@ import UserProfileButton from './UserProfileButton';
import UserProfileContent from './UserProfileContent';
const UserProfileMobile = () => {
const [ authInitialScreen, setAuthInitialScreen ] = React.useState<Screen>({
type: config.features.blockchainInteraction.isEnabled ? 'select_method' : 'email',
});
const router = useRouter();
const authModal = useDisclosure();
......@@ -33,6 +38,11 @@ const UserProfileMobile = () => {
authModal.onOpen();
}, [ profileQuery.data, router.pathname, authModal, profileMenu, signInWithWallet ]);
const handleAddEmailClick = React.useCallback(() => {
setAuthInitialScreen({ type: 'email', isAuth: true });
authModal.onOpen();
}, [ authModal ]);
return (
<>
<UserProfileButton
......@@ -50,7 +60,7 @@ const UserProfileMobile = () => {
<DrawerOverlay/>
<DrawerContent maxWidth="300px">
<DrawerBody p={ 6 }>
<UserProfileContent data={ profileQuery.data } onClose={ profileMenu.onClose }/>
<UserProfileContent data={ profileQuery.data } onClose={ profileMenu.onClose } onAddEmail={ handleAddEmailClick }/>
</DrawerBody>
</DrawerContent>
</Drawer>
......@@ -58,7 +68,7 @@ const UserProfileMobile = () => {
{ authModal.isOpen && (
<AuthModal
onClose={ authModal.onClose }
initialScreen={{ type: config.features.blockchainInteraction.isEnabled ? 'select_method' : 'email' }}
initialScreen={ authInitialScreen }
/>
) }
</>
......
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