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