import Router from "next/router"; import React, { useCallback, useMemo, useState } from "react"; import Web3 from "web3"; import type { AbstractProvider } from "web3-core"; import { AbiItem } from "web3-utils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; import { Button } from "@calcom/ui/Button"; import { withLicenseRequired } from "../../common/components/LicenseRequired"; import genericAbi from "../abis/abiWithGetBalance.json"; import { useContracts } from "../contexts/contractsContext"; import verifyAccount, { AUTH_MESSAGE } from "../utils/verifyAccount"; interface Window { ethereum: AbstractProvider & { selectedAddress: string }; web3: Web3; } interface EvtsToVerify { [eventId: string]: boolean; } declare const window: Window; export interface CryptoSectionProps { id: number | string; pathname: string; smartContractAddress: string; /** When set to true, there will be only 1 button which will both connect Metamask and verify the user's wallet. Otherwise, it will be in 2 steps with 2 buttons. */ oneStep: boolean; verified: boolean | undefined; setEvtsToVerify: React.Dispatch>>; } const CryptoSection = (props: CryptoSectionProps) => { // Crypto section which should be shown on booking page if event type requires a smart contract token. const [ethEnabled, toggleEthEnabled] = useState(false); const { addContract } = useContracts(); const { t } = useLocale(); const connectMetamask = useCallback(async () => { if (window.ethereum && typeof window.ethereum.request === "function") { await window.ethereum.request({ method: "eth_requestAccounts" }); window.web3 = new Web3(window.ethereum); toggleEthEnabled(true); } else { toggleEthEnabled(false); } }, []); const verifyWallet = useCallback(async () => { try { if (!window.web3) throw new Error("MetaMask browser extension is not installed"); const contract = new window.web3.eth.Contract(genericAbi as AbiItem[], props.smartContractAddress); const balance = await contract.methods.balanceOf(window.ethereum.selectedAddress).call(); const hasToken = balance > 0; if (!hasToken) throw new Error("Specified wallet does not own any tokens belonging to this smart contract"); const [account] = await window.web3.eth.getAccounts(); const signature = await window.web3.eth.personal.sign(AUTH_MESSAGE, account, ""); addContract({ address: props.smartContractAddress, signature }); await verifyAccount(signature, account); props.setEvtsToVerify((prevState: EvtsToVerify) => ({ ...prevState, [props.id]: hasToken })); } catch (err) { const message = err instanceof Error ? err.message : "An error has occurred"; showToast(message, "error"); } }, [props, addContract]); // @TODO: Show error on either of buttons if fails. Yup schema already contains the error message. const successButton = useMemo(() => { if (props.verified) { Router.push(props.pathname); } return
; }, [props.pathname, props.verified]); const verifyButton = useMemo(() => { return ( ); }, [verifyWallet, t]); const connectButton = useMemo(() => { return ( ); }, [connectMetamask, t]); const oneStepButton = useMemo(() => { return ( ); }, [connectMetamask, verifyWallet, t]); const determineButton = useCallback(() => { // Did it in an extra function for some added readability, but this can be done in a ternary depending on preference if (props.oneStep) { return props.verified ? successButton : oneStepButton; } else { if (ethEnabled) { return props.verified ? successButton : verifyButton; } else { return connectButton; } } }, [props.verified, successButton, oneStepButton, connectButton, ethEnabled, props.oneStep, verifyButton]); return (
{determineButton()}
); }; export default withLicenseRequired(CryptoSection);