Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
at main 228 lines 7.5 kB view raw
1import { KeyIcon } from "@heroicons/react/24/outline"; 2import { HEY_APP, IS_MAINNET } from "@hey/data/constants"; 3import { ERRORS } from "@hey/data/errors"; 4import { 5 type ChallengeRequest, 6 ManagedAccountsVisibility, 7 useAccountsAvailableQuery, 8 useAuthenticateMutation, 9 useChallengeMutation 10} from "@hey/indexer"; 11import { AnimatePresence, motion } from "motion/react"; 12import type { Dispatch, SetStateAction } from "react"; 13import { useCallback, useState } from "react"; 14import { toast } from "sonner"; 15import { useAccount, useDisconnect, useSignMessage } from "wagmi"; 16import SingleAccount from "@/components/Shared/Account/SingleAccount"; 17import Loader from "@/components/Shared/Loader"; 18import { Button, Card, ErrorMessage } from "@/components/Shared/UI"; 19import errorToast from "@/helpers/errorToast"; 20import reloadAllTabs from "@/helpers/reloadAllTabs"; 21import { signIn } from "@/store/persisted/useAuthStore"; 22import { EXPANSION_EASE } from "@/variants"; 23import SignupCard from "./SignupCard"; 24import WalletSelector from "./WalletSelector"; 25 26interface LoginProps { 27 setHasAccounts: Dispatch<SetStateAction<boolean>>; 28} 29 30const Login = ({ setHasAccounts }: LoginProps) => { 31 const [isSubmitting, setIsSubmitting] = useState(false); 32 const [loggingInAccountId, setLoggingInAccountId] = useState<null | string>( 33 null 34 ); 35 const [isExpanded, setIsExpanded] = useState(true); 36 37 const onError = useCallback((error?: any) => { 38 setIsSubmitting(false); 39 setLoggingInAccountId(null); 40 errorToast(error); 41 }, []); 42 43 const { disconnect } = useDisconnect(); 44 const { address, connector: activeConnector } = useAccount(); 45 const { signMessageAsync } = useSignMessage({ 46 mutation: { onError } 47 }); 48 const [loadChallenge, { error: errorChallenge }] = useChallengeMutation({ 49 onError 50 }); 51 const [authenticate, { error: errorAuthenticate }] = useAuthenticateMutation({ 52 onError 53 }); 54 55 const { data, loading } = useAccountsAvailableQuery({ 56 onCompleted: (data) => { 57 setHasAccounts(data?.accountsAvailable.items.length > 0); 58 setIsExpanded(true); 59 }, 60 skip: !address, 61 variables: { 62 accountsAvailableRequest: { 63 hiddenFilter: ManagedAccountsVisibility.NoneHidden, 64 managedBy: address 65 }, 66 lastLoggedInAccountRequest: { address } 67 } 68 }); 69 70 const allAccounts = data?.accountsAvailable.items || []; 71 const lastLogin = data?.lastLoggedInAccount; 72 73 const remainingAccounts = lastLogin 74 ? allAccounts 75 .filter(({ account }) => account.address !== lastLogin.address) 76 .map(({ account }) => account) 77 : allAccounts.map(({ account }) => account); 78 79 const accounts = lastLogin 80 ? [lastLogin, ...remainingAccounts] 81 : remainingAccounts; 82 83 const handleSign = async (account: string) => { 84 const isManager = allAccounts.some( 85 ({ account: a, __typename }) => 86 __typename === "AccountManaged" && a.address === account 87 ); 88 89 const meta = { account, app: IS_MAINNET ? HEY_APP : undefined }; 90 const request: ChallengeRequest = isManager 91 ? { accountManager: { manager: address, ...meta } } 92 : { accountOwner: { owner: address, ...meta } }; 93 94 try { 95 setLoggingInAccountId(account || null); 96 setIsSubmitting(true); 97 // Get challenge 98 const challenge = await loadChallenge({ 99 variables: { request } 100 }); 101 102 if (!challenge?.data?.challenge?.text) { 103 return toast.error(ERRORS.SomethingWentWrong); 104 } 105 106 // Get signature 107 const signature = await signMessageAsync({ 108 message: challenge?.data?.challenge?.text 109 }); 110 111 // Auth account 112 const auth = await authenticate({ 113 variables: { request: { id: challenge.data.challenge.id, signature } } 114 }); 115 116 if (auth.data?.authenticate.__typename === "AuthenticationTokens") { 117 const accessToken = auth.data?.authenticate.accessToken; 118 const refreshToken = auth.data?.authenticate.refreshToken; 119 signIn({ accessToken, refreshToken }); 120 reloadAllTabs(); 121 return; 122 } 123 124 return onError({ message: ERRORS.SomethingWentWrong }); 125 } catch { 126 onError(); 127 } 128 }; 129 130 return activeConnector?.id ? ( 131 <div className="space-y-3"> 132 <div className="space-y-2.5"> 133 {errorChallenge || errorAuthenticate ? ( 134 <ErrorMessage 135 className="text-red-500" 136 error={errorChallenge || errorAuthenticate} 137 title={ERRORS.SomethingWentWrong} 138 /> 139 ) : null} 140 {loading ? ( 141 <Card className="w-full dark:divide-gray-700" forceRounded> 142 <Loader 143 className="my-4" 144 message="Loading accounts managed by you..." 145 small 146 /> 147 </Card> 148 ) : accounts.length > 0 ? ( 149 <AnimatePresence mode="popLayout"> 150 {isExpanded && ( 151 <motion.div 152 animate="visible" 153 initial="hidden" 154 variants={{ 155 hidden: { height: 0, opacity: 0, overflow: "hidden" }, 156 visible: { 157 height: "auto", 158 opacity: 1, 159 transition: { duration: 0.2, ease: EXPANSION_EASE } 160 } 161 }} 162 > 163 <Card 164 className="max-h-[50vh] w-full overflow-y-auto dark:divide-gray-700" 165 forceRounded 166 > 167 {accounts.map((account, index) => ( 168 <motion.div 169 className="flex items-center justify-between p-3" 170 custom={index} 171 key={account.address} 172 variants={{ 173 hidden: { opacity: 0, y: 20 }, 174 visible: { 175 opacity: 1, 176 transition: { duration: 0.1 }, 177 y: 0 178 } 179 }} 180 whileHover={{ 181 backgroundColor: "rgba(0, 0, 0, 0.05)", 182 transition: { duration: 0.2 } 183 }} 184 > 185 <SingleAccount 186 account={account} 187 hideFollowButton 188 hideUnfollowButton 189 linkToAccount={false} 190 showUserPreview={false} 191 /> 192 <Button 193 disabled={ 194 isSubmitting && loggingInAccountId === account.address 195 } 196 loading={ 197 isSubmitting && loggingInAccountId === account.address 198 } 199 onClick={() => handleSign(account.address)} 200 outline 201 > 202 Login 203 </Button> 204 </motion.div> 205 ))} 206 </Card> 207 </motion.div> 208 )} 209 </AnimatePresence> 210 ) : ( 211 <SignupCard /> 212 )} 213 <button 214 className="flex items-center space-x-1 text-sm underline" 215 onClick={() => disconnect?.()} 216 type="reset" 217 > 218 <KeyIcon className="size-4" /> 219 <div>Change wallet</div> 220 </button> 221 </div> 222 </div> 223 ) : ( 224 <WalletSelector /> 225 ); 226}; 227 228export default Login;