From 67524746281aea53b6f7928c90f890d808b595d6 Mon Sep 17 00:00:00 2001
From: ctrlc03 <93448202+ctrlc03@users.noreply.github.com>
Date: Thu, 5 Sep 2024 20:40:48 +0100
Subject: [PATCH 1/3] feat: better signup error handling
---
.../signup/components/RegisterEmail.tsx | 69 ++---
.../features/signup/components/VerifyOtp.tsx | 290 ++++++++++++------
.../interface/src/pages/signup/register.tsx | 25 +-
3 files changed, 240 insertions(+), 144 deletions(-)
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..244157e 100644
--- a/packages/interface/src/features/signup/components/VerifyOtp.tsx
+++ b/packages/interface/src/features/signup/components/VerifyOtp.tsx
@@ -1,47 +1,89 @@
-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 () => {
+ const hatsClient = getHatsClient();
+ const wearingHat = await hatsClient.isWearerOfHat({
+ wearer: address!,
+ hatId: semaphore.hatId,
+ });
+ setIsWearerOfHat(wearingHat);
+
+ const semaphoreIdentity = localStorage.getItem("semaphoreIdentity");
+ if (!semaphoreIdentity) {
+ return;
+ }
+
+ const identityCommitment = new Identity(semaphoreIdentity).commitment;
+
+ // 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);
+ }
- const [verifying, setVerifying] = useState(false)
+ getElegibility().catch(console.error);
+ }, [otpVerified, 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 +93,135 @@ 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");
+ }
+
+ const hatsClient = getHatsClient();
+ const wearingHat = await hatsClient.isWearerOfHat({
+ wearer: address!,
+ hatId: semaphore.hatId,
+ });
+ setIsWearerOfHat(wearingHat);
+
+ if (!wearingHat) {
+ 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 (
-
+ )}
+
+ {
+ 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 && (
+
+ )}
From 16305be96204543960a51b3750b99b1a114f5e3f Mon Sep 17 00:00:00 2001
From: JohnGuilding
Date: Thu, 5 Sep 2024 21:12:58 +0100
Subject: [PATCH 2/3] Modify how form is rendered
---
packages/interface/src/features/signup/components/VerifyOtp.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/interface/src/features/signup/components/VerifyOtp.tsx b/packages/interface/src/features/signup/components/VerifyOtp.tsx
index 244157e..4c22c76 100644
--- a/packages/interface/src/features/signup/components/VerifyOtp.tsx
+++ b/packages/interface/src/features/signup/components/VerifyOtp.tsx
@@ -188,7 +188,7 @@ const VerifyOtp = ({
return (
- {!otpVerified && (
+ {!isWearerOfHat && !isSemaphoreMember && (