Skip to content

Instantly share code, notes, and snippets.

@Kais3rP
Created September 8, 2023 08:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kais3rP/969857e2641ed8d0f02e9e8eb6a33b39 to your computer and use it in GitHub Desktop.
Save Kais3rP/969857e2641ed8d0f02e9e8eb6a33b39 to your computer and use it in GitHub Desktop.
import { useCallback, useRef, useState } from 'react';
import { useBlogStore, useBlogUIStore } from 'store';
import { calculateRelativePosition, shortenAddress } from 'utils';
import {
useAccount,
useEnsName,
useEnsAvatar,
useConnect,
useDisconnect,
useNetwork,
useSignMessage,
useSwitchNetwork,
} from 'wagmi';
import { SiweMessage } from 'siwe';
import jwtDecode from 'jwt-decode';
import { googleLogout } from '@react-oauth/google';
import { getData, postJSONData } from 'libs/fetch';
import { siweMessage } from 'data/web3';
export const useWeb3Login = ({ elementContainer }) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const modalWalletsRef = useRef(null);
/* const { setIsFirstConnectAuthentication, isFirstConnectAuthentication } =
useGlobalStore(); */
const { updateWeb3User, removeWeb3User, updateModal } = useBlogStore();
const { setCurrentConfetti } = useBlogUIStore();
/* Get the current chain */
const { chain } = useNetwork();
/* Get the Wallet connector and current active address of the connected account */
const { connector: activeConnector, address } = useAccount({
async onConnect({ address, connector, isReconnected }) {
/* This is triggered everytime a route changes and the Layout is rerendered hence not very useless using NextJS */
},
});
/* Handle chain switch */
/* If the user is not on Ethereum network on connect it's prompted to change network */
const {
chains,
// isLoading: isLoadingChains,
// pendingChainId,
switchNetwork,
} = useSwitchNetwork({
throwForSwitchChainNotSupported: true,
chainId: 1,
onMutate() {
/* Sign message on chain change if it's Ethereum chain */
console.log('Chain mutate', address, chain);
if (chain?.id === 1) {
// signMessage({ address, chainId: chain?.id });
updateModal({
title: 'Network',
body: `You are on Ethereum mainnet`,
type: 'ok',
});
} else {
// switchNetwork?.(1);
updateModal({
title: 'Network',
body: `You are not on Ethereum mainnet`,
type: 'warn',
});
}
},
onSuccess() {
console.log('Chain changed', address, chain);
if (chain?.id === 1) {
signMessage({ address, chainId: chain?.id });
updateModal({
title: 'Network',
body: `You are on Ethereum mainnet`,
type: 'ok',
});
}
},
onError(e) {
console.log('Chain error', e);
handleWeb3Logout();
},
});
/* Connect / Disconnect */
const { connectAsync, connectors, isLoading, pendingConnector } =
useConnect({
onError() {
removeWeb3User();
updateModal({
title: 'Connect',
body: `Connection error, please refresh the page or try with a different wallet`,
type: 'error',
});
},
onSuccess(data) {
if (chain?.id === 1)
signMessage({ address, chainId: chain?.id });
else switchNetwork?.(1);
updateWeb3User({ address: data.account });
// This is the Global modal
updateModal({
title: 'Connect',
body: `You connected your wallet ${shortenAddress(
data.account
)}`,
type: 'ok',
});
/* setCurrentConfetti({
id: 'web3-login-confetti',
position: calculateRelativePosition(elementContainer),
}); */
},
});
const { disconnect } = useDisconnect({
onError() {
updateModal({
title: 'Disconnect',
body: `Disconnection error, please disconnect your wallet manually`,
type: 'error',
});
},
onSuccess() {
removeWeb3User();
updateModal({
title: 'Disconnect',
body: `You disconnected your wallet`,
type: 'ok',
});
},
});
/* Sign a message */
const {
data: signData,
isLoading: isSignLoading,
signMessageAsync,
} = useSignMessage({
onError(e) {
updateWeb3User({ signed: false, signData: null });
disconnect();
updateModal({
title: 'Sign with Ethereum',
body: `You need to sign in order to authenticate`,
type: 'error',
});
},
onSuccess(data) {
updateWeb3User({ signed: true, signData: data });
// This is the Global modal
updateModal({
title: 'Sign with Ethereum',
body: `Signature received correctly`,
type: 'ok',
});
// This is the Web3 Wallets Connectors selection modal
hideModal();
setCurrentConfetti({
id: 'web3-login-confetti',
position: calculateRelativePosition(elementContainer),
});
},
});
/* Get ENS name */
const { data: ensName } = useEnsName({
address,
onError(e) {
updateWeb3User({ ensName: null });
// TO-DO fix the retrigger of this hook on each new render
/* updateModal({
title: 'ENS',
body: `Couldn't retrieve an ENS name for this address, check that you are currently on the right network.`,
type: 'warn',
}); */
},
onSuccess(data) {
updateWeb3User({ ensName: data });
// This is the Global modal
updateModal({
title: 'ENS',
body: `ENS name retrieved ${data}`,
type: 'ok',
});
// This is the Web3 Wallets Connectors selection modal
hideModal();
setCurrentConfetti({
id: 'web3-login-confetti',
position: calculateRelativePosition(elementContainer),
});
},
});
/* Get ENS Avatar */
const { data: ensAvatar } = useEnsAvatar({
ensName,
onError(e) {
updateWeb3User({ ensAvatar: null });
updateModal({
title: 'ENS',
body: `Couldn't retrieve an ENS Avatar for this address, check that you are currently on the right network`,
type: 'warn',
});
},
onSuccess(data) {
updateWeb3User({ ensAvatar: data });
// This is the Global modal
updateModal({
title: 'ENS',
body: `ENS Avatar retrieved ${data}`,
type: 'ok',
});
// This is the Web3 Wallets Connectors selection modal
hideModal();
setCurrentConfetti({
id: 'web3-login-confetti',
position: calculateRelativePosition(elementContainer),
});
},
});
/* Handlers */
const signMessage = useCallback(
async ({ address, chainId }) => {
const nonce = await getData('/api/v2/web3/generate-nonce');
// console.log(address, chainId, nonce);
/* Now send the sign message */
const message = new SiweMessage({
domain: window.location.host,
address,
statement: siweMessage,
uri: window.location.origin,
version: '1',
chainId,
nonce, // Retrieved on first load
});
console.log(message);
const signature = await signMessageAsync({
message: message.prepareMessage(),
});
// console.log('Signature:', signature);
// Verify signature
await postJSONData(
'/api/v2/web3/verify-signature',
JSON.stringify({ message, signature })
);
// console.log(verifyRes);
updateModal({
title: 'Sign',
body: 'The signature was verified correctly',
type: 'ok',
});
},
[signMessageAsync, updateModal]
);
const handleWeb3Login = useCallback(
async (connector) => {
try {
/* First step connect and retrieve address and chain */
await connectAsync({ connector });
/* Handle the rest of the procedure, the signature method, directly in the onConnect callback of useAccount */
} catch (e) {
console.log(e.name);
updateModal({
title: 'Error',
body:
e.name === 'ResourceNotFoundRpcError'
? 'You already have a pending connection request in your wallet'
: e.name,
type: 'error',
});
// handleWeb3Logout();
}
},
[connectAsync, updateModal]
);
const handleWeb3Logout = useCallback(async () => {
try {
disconnect(); // Disconnects from the wallet
/* Destroy the SIWE session */
await getData('/api/v2/session/logout');
} catch (e) {
updateModal({
title: 'Error',
body: e,
type: 'error',
});
}
}, [disconnect, updateModal]);
/* UI Handlers */
const showModal = () => setIsModalVisible(true);
const hideModal = () => setIsModalVisible(false);
return {
connectAsync,
disconnect,
handleWeb3Login,
handleWeb3Logout,
showModal,
hideModal,
connectors,
isLoading,
pendingConnector,
activeConnector,
isModalVisible,
address,
ensName,
ensAvatar,
chain,
chains,
signData,
isSignLoading,
modalWalletsRef,
};
};
import { useCallback, useEffect, useRef, useState } from 'react';
import { useBlogUIStore, useBlogStore } from 'store';
import GenericButton from 'components/shared/Buttons/GenericButton';
import { useWeb3Login } from 'hooks/authentication';
import { useOnClickOutside } from 'hooks';
import metaMaskLogo from 'assets/metamask.svg';
import coinbaseLogo from 'assets/coinbase.svg';
import ledgerLogo from 'assets/ledger.svg';
import safeWalletLogo from 'assets/safe-wallet.svg';
import genericLogo from 'assets/generic-wallet.svg';
import { shortenAddress } from 'utils';
import { MdOutlineKeyboardArrowDown } from 'react-icons/md';
import LoadingSquares from 'components/shared/Spinners/LoadingSquares';
import Link from 'next/link';
const Web3Login = () => {
const loginRef = useRef(null);
const buttonModalRef = useRef(null);
const [isButtonModalVisible, setisButtonModalVisible] = useState(false);
/* Stores */
const { web3User, updateModal } = useBlogStore();
const { isDarkMode } = useBlogUIStore();
/* Web3 Login */
const {
handleWeb3Login,
handleWeb3Logout,
showModal,
hideModal,
connectors,
isLoading,
pendingConnector,
activeConnector,
isModalVisible,
isSignLoading,
modalWalletsRef,
} = useWeb3Login({ elementContainer: loginRef.current }); // The element container is used to show the confetti animation
/* ButtonModal is the dropdown that contains info about the connected wallet. It's showed when clicking on the login button */
const showButtonModal = () => setisButtonModalVisible(true);
const hideButtonModal = () => setisButtonModalVisible(false);
/* Close the wallets modal and diconnect on click outside during the login attempt */
useOnClickOutside(modalWalletsRef, () => {
hideModal();
if (activeConnector && isModalVisible) handleWeb3Logout();
});
/* Close the button modal on click outside */
// useOnClickOutside(buttonModalRef, hideButtonModal);
/* Close the button modal if the user disconnects */
useEffect(() => {
if (!web3User) hideButtonModal();
}, [web3User]);
/* Handle the connect click */
const handleConnectButtonClick = useCallback(() => {
// if (isSignLoading) return;
if (!activeConnector) showModal();
else {
if (isButtonModalVisible) hideButtonModal();
else showButtonModal();
}
}, [activeConnector, isButtonModalVisible, showModal]);
return (
<div className="max-w-[215px] relative z-10 flex flex-col">
<GenericButton
ref={loginRef}
isDarkMode={isDarkMode}
theme={{
primary: 'var(--bg-light)',
secondary: 'var(--blue1)',
}}
className={`relative text-xs flex justify-center items-center whitespace-nowrap`}
onPointerUp={handleConnectButtonClick}
>
{!activeConnector ? (
'Connect with Ethereum'
) : isSignLoading || isLoading ? (
'Check your wallet...'
) : (
<div className="pr-3">
<h5>
{web3User?.ensName ||
shortenAddress(web3User?.address)}
</h5>{' '}
<div
style={{
transform: isButtonModalVisible
? 'rotateZ(180deg) translateY(50%)'
: ' translateY(-50%)',
}}
className="absolute top-[50%] right-[10px] cursor-pointer transition-all text-xl"
>
<MdOutlineKeyboardArrowDown />
</div>
</div>
)}
</GenericButton>
{/* Drop down */}
<div
style={{
height: isButtonModalVisible ? '100%' : '0px',
}}
ref={buttonModalRef}
className={`w-[305px] overflow-hidden transition-all`}
>
<div
className={`relative min-h-[97px] rounded-[10px] transition-all p-2`}
>
<div
className={`${
isDarkMode
? 'bg-bgLight text-colorLight'
: 'bg-bgDark text-colorDark'
} w-[40px] h-[40px] rounded-full flex justify-center items-center float-left text-[0.5rem] overflow-hidden`}
>
{web3User?.ensAvatar ? (
<img
className=""
alt="avatar"
src={web3User?.ensAvatar}
/>
) : (
<h5 className="w-[80%] text-ellipsis overflow-hidden text-center">
{shortenAddress(web3User?.address, 'short')}
</h5>
)}
</div>
<h5 className="w-full flex pl-2 text-xs">
Address:
<i className="ml-1">
{shortenAddress(web3User?.address, 'long')}
</i>
</h5>
<h5 className="w-full flex pl-2 text-xs">
ENS name:{' '}
{web3User?.ensName || (
<a
className={`underline text-xs ml-1 ${
isDarkMode
? 'hover:text-colorLight'
: 'hover:text-colorDark'
} transition-all whitespace-nowrap`}
target="_blank"
href="https://ens.domains/"
rel="noopener noreferrer"
>
Get one here
</a>
)}
</h5>
<button
onPointerUp={handleWeb3Logout}
className={`underline text-xs m-1 ${
isDarkMode
? 'hover:text-colorLight'
: 'hover:text-colorDark'
} transition-all`}
>
Disconnect
</button>
</div>
</div>
{/* Wallets selector fixed position */}
<div
style={{
opacity: isModalVisible ? 1 : 0,
pointerEvents: isModalVisible ? 'all' : 'none',
'-webkit-backdrop-filter': 'blur(12px)',
backdropFilter: 'blur(12px)',
}}
className="w-[100vw] h-[100vh] fixed top-0 left-0 z-[5000] flex justify-center items-center"
>
{/* Wallet selector */}
<div
ref={modalWalletsRef}
style={{
backgroundColor: isDarkMode
? 'var(--bg-dark)'
: 'var(--bg-light)',
boxShadow: `0px 0px 5px 1px ${
isDarkMode ? 'var(--bg-light)' : 'var(--bg-dark)'
}
`,
transform: isModalVisible
? 'translateY(0px)'
: 'translateY(200px)',
opacity: isModalVisible ? 1 : 0,
}}
className="relative rounded-lg flex flex-col max-w-[350px] p-5 overflow-hidden transition-transform"
>
{/* Sign in Loading overlay */}
{isSignLoading && (
<div
style={{
backgroundColor: isDarkMode
? 'var(--bg-dark)'
: 'var(--bg-light)',
}}
className="absolute z-[10] w-full h-full top-0 left-0 flex justify-center items-center"
>
<div className="flex flex-col justify-center items-center">
<h5>Sign in your wallet</h5>
<div className="m-5">
<LoadingSquares
size={30}
color={
isDarkMode
? 'var(--bg-light)'
: 'var(--bg-dark)'
}
/>
</div>
</div>
</div>
)}
<div className="w-full text-center mb-2">
Select your wallet
</div>
<div className="w-full text-center mb-2">
{connectors.map((connector) => (
<GenericButton
disabled={!isModalVisible}
isDarkMode={isDarkMode}
key={connector.id}
onPointerUp={
!connector.ready
? () =>
updateModal({
title: 'Error',
body: 'The wallet connector is not ready, try to refresh the page',
type: 'warn',
})
: () => handleWeb3Login(connector)
}
className="my-1 text-sm"
theme={{
primary: 'var(--bg-light)',
secondary: 'var(--blue1)',
}}
>
<div className="flex items-center">
<div className={`w-[30px] h-[30px]`}>
<img
src={
connector?.id === 'metaMask'
? metaMaskLogo.src
: connector?.id ===
'coinbaseWallet'
? coinbaseLogo.src
: connector?.id === 'ledger'
? ledgerLogo.src
: connector?.id === 'safe'
? safeWalletLogo.src
: genericLogo.src
}
alt="wallet-logo"
/>
</div>
<h5 className="font-semibold ml-2">
{connector?.name === 'Injected'
? 'Default Wallet'
: connector?.name}
</h5>
<div className="ml-2">
{(isLoading || pendingConnector) &&
pendingConnector?.id ===
connector.id && (
<LoadingSquares
size={15}
color={
isDarkMode
? 'var(--bg-light)'
: 'var(--bg-dark)'
}
/>
)}
</div>
</div>
</GenericButton>
))}
</div>
<div className="text-xs leading-4 text-justify">
By connecting to the wallet, you automatically agree to
our{' '}
<Link
className="underline hover:opacity-[0.9] mb-1 whitespace-nowrap text-blue1"
href="/terms"
>
Terms of service
</Link>{' '}
and{' '}
<Link
className="underline hover:opacity-[0.9] whitespace-nowrap mb-1 text-blue1"
href="/privacypolicy"
>
Privacy Policy
</Link>
.
<h5 className="mt-3 text-[0.6rem]">
{' '}
New to Ethereum?{' '}
<Link
className="underline hover:opacity-[0.9] whitespace-nowrap mb-1 text-blue1"
href="https://ethereum.org/en/wallets/"
target="_blank"
rel="noreferrer noopener"
>
learn more
</Link>{' '}
about wallets
</h5>
</div>
</div>
</div>
</div>
);
};
export default Web3Login;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment