This repository has no description
1'use client';
2
3import { ExtensionService } from '@/services/extensionService';
4import {
5 Stack,
6 Text,
7 Button,
8 UnstyledButton,
9 TextInput,
10 PasswordInput,
11 Alert,
12} from '@mantine/core';
13import { BiRightArrowAlt } from 'react-icons/bi';
14import { MdOutlineAlternateEmail, MdLock } from 'react-icons/md';
15import { useAuth } from '@/hooks/useAuth';
16import { useForm } from '@mantine/form';
17import { useRouter, useSearchParams } from 'next/navigation';
18import { useEffect, useState } from 'react';
19import { createSembleClient } from '@/services/apiClient';
20
21export default function LoginForm() {
22 const router = useRouter();
23 const searchParams = useSearchParams();
24 const { isAuthenticated, refreshAuth } = useAuth();
25
26 const [isCheckingAuth, setIsCheckingAuth] = useState(true);
27 const [isLoading, setIsLoading] = useState(false);
28 const [error, setError] = useState('');
29
30 const isExtensionLogin = searchParams.get('extension-login') === 'true';
31 const client = createSembleClient();
32
33 const handleExtensionTokenGeneration = async () => {
34 try {
35 setIsLoading(true);
36 const tokens = await client.generateExtensionTokens();
37
38 await ExtensionService.sendTokensToExtension(tokens);
39
40 setError('');
41
42 // Clear the extension tokens requested flag
43 ExtensionService.clearExtensionTokensRequested();
44
45 // Redirect to extension success page after successful extension token generation
46 router.push('/extension/auth/complete');
47 } catch (err: any) {
48 // Clear the flag even on failure
49 ExtensionService.clearExtensionTokensRequested();
50
51 // Redirect to extension error page
52 router.push('/extension/auth/error');
53 } finally {
54 setIsLoading(false);
55 }
56 };
57
58 useEffect(() => {
59 if (isAuthenticated) {
60 isExtensionLogin
61 ? handleExtensionTokenGeneration()
62 : router.push('/home');
63 } else {
64 setIsCheckingAuth(false);
65 }
66 }, [isAuthenticated, isExtensionLogin]);
67
68 const handleOAuthSubmit = async (e: React.FormEvent) => {
69 e.preventDefault();
70
71 // validate form
72 const isValid = form.validateField('handle');
73 if (!isValid) return;
74
75 try {
76 setIsLoading(true);
77 setError('');
78
79 if (isExtensionLogin) {
80 ExtensionService.setExtensionTokensRequested();
81 }
82 console.log('HANDLE', form.values.handle.trimEnd());
83 const { authUrl } = await client.initiateOAuthSignIn({
84 handle: form.values.handle.trimEnd(),
85 });
86
87 window.location.href = authUrl;
88 } catch (err: any) {
89 setError(err.message || 'An error occurred during login');
90 } finally {
91 setIsLoading(false);
92 }
93 };
94
95 const handleAppPasswordSubmit = async (e: React.FormEvent) => {
96 e.preventDefault();
97
98 // validate
99 const validation = form.validate();
100 if (validation.hasErrors) return;
101
102 try {
103 setIsLoading(true);
104 setError('');
105
106 await client.loginWithAppPassword({
107 identifier: form.values.handle.trimEnd(),
108 appPassword: form.values.appPassword,
109 });
110
111 // Refresh auth state to fetch user profile with new tokens (cookies are set automatically)
112 await refreshAuth();
113
114 if (isExtensionLogin) {
115 await handleExtensionTokenGeneration();
116 } else {
117 router.push('/home');
118 }
119 } catch (err: any) {
120 setError(err.message || 'Invalid credentials');
121 } finally {
122 setIsLoading(false);
123 }
124 };
125
126 const form = useForm({
127 initialValues: {
128 handle: '',
129 appPassword: '',
130 useAppPassword: false,
131 },
132
133 validate: {
134 handle: (value) => (value.trim() ? null : 'Handle is required'),
135 appPassword: (value, values) =>
136 values.useAppPassword && value.trim() === ''
137 ? 'App password is required'
138 : null,
139 },
140 });
141
142 if (form.values.useAppPassword) {
143 return (
144 <Stack gap={'lg'}>
145 <form onSubmit={handleAppPasswordSubmit}>
146 <Stack align="center">
147 <TextInput
148 autoComplete="username"
149 name="username"
150 label="Handle"
151 placeholder="you.bsky.social"
152 key={form.key('handle')}
153 value={form.values.handle}
154 onChange={(event) => {
155 form.setFieldValue('handle', event.currentTarget.value);
156 if (error) setError('');
157 }}
158 leftSection={<MdOutlineAlternateEmail size={22} />}
159 variant="filled"
160 size="lg"
161 w={'100%'}
162 required
163 />
164 <PasswordInput
165 autoComplete="password"
166 name="password"
167 label="App password"
168 placeholder="Your password"
169 key={form.key('appPassword')}
170 {...form.getInputProps('appPassword')}
171 leftSection={<MdLock size={22} />}
172 variant="filled"
173 size="lg"
174 w={'100%'}
175 required
176 />
177 <Button
178 type="submit"
179 size="lg"
180 color="dark"
181 fullWidth
182 rightSection={<BiRightArrowAlt size={22} />}
183 loading={isLoading}
184 >
185 Log in
186 </Button>
187 {error && <Alert title={error} color="red" />}
188 </Stack>
189 </form>
190 <Stack align="center">
191 <UnstyledButton
192 fw={500}
193 onClick={() => {
194 form.setFieldValue('useAppPassword', false);
195 setError('');
196 }}
197 >
198 Use OAuth login
199 </UnstyledButton>
200 </Stack>
201 </Stack>
202 );
203 }
204
205 return (
206 <Stack gap={'xl'}>
207 <form onSubmit={handleOAuthSubmit}>
208 <Stack align="center">
209 <TextInput
210 autoComplete="username"
211 name="username"
212 label="Handle"
213 placeholder="you.bsky.social"
214 key={form.key('handle')}
215 value={form.values.handle}
216 onChange={(event) => {
217 form.setFieldValue('handle', event.currentTarget.value);
218 if (error) setError('');
219 }}
220 leftSection={<MdOutlineAlternateEmail size={22} />}
221 variant="filled"
222 size="lg"
223 w={'100%'}
224 required
225 />
226 <Button
227 type="submit"
228 size="lg"
229 color="dark"
230 fullWidth
231 rightSection={<BiRightArrowAlt size={22} />}
232 loading={isLoading}
233 >
234 Log in
235 </Button>
236 {error && <Alert title={error} color="red" />}
237 <Text fw={500} c={'stone'}>
238 Or
239 </Text>
240 <UnstyledButton
241 fw={500}
242 onClick={() => {
243 form.setFieldValue('useAppPassword', true);
244 setError('');
245 }}
246 >
247 Use your app password
248 </UnstyledButton>
249 </Stack>
250 </form>
251 </Stack>
252 );
253}