diff --git a/packages/interface/src/features/signup/components/RegisterEmail.tsx b/packages/interface/src/features/signup/components/RegisterEmail.tsx index 1221d32..00e4346 100644 --- a/packages/interface/src/features/signup/components/RegisterEmail.tsx +++ b/packages/interface/src/features/signup/components/RegisterEmail.tsx @@ -1,62 +1,51 @@ -import { Dispatch, SetStateAction, useState } from "react" -import { toast } from "sonner" +import { useState } from "react"; +import { toast } from "sonner"; -import { config } from "~/config" -import { Form, FormControl, FormSection } from "~/components/ui/Form" -import { Input } from "~/components/ui/Input" -import { EmailFieldSchema, EmailField } from "../types" -import { Button } from "~/components/ui/Button" -import { Spinner } from "~/components/ui/Spinner" +import { config } from "~/config"; +import { Form, FormControl, FormSection } from "~/components/ui/Form"; +import { Input } from "~/components/ui/Input"; +import { EmailFieldSchema, EmailField } from "../types"; +import { Button } from "~/components/ui/Button"; +import { Spinner } from "~/components/ui/Spinner"; interface IRegisterEmailProps { - emailField: - | { - email: string - } - | undefined - setEmail: Dispatch< - SetStateAction< - | { - email: string - } - | undefined - > - > + otpVerified: boolean; + setEmail: (emailField: EmailField) => void; } const RegisterEmail = ({ - emailField, + otpVerified, setEmail, }: IRegisterEmailProps): JSX.Element => { - const [registering, setRegistering] = useState(false) + const [registering, setRegistering] = useState(false); const registerEmail = async (emailField: EmailField) => { try { - setRegistering(true) + setRegistering(true); const response = await fetch(`${config.backendUrl}/send-otp`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify(emailField), - }) - const json = await response.json() + }); + const json = await response.json(); if (!response.ok) { - console.log(response.status) - console.error(json) - toast.error((json.errors && json.errors[0]) ?? json.message) + console.log(response.status); + console.error(json); + toast.error((json.errors && json.errors[0]) ?? json.message); } else { - setEmail(emailField) - toast.success(`OTP has been sent to ${emailField.email}`) + setEmail(emailField); + toast.success(`OTP has been sent to ${emailField.email}`); } } catch (error: any) { - console.error(error) - toast.error("An unexpected error occured registering your email") + console.error(error); + toast.error("An unexpected error occured registering your email"); } finally { - setRegistering(false) + setRegistering(false); } - } + }; return (
@@ -74,20 +63,20 @@ const RegisterEmail = ({ label="Email address" name="email" > - +
- ) -} + ); +}; -export default RegisterEmail +export default RegisterEmail; diff --git a/packages/interface/src/features/signup/components/VerifyOtp.tsx b/packages/interface/src/features/signup/components/VerifyOtp.tsx index 1c74fc7..5dbaad1 100644 --- a/packages/interface/src/features/signup/components/VerifyOtp.tsx +++ b/packages/interface/src/features/signup/components/VerifyOtp.tsx @@ -1,47 +1,93 @@ -import { useState } from "react" -import { useRouter } from "next/router" -import { toast } from "sonner" -import { Address, encodeAbiParameters, parseAbiParameters } from "viem" -import { publicClient } from "~/utils/permissionless" -import { Identity } from "@semaphore-protocol/core" -import SemaphoreAbi from "~/utils/Semaphore.json" - -import { config, semaphore } from "~/config" -import { Form, FormControl, FormSection } from "~/components/ui/Form" -import { Input } from "~/components/ui/Input" -import { OtpFieldSchema, OtpField } from "../types" -import { Button } from "~/components/ui/Button" -import useSmartAccount from "~/hooks/useSmartAccount" -import { getSemaphoreProof } from "~/utils/semaphore" -import { useMaci } from "~/contexts/Maci" -import { useEthersSigner } from "~/hooks/useEthersSigner" -import { Spinner } from "~/components/ui/Spinner" +import { Dispatch, SetStateAction, useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { toast } from "sonner"; +import { Address, encodeAbiParameters, parseAbiParameters } from "viem"; +import { publicClient } from "~/utils/permissionless"; +import { Identity } from "@semaphore-protocol/core"; +import SemaphoreAbi from "~/utils/Semaphore.json"; + +import { config, semaphore } from "~/config"; +import { Form, FormControl, FormSection } from "~/components/ui/Form"; +import { Input } from "~/components/ui/Input"; +import { OtpFieldSchema, OtpField } from "../types"; +import { Button } from "~/components/ui/Button"; +import useSmartAccount from "~/hooks/useSmartAccount"; +import { getSemaphoreProof } from "~/utils/semaphore"; +import { useMaci } from "~/contexts/Maci"; +import { useEthersSigner } from "~/hooks/useEthersSigner"; +import { Spinner } from "~/components/ui/Spinner"; +import { getHatsClient } from "~/utils/hatsProtocol"; +import { Signer } from "ethers"; interface IVerifyOtpProps { emailField: { - email: string - } + email: string; + }; + otpVerified: boolean; + setOtpVerified: Dispatch>; } -const VerifyOtp = ({ emailField }: IVerifyOtpProps): JSX.Element => { - const { address, smartAccount, smartAccountClient } = useSmartAccount() - const signer = useEthersSigner({ client: smartAccountClient }) - const { updateEligibility } = useMaci() - const router = useRouter() +const VerifyOtp = ({ + emailField, + otpVerified, + setOtpVerified, +}: IVerifyOtpProps): JSX.Element => { + const { address, smartAccount, smartAccountClient } = useSmartAccount(); + const signer = useEthersSigner({ client: smartAccountClient }); + const { updateEligibility } = useMaci(); + const router = useRouter(); + + const [loading, setLoading] = useState(false); + const [isWearerOfHat, setIsWearerOfHat] = useState(false); + const [isSemaphoreMember, setIsSemaphoreMember] = useState(false); + + useEffect(() => { + const getElegibility = async () => { + if (!address || !smartAccount || !smartAccountClient) { + throw new Error("Smart account does not exist"); + } + + const semaphoreIdentity = localStorage.getItem("semaphoreIdentity"); + if (!semaphoreIdentity || !signer) { + throw new Error("No Semaphore Identity or signer"); + } + + const hatsClient = getHatsClient(); + const wearingHat = await hatsClient.isWearerOfHat({ + wearer: address!, + hatId: semaphore.hatId, + }); + setIsWearerOfHat(wearingHat); + + const identityCommitment = new Identity(semaphoreIdentity).commitment; - const [verifying, setVerifying] = useState(false) + // check if already signed to semaphore + const isMember = await publicClient.readContract({ + address: semaphore.contracts.semaphore as Address, + abi: SemaphoreAbi.abi, + functionName: "hasMember", + args: [1n, identityCommitment] + }) as unknown as boolean; + + setIsSemaphoreMember(isMember); + } + + getElegibility().catch(console.error); + }, [otpVerified, address, smartAccount, smartAccountClient, signer, setIsWearerOfHat, setIsSemaphoreMember]); const verifyOtp = async (otpField: OtpField) => { + if (!address) { + throw new Error("Smart account does not exist"); + } + + let response: Response | undefined; try { - setVerifying(true) - if (!address) { - throw new Error("Smart account does not exist") - } + setLoading(true); const { email: email } = emailField // the component that can call this function only renders when the email exists const { otp: otp } = otpField - const response = await fetch(`${config.backendUrl}/verify-otp`, { + response = await fetch(`${config.backendUrl}/verify-otp`, { method: "POST", headers: { "Content-Type": "application/json", @@ -51,89 +97,128 @@ const VerifyOtp = ({ emailField }: IVerifyOtpProps): JSX.Element => { otp, address, }), - }) - const json = await response.json() - - if (!response.ok) { - console.log(response.status) - console.error(json) - toast.error((json.errors && json.errors[0]) ?? json.message) - } else { - toast.success("OTP verified - now joining Semaphore group") - await joinSemaphoreGroup() - router.push("/signup") - } + }); } catch (error: any) { - console.error(error) - toast.error("An unexpected error occured verifying the OTP") - } finally { - setVerifying(false) + console.error(error); + setLoading(false); + toast.error( + `An unexpected error occured verifying the OTP ${error instanceof Error ? `- ${error.message}` : ""}` + ); + return; + } + + if (!response.ok) { + const json = await response.json(); + console.log(response.status); + console.error(json); + toast.error((json.errors && json.errors[0]) ?? json.message); + setLoading(false); + } else { + toast.success("OTP verified - now joining Semaphore group"); + setOtpVerified(true); + await joinSemaphoreGroup(); } } const joinSemaphoreGroup = async () => { - if (!smartAccount || !smartAccountClient) { - throw new Error("Smart account does not exist") - } + try { + setLoading(true); + if (!address || !smartAccount || !smartAccountClient) { + throw new Error("Smart account does not exist"); + } + + const semaphoreIdentity = localStorage.getItem("semaphoreIdentity"); + if (!semaphoreIdentity || !signer) { + throw new Error("No Semaphore Identity or signer"); + } + + if (!isWearerOfHat) { + throw new Error("Account not wearing hat"); + } - const semaphoreIdentity = localStorage.getItem("semaphoreIdentity") - if (!semaphoreIdentity || !signer) { - throw new Error("No Semaphore Identity or signer") + const identityCommitment = new Identity(semaphoreIdentity).commitment; + const data = encodeAbiParameters(parseAbiParameters("uint"), [ + semaphore.hatId, + ]); + + const { request } = await publicClient.simulateContract({ + account: smartAccount, + address: semaphore.contracts.semaphore as Address, + abi: SemaphoreAbi.abi, + functionName: "gateAndAddMember", + args: [identityCommitment, data], + }); + const txHash = await smartAccountClient.writeContract(request); + console.log("txHash", txHash); + + // TODO: (merge-ok) come up with a better fix + await new Promise((resolve) => setTimeout(resolve, 20000)); + toast.success("Joined Semaphore group"); + + await tryUpdateEligibility(signer, semaphoreIdentity); + } catch (error) { + toast.error( + `An unexpected error occured ${error instanceof Error ? `- ${error.message}` : ""}` + ); + } finally { + setLoading(false); } + }; - const identityCommitment = new Identity(semaphoreIdentity).commitment - const data = encodeAbiParameters(parseAbiParameters("uint"), [ - semaphore.hatId, - ]) - - const { request } = await publicClient.simulateContract({ - account: smartAccount, - address: semaphore.contracts.semaphore as Address, - abi: SemaphoreAbi.abi, - functionName: "gateAndAddMember", - args: [identityCommitment, data], - }) - const txHash = await smartAccountClient.writeContract(request) - console.log("txHash", txHash) - - // TODO: (merge-ok) come up with a better fix - await new Promise((resolve) => setTimeout(resolve, 20000)) - - const proof = await getSemaphoreProof( - signer, - new Identity(semaphoreIdentity) - ) - await updateEligibility(proof, address) - - toast.success("Joined Semaphore group") - } + const tryUpdateEligibility = async ( + signer: Signer, + semaphoreIdentity: string + ) => { + try { + const proof = await getSemaphoreProof( + signer, + new Identity(semaphoreIdentity) + ); + await updateEligibility(proof, address); + router.push("/signup"); + } catch { + throw new Error( + "Could not update eligibility but joined semaphore group. Navigate to the homepage and wait a few mins for eligibility to be updated" + ); + } + }; return (
-
verifyOtp(otp)}> - - - - - + +
+ )} + + { + isWearerOfHat && !isSemaphoreMember && ( + - - + ) + }
) } diff --git a/packages/interface/src/pages/signup/register.tsx b/packages/interface/src/pages/signup/register.tsx index 8859ad9..d262f0d 100644 --- a/packages/interface/src/pages/signup/register.tsx +++ b/packages/interface/src/pages/signup/register.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { format } from "date-fns"; import { EligibilityDialog } from "~/components/EligibilityDialog"; @@ -15,6 +15,19 @@ const Register = (): JSX.Element => { const { address } = useSmartAccount(); const [emailField, setEmail] = useState(); + const [otpVerified, setOtpVerified] = useState(false); + + const handleSetEmail = (emailField: EmailField) => { + setEmail(emailField); + localStorage.setItem("email", emailField.email); + }; + + useEffect(() => { + const email = localStorage.getItem("email"); + if (email) { + setEmail({ email }); + } + }, []); return ( @@ -41,9 +54,15 @@ const Register = (): JSX.Element => {

- + - {emailField && address && } + {emailField && address && ( + + )}