Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 244 lines 7.1 kB view raw
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;