Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import {
2 CheckIcon,
3 ExclamationTriangleIcon,
4 FaceFrownIcon,
5 FaceSmileIcon
6} from "@heroicons/react/24/outline";
7import { HEY_APP, IS_MAINNET } from "@hey/data/constants";
8import { ERRORS } from "@hey/data/errors";
9import { Regex } from "@hey/data/regex";
10import {
11 useAccountQuery,
12 useAuthenticateMutation,
13 useChallengeMutation,
14 useCreateAccountWithUsernameMutation
15} from "@hey/indexer";
16import { account as accountMetadata } from "@lens-protocol/metadata";
17import { useCallback, useState } from "react";
18import { toast } from "sonner";
19import { useAccount, useSignMessage } from "wagmi";
20import { z } from "zod";
21import AuthMessage from "@/components/Shared/Auth/AuthMessage";
22import { Button, Form, Input, useZodForm } from "@/components/Shared/UI";
23import errorToast from "@/helpers/errorToast";
24import uploadMetadata from "@/helpers/uploadMetadata";
25import useHandleWrongNetwork from "@/hooks/useHandleWrongNetwork";
26import useTransactionLifecycle from "@/hooks/useTransactionLifecycle";
27import { useSignupStore } from ".";
28
29export const SignupMessage = () => (
30 <AuthMessage
31 description="Let's start by buying your username for you. Buying you say? Yep - usernames cost a little bit of money to support the network and keep bots away"
32 title="Welcome to Hey!"
33 />
34);
35
36const ValidationSchema = z.object({
37 username: z
38 .string()
39 .min(3, { message: "Username must be at least 3 characters long" })
40 .max(26, { message: "Username must be at most 26 characters long" })
41 .regex(Regex.username, {
42 message:
43 "Username must start with a letter/number, only _ allowed in between"
44 })
45});
46
47const ChooseUsername = () => {
48 const {
49 setChosenUsername,
50 setScreen,
51 setTransactionHash,
52 setOnboardingToken
53 } = useSignupStore();
54 const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
55 const [isSubmitting, setIsSubmitting] = useState(false);
56 const { address } = useAccount();
57 const handleWrongNetwork = useHandleWrongNetwork();
58 const handleTransactionLifecycle = useTransactionLifecycle();
59 const form = useZodForm({ mode: "onChange", schema: ValidationSchema });
60
61 const onCompleted = (hash: string) => {
62 setIsSubmitting(false);
63 setChosenUsername(username);
64 setTransactionHash(hash);
65 setScreen("minting");
66 };
67
68 const onError = useCallback((error?: any) => {
69 setIsSubmitting(false);
70 errorToast(error);
71 }, []);
72
73 const { signMessageAsync } = useSignMessage({ mutation: { onError } });
74 const [loadChallenge] = useChallengeMutation({ onError });
75 const [authenticate] = useAuthenticateMutation({ onError });
76
77 const [createAccountWithUsername] = useCreateAccountWithUsernameMutation({
78 onCompleted: async ({ createAccountWithUsername }) => {
79 if (createAccountWithUsername.__typename === "CreateAccountResponse") {
80 return onCompleted(createAccountWithUsername.hash);
81 }
82
83 if (createAccountWithUsername.__typename === "UsernameTaken") {
84 return onError({ message: createAccountWithUsername.reason });
85 }
86
87 return await handleTransactionLifecycle({
88 onCompleted,
89 onError,
90 transactionData: createAccountWithUsername
91 });
92 },
93 onError
94 });
95
96 const username = form.watch("username");
97 const canCheck = Boolean(username && username.length > 2);
98 const isInvalid = !form.formState.isValid;
99
100 useAccountQuery({
101 fetchPolicy: "no-cache",
102 onCompleted: (data) => setIsAvailable(!data.account),
103 skip: !canCheck,
104 variables: {
105 request: { username: { localName: username?.toLowerCase() } }
106 }
107 });
108
109 const handleSignup = async ({
110 username
111 }: z.infer<typeof ValidationSchema>) => {
112 try {
113 setIsSubmitting(true);
114 await handleWrongNetwork();
115
116 const challenge = await loadChallenge({
117 variables: {
118 request: {
119 onboardingUser: {
120 app: IS_MAINNET ? HEY_APP : undefined,
121 wallet: address
122 }
123 }
124 }
125 });
126
127 if (!challenge?.data?.challenge?.text) {
128 return toast.error(ERRORS.SomethingWentWrong);
129 }
130
131 // Get signature
132 const signature = await signMessageAsync({
133 message: challenge?.data?.challenge?.text
134 });
135
136 // Auth account
137 const auth = await authenticate({
138 variables: { request: { id: challenge.data.challenge.id, signature } }
139 });
140
141 if (auth.data?.authenticate.__typename === "AuthenticationTokens") {
142 const accessToken = auth.data?.authenticate.accessToken;
143 const metadataUri = await uploadMetadata(
144 accountMetadata({ name: username })
145 );
146
147 setOnboardingToken(accessToken);
148 return await createAccountWithUsername({
149 context: { headers: { "X-Access-Token": accessToken } },
150 variables: {
151 request: {
152 metadataUri,
153 username: { localName: username.toLowerCase() }
154 }
155 }
156 });
157 }
158
159 return onError({ message: ERRORS.SomethingWentWrong });
160 } catch {
161 onError();
162 } finally {
163 setIsSubmitting(false);
164 }
165 };
166
167 const disabled = !canCheck || !isAvailable || isSubmitting || isInvalid;
168
169 return (
170 <div className="space-y-5">
171 <SignupMessage />
172 <Form
173 className="space-y-5 pt-3"
174 form={form}
175 onSubmit={async ({ username }) =>
176 await handleSignup({ username: username.toLowerCase() })
177 }
178 >
179 <div className="mb-5">
180 <Input
181 hideError
182 placeholder="username"
183 prefix="@lens/"
184 {...form.register("username")}
185 />
186 {canCheck && !isInvalid ? (
187 isAvailable === false ? (
188 <div className="mt-2 flex items-center space-x-1 text-red-500 text-sm">
189 <FaceFrownIcon className="size-4" />
190 <b>Username not available!</b>
191 </div>
192 ) : isAvailable === true ? (
193 <div className="mt-2 flex items-center space-x-1 text-green-500 text-sm">
194 <CheckIcon className="size-4" />
195 <b>You're in luck - it's available!</b>
196 </div>
197 ) : null
198 ) : canCheck && isInvalid ? (
199 <div className="mt-2 flex items-center space-x-1 text-red-500 text-sm">
200 <ExclamationTriangleIcon className="size-4" />
201 <b>{form.formState.errors.username?.message?.toString()}</b>
202 </div>
203 ) : (
204 <div className="mt-2 flex items-center space-x-1 text-gray-500 text-sm dark:text-gray-200">
205 <FaceSmileIcon className="size-4" />
206 <b>Hope you get a good one!</b>
207 </div>
208 )}
209 </div>
210 <div className="flex items-center space-x-3">
211 <Button
212 className="w-full"
213 disabled={disabled}
214 loading={isSubmitting}
215 type="submit"
216 >
217 Signup
218 </Button>
219 </div>
220 </Form>
221 </div>
222 );
223};
224
225export default ChooseUsername;