Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
1import {
2 ExclamationTriangleIcon,
3 MagnifyingGlassIcon
4} from "@heroicons/react/24/outline";
5import {
6 HEY_ENS_NAMESPACE,
7 NATIVE_TOKEN_SYMBOL,
8 STATIC_IMAGES_URL
9} from "@hey/data/constants";
10import {
11 useBalancesBulkQuery,
12 useCreateUsernameMutation,
13 useUsernameQuery
14} from "@hey/indexer";
15import { useCallback, useState } from "react";
16import z from "zod";
17import NotLoggedIn from "@/components/Shared/NotLoggedIn";
18import errorToast from "@/helpers/errorToast";
19import getTokenImage from "@/helpers/getTokenImage";
20import useHandleWrongNetwork from "@/hooks/useHandleWrongNetwork";
21import useTransactionLifecycle from "@/hooks/useTransactionLifecycle";
22import { useAccountStore } from "@/store/persisted/useAccountStore";
23import TopUpButton from "../Shared/Account/TopUp/Button";
24import {
25 Button,
26 Card,
27 Form,
28 Image,
29 Input,
30 Spinner,
31 Tooltip,
32 useZodForm
33} from "../Shared/UI";
34import { useENSCreateStore } from ".";
35import Usernames from "./Usernames";
36
37const ValidationSchema = z.object({
38 username: z
39 .string()
40 .min(1, { message: "ENS name must be at least 1 character long" })
41 .max(50, { message: "ENS name must be at most 50 characters long" })
42 .regex(/^[A-Za-z]+$/, { message: "ENS name can contain only alphabets" })
43});
44
45const Choose = () => {
46 const { currentAccount } = useAccountStore();
47 const { setChosenUsername, setTransactionHash, setScreen } =
48 useENSCreateStore();
49 const [isSubmitting, setIsSubmitting] = useState(false);
50 const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
51 const handleWrongNetwork = useHandleWrongNetwork();
52 const handleTransactionLifecycle = useTransactionLifecycle();
53 const form = useZodForm({ mode: "onChange", schema: ValidationSchema });
54
55 const { data: balance, loading: balanceLoading } = useBalancesBulkQuery({
56 fetchPolicy: "no-cache",
57 pollInterval: 3000,
58 skip: !currentAccount?.address,
59 variables: {
60 request: {
61 address: currentAccount?.address,
62 includeNative: true
63 }
64 }
65 });
66
67 const onCompleted = (hash: string) => {
68 setIsSubmitting(false);
69 setChosenUsername(username);
70 setTransactionHash(hash);
71 setScreen("minting");
72 };
73
74 const onError = useCallback((error?: any) => {
75 setIsSubmitting(false);
76 errorToast(error);
77 }, []);
78
79 const [createUsername] = useCreateUsernameMutation({
80 onCompleted: async ({ createUsername }) => {
81 if (createUsername.__typename === "CreateUsernameResponse") {
82 return onCompleted(createUsername.hash);
83 }
84
85 if (createUsername.__typename === "UsernameTaken") {
86 return onError({ message: createUsername.reason });
87 }
88
89 return await handleTransactionLifecycle({
90 onCompleted,
91 onError,
92 transactionData: createUsername
93 });
94 },
95 onError
96 });
97
98 const username = form.watch("username");
99 const canCheck = Boolean(username && username.length > 0);
100 const isInvalid = !form.formState.isValid;
101 const lengthPriceMap: Record<number, number> = {
102 1: 1000,
103 2: 500,
104 3: 50,
105 4: 20
106 };
107
108 const len = username?.length || 0;
109 const price = len > 4 ? 5 : (lengthPriceMap[len] ?? 0);
110
111 const tokenBalance =
112 balance?.balancesBulk[0].__typename === "NativeAmount"
113 ? Number(balance.balancesBulk[0].value).toFixed(2)
114 : 0;
115
116 const canMint = Number(tokenBalance) >= price;
117
118 useUsernameQuery({
119 fetchPolicy: "no-cache",
120 onCompleted: (data) => setIsAvailable(!data.username),
121 skip: !canCheck,
122 variables: {
123 request: {
124 username: {
125 localName: username?.toLowerCase(),
126 namespace: HEY_ENS_NAMESPACE
127 }
128 }
129 }
130 });
131
132 const handleCreate = async ({
133 username
134 }: z.infer<typeof ValidationSchema>) => {
135 setIsSubmitting(true);
136 await handleWrongNetwork();
137
138 return await createUsername({
139 variables: {
140 request: {
141 autoAssign: true,
142 username: {
143 localName: username.toLowerCase(),
144 namespace: HEY_ENS_NAMESPACE
145 }
146 }
147 }
148 });
149 };
150
151 if (!currentAccount) {
152 return <NotLoggedIn />;
153 }
154
155 return (
156 <Card className="p-5">
157 <div className="flex items-center justify-between">
158 <div className="flex items-center gap-2">
159 <Image
160 alt="Logo"
161 className="size-4"
162 height={16}
163 src={`${STATIC_IMAGES_URL}/app-icon/0.png`}
164 width={16}
165 />
166 <div className="font-black">Heynames</div>
167 </div>
168 <div className="text-gray-500 text-sm">Powered by ENS</div>
169 </div>
170 <Form
171 className="space-y-5 pt-2"
172 form={form}
173 onSubmit={async ({ username }) =>
174 await handleCreate({ username: username.toLowerCase() })
175 }
176 >
177 <Input
178 iconLeft={<MagnifyingGlassIcon />}
179 iconRight={<span>hey.xyz</span>}
180 placeholder="Search for a name"
181 {...form.register("username")}
182 hideError
183 />
184 {canCheck && !isInvalid ? (
185 isAvailable === false ? (
186 <Card className="p-5">
187 <b>{username}.hey.xyz</b> is already taken.
188 </Card>
189 ) : isAvailable === true ? (
190 <Card className="space-y-5 p-5">
191 <div>
192 Register <b>{username}.hey.xyz</b> for{" "}
193 <span className="inline-flex items-center gap-x-1">
194 {price}{" "}
195 <Tooltip content={NATIVE_TOKEN_SYMBOL} placement="top">
196 <img
197 alt={NATIVE_TOKEN_SYMBOL}
198 className="size-5"
199 src={getTokenImage(NATIVE_TOKEN_SYMBOL)}
200 />
201 </Tooltip>
202 / once
203 </span>
204 </div>
205 {balanceLoading ? (
206 <Button
207 className="w-full"
208 disabled
209 icon={<Spinner className="my-1" size="xs" />}
210 />
211 ) : canMint ? (
212 <Button
213 className="w-full"
214 disabled={isSubmitting}
215 loading={isSubmitting}
216 type="submit"
217 >
218 Subscribe for ${price}/year
219 </Button>
220 ) : (
221 <TopUpButton
222 amountToTopUp={
223 Math.ceil((price - Number(tokenBalance)) * 20) / 20
224 }
225 className="w-full"
226 label={`Top-up ${price} ${NATIVE_TOKEN_SYMBOL} to your account`}
227 outline
228 />
229 )}
230 </Card>
231 ) : null
232 ) : canCheck && isInvalid ? (
233 <Card className="flex items-center space-x-1 p-5 text-red-500 text-sm">
234 <ExclamationTriangleIcon className="size-4" />
235 <b>{form.formState.errors.username?.message?.toString()}</b>
236 </Card>
237 ) : null}
238 </Form>
239 <Usernames />
240 </Card>
241 );
242};
243
244export default Choose;