Hey is a decentralized and permissionless social media app built with Lens Protocol 馃尶
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;