2022-02-01 21:48:40 +00:00
import Router from "next/router" ;
import { useCallback , useMemo , useState } from "react" ;
import React from "react" ;
import Web3 from "web3" ;
import { AbiItem } from "web3-utils" ;
2022-03-16 23:36:43 +00:00
import showToast from "@calcom/lib/notification" ;
import { Button } from "@calcom/ui/Button" ;
2022-02-01 21:48:40 +00:00
2022-03-16 23:36:43 +00:00
import { useLocale } from "@lib/hooks/useLocale" ;
2022-02-01 21:48:40 +00:00
import { useContracts } from "../../../contexts/contractsContext" ;
import genericAbi from "../../../web3/abis/abiWithGetBalance.json" ;
2022-02-02 11:07:13 +00:00
import verifyAccount , { AUTH_MESSAGE } from "../../../web3/utils/verifyAccount" ;
2022-02-01 21:48:40 +00:00
interface Window {
ethereum : any ;
web3 : Web3 ;
}
interface EvtsToVerify {
[ eventId : string ] : boolean ;
}
declare const window : Window ;
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 < React.SetStateAction < Record < number | string , boolean > >> ;
}
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 < boolean > ( false ) ;
const { addContract } = useContracts ( ) ;
const { t } = useLocale ( ) ;
const connectMetamask = useCallback ( async ( ) = > {
if ( window . ethereum ) {
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 ) {
2022-02-03 20:08:25 +00:00
throw new Error ( "MetaMask browser extension is not installed" ) ;
2022-02-01 21:48:40 +00:00
}
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" ) ;
} else {
2022-02-03 20:08:25 +00:00
const account = ( await window . web3 . eth . getAccounts ( ) ) [ 0 ] ;
2022-02-01 21:48:40 +00:00
2022-02-03 20:08:25 +00:00
// @ts-ignore
2022-02-01 21:48:40 +00:00
const signature = await window . web3 . eth . personal . sign ( AUTH_MESSAGE , account ) ;
2022-02-03 20:08:25 +00:00
// @ts-ignore
2022-02-01 21:48:40 +00:00
addContract ( { address : props.smartContractAddress , signature } ) ;
await verifyAccount ( signature , account ) ;
props . setEvtsToVerify ( ( prevState : EvtsToVerify ) = > {
const changedEvt = { [ props . id ] : hasToken } ;
return { . . . prevState , . . . changedEvt } ;
} ) ;
}
} catch ( err ) {
err instanceof Error ? showToast ( err . message , "error" ) : showToast ( "An error has occurred" , "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 < div / > ;
} , [ props . verified ] ) ;
const verifyButton = useMemo ( ( ) = > {
return (
< Button color = "secondary" onClick = { verifyWallet } type = "button" id = "hasToken" name = "hasToken" >
2022-03-23 22:00:30 +00:00
< img className = "mr-1 h-5" src = "/apps/metamask.svg" / >
2022-02-01 21:48:40 +00:00
{ t ( "verify_wallet" ) }
< / Button >
) ;
} , [ verifyWallet , t ] ) ;
const connectButton = useMemo ( ( ) = > {
return (
< Button color = "secondary" onClick = { connectMetamask } type = "button" >
2022-03-23 22:00:30 +00:00
< img className = "mr-1 h-5" src = "/apps/metamask.svg" / >
2022-02-01 21:48:40 +00:00
{ t ( "connect_metamask" ) }
< / Button >
) ;
} , [ connectMetamask , t ] ) ;
const oneStepButton = useMemo ( ( ) = > {
return (
< Button
color = "secondary"
type = "button"
onClick = { async ( ) = > {
await connectMetamask ( ) ;
await verifyWallet ( ) ;
} } >
2022-03-23 22:00:30 +00:00
< img className = "mr-1 h-5" src = "/apps/metamask.svg" / >
2022-02-01 21:48:40 +00:00
{ t ( "verify_wallet" ) }
< / Button >
) ;
} , [ 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 (
< div
2022-02-09 00:05:13 +00:00
className = "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 transform opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-900"
2022-02-01 21:48:40 +00:00
id = { ` crypto- ${ props . id } ` } >
{ determineButton ( ) }
< / div >
) ;
} ;
export default CryptoSection ;