mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useRef, useState} from 'react'
2import {
3 ActivityIndicator,
4 Keyboard,
5 LayoutAnimation,
6 type TextInput,
7 View,
8} from 'react-native'
9import {
10 ComAtprotoServerCreateSession,
11 type ComAtprotoServerDescribeServer,
12} from '@atproto/api'
13import {msg, Trans} from '@lingui/macro'
14import {useLingui} from '@lingui/react'
15
16import {useRequestNotificationsPermission} from '#/lib/notifications/notifications'
17import {isNetworkError} from '#/lib/strings/errors'
18import {cleanError} from '#/lib/strings/errors'
19import {createFullHandle} from '#/lib/strings/handles'
20import {logger} from '#/logger'
21import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-packs'
22import {useSessionApi} from '#/state/session'
23import {useLoggedOutViewControls} from '#/state/shell/logged-out'
24import {atoms as a, useTheme} from '#/alf'
25import {Button, ButtonIcon, ButtonText} from '#/components/Button'
26import {FormError} from '#/components/forms/FormError'
27import {HostingProvider} from '#/components/forms/HostingProvider'
28import * as TextField from '#/components/forms/TextField'
29import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
30import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
31import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
32import {Loader} from '#/components/Loader'
33import {Text} from '#/components/Typography'
34import {FormContainer} from './FormContainer'
35
36type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
37
38export const LoginForm = ({
39 error,
40 serviceUrl,
41 serviceDescription,
42 initialHandle,
43 setError,
44 setServiceUrl,
45 onPressRetryConnect,
46 onPressBack,
47 onPressForgotPassword,
48 onAttemptSuccess,
49 onAttemptFailed,
50}: {
51 error: string
52 serviceUrl: string
53 serviceDescription: ServiceDescription | undefined
54 initialHandle: string
55 setError: (v: string) => void
56 setServiceUrl: (v: string) => void
57 onPressRetryConnect: () => void
58 onPressBack: () => void
59 onPressForgotPassword: () => void
60 onAttemptSuccess: () => void
61 onAttemptFailed: () => void
62}) => {
63 const t = useTheme()
64 const [isProcessing, setIsProcessing] = useState<boolean>(false)
65 const [isAuthFactorTokenNeeded, setIsAuthFactorTokenNeeded] =
66 useState<boolean>(false)
67 const [isAuthFactorTokenValueEmpty, setIsAuthFactorTokenValueEmpty] =
68 useState<boolean>(true)
69 const identifierValueRef = useRef<string>(initialHandle || '')
70 const passwordValueRef = useRef<string>('')
71 const authFactorTokenValueRef = useRef<string>('')
72 const passwordRef = useRef<TextInput>(null)
73 const {_} = useLingui()
74 const {login} = useSessionApi()
75 const requestNotificationsPermission = useRequestNotificationsPermission()
76 const {setShowLoggedOut} = useLoggedOutViewControls()
77 const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
78
79 const onPressSelectService = React.useCallback(() => {
80 Keyboard.dismiss()
81 }, [])
82
83 const onPressNext = async () => {
84 if (isProcessing) return
85 Keyboard.dismiss()
86 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
87 setError('')
88
89 const identifier = identifierValueRef.current.toLowerCase().trim()
90 const password = passwordValueRef.current
91 const authFactorToken = authFactorTokenValueRef.current
92
93 if (!identifier) {
94 setError(_(msg`Please enter your username`))
95 return
96 }
97
98 if (!password) {
99 setError(_(msg`Please enter your password`))
100 return
101 }
102
103 setIsProcessing(true)
104
105 try {
106 // try to guess the handle if the user just gave their own username
107 let fullIdent = identifier
108 if (
109 !identifier.includes('@') && // not an email
110 !identifier.includes('.') && // not a domain
111 serviceDescription &&
112 serviceDescription.availableUserDomains.length > 0
113 ) {
114 let matched = false
115 for (const domain of serviceDescription.availableUserDomains) {
116 if (fullIdent.endsWith(domain)) {
117 matched = true
118 }
119 }
120 if (!matched) {
121 fullIdent = createFullHandle(
122 identifier,
123 serviceDescription.availableUserDomains[0],
124 )
125 }
126 }
127
128 // TODO remove double login
129 await login(
130 {
131 service: serviceUrl,
132 identifier: fullIdent,
133 password,
134 authFactorToken: authFactorToken.trim(),
135 },
136 'LoginForm',
137 )
138 onAttemptSuccess()
139 setShowLoggedOut(false)
140 setHasCheckedForStarterPack(true)
141 requestNotificationsPermission('Login')
142 } catch (e: any) {
143 const errMsg = e.toString()
144 LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
145 setIsProcessing(false)
146 if (
147 e instanceof ComAtprotoServerCreateSession.AuthFactorTokenRequiredError
148 ) {
149 setIsAuthFactorTokenNeeded(true)
150 } else {
151 onAttemptFailed()
152 if (errMsg.includes('Token is invalid')) {
153 logger.debug('Failed to login due to invalid 2fa token', {
154 error: errMsg,
155 })
156 setError(_(msg`Invalid 2FA confirmation code.`))
157 } else if (
158 errMsg.includes('Authentication Required') ||
159 errMsg.includes('Invalid identifier or password')
160 ) {
161 logger.debug('Failed to login due to invalid credentials', {
162 error: errMsg,
163 })
164 setError(_(msg`Incorrect username or password`))
165 } else if (isNetworkError(e)) {
166 logger.warn('Failed to login due to network error', {error: errMsg})
167 setError(
168 _(
169 msg`Unable to contact your service. Please check your Internet connection.`,
170 ),
171 )
172 } else {
173 logger.warn('Failed to login', {error: errMsg})
174 setError(cleanError(errMsg))
175 }
176 }
177 }
178 }
179
180 return (
181 <FormContainer testID="loginForm" titleText={<Trans>Sign in</Trans>}>
182 <View>
183 <TextField.LabelText>
184 <Trans>Hosting provider</Trans>
185 </TextField.LabelText>
186 <HostingProvider
187 serviceUrl={serviceUrl}
188 onSelectServiceUrl={setServiceUrl}
189 onOpenDialog={onPressSelectService}
190 />
191 </View>
192 <View>
193 <TextField.LabelText>
194 <Trans>Account</Trans>
195 </TextField.LabelText>
196 <View style={[a.gap_sm]}>
197 <TextField.Root>
198 <TextField.Icon icon={At} />
199 <TextField.Input
200 testID="loginUsernameInput"
201 label={_(msg`Username or email address`)}
202 autoCapitalize="none"
203 autoFocus
204 autoCorrect={false}
205 autoComplete="username"
206 returnKeyType="next"
207 textContentType="username"
208 defaultValue={initialHandle || ''}
209 onChangeText={v => {
210 identifierValueRef.current = v
211 }}
212 onSubmitEditing={() => {
213 passwordRef.current?.focus()
214 }}
215 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
216 editable={!isProcessing}
217 accessibilityHint={_(
218 msg`Enter the username or email address you used when you created your account`,
219 )}
220 />
221 </TextField.Root>
222
223 <TextField.Root>
224 <TextField.Icon icon={Lock} />
225 <TextField.Input
226 testID="loginPasswordInput"
227 inputRef={passwordRef}
228 label={_(msg`Password`)}
229 autoCapitalize="none"
230 autoCorrect={false}
231 autoComplete="password"
232 returnKeyType="done"
233 enablesReturnKeyAutomatically={true}
234 secureTextEntry={true}
235 textContentType="password"
236 clearButtonMode="while-editing"
237 onChangeText={v => {
238 passwordValueRef.current = v
239 }}
240 onSubmitEditing={onPressNext}
241 blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
242 editable={!isProcessing}
243 accessibilityHint={_(msg`Enter your password`)}
244 />
245 <Button
246 testID="forgotPasswordButton"
247 onPress={onPressForgotPassword}
248 label={_(msg`Forgot password?`)}
249 accessibilityHint={_(msg`Opens password reset form`)}
250 variant="solid"
251 color="secondary"
252 style={[
253 a.rounded_sm,
254 // t.atoms.bg_contrast_100,
255 {marginLeft: 'auto', left: 6, padding: 6},
256 a.z_10,
257 ]}>
258 <ButtonText>
259 <Trans>Forgot?</Trans>
260 </ButtonText>
261 </Button>
262 </TextField.Root>
263 </View>
264 </View>
265 {isAuthFactorTokenNeeded && (
266 <View>
267 <TextField.LabelText>
268 <Trans>2FA Confirmation</Trans>
269 </TextField.LabelText>
270 <TextField.Root>
271 <TextField.Icon icon={Ticket} />
272 <TextField.Input
273 testID="loginAuthFactorTokenInput"
274 label={_(msg`Confirmation code`)}
275 autoCapitalize="none"
276 autoFocus
277 autoCorrect={false}
278 autoComplete="one-time-code"
279 returnKeyType="done"
280 textContentType="username"
281 blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
282 onChangeText={v => {
283 setIsAuthFactorTokenValueEmpty(v === '')
284 authFactorTokenValueRef.current = v
285 }}
286 onSubmitEditing={onPressNext}
287 editable={!isProcessing}
288 accessibilityHint={_(
289 msg`Input the code which has been emailed to you`,
290 )}
291 style={[
292 {
293 textTransform: isAuthFactorTokenValueEmpty
294 ? 'none'
295 : 'uppercase',
296 },
297 ]}
298 />
299 </TextField.Root>
300 <Text style={[a.text_sm, t.atoms.text_contrast_medium, a.mt_sm]}>
301 <Trans>
302 Check your email for a sign in code and enter it here.
303 </Trans>
304 </Text>
305 </View>
306 )}
307 <FormError error={error} />
308 <View style={[a.flex_row, a.align_center, a.pt_md]}>
309 <Button
310 label={_(msg`Back`)}
311 variant="solid"
312 color="secondary"
313 size="large"
314 onPress={onPressBack}>
315 <ButtonText>
316 <Trans>Back</Trans>
317 </ButtonText>
318 </Button>
319 <View style={a.flex_1} />
320 {!serviceDescription && error ? (
321 <Button
322 testID="loginRetryButton"
323 label={_(msg`Retry`)}
324 accessibilityHint={_(msg`Retries signing in`)}
325 variant="solid"
326 color="secondary"
327 size="large"
328 onPress={onPressRetryConnect}>
329 <ButtonText>
330 <Trans>Retry</Trans>
331 </ButtonText>
332 </Button>
333 ) : !serviceDescription ? (
334 <>
335 <ActivityIndicator />
336 <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
337 <Trans>Connecting...</Trans>
338 </Text>
339 </>
340 ) : (
341 <Button
342 testID="loginNextButton"
343 label={_(msg`Next`)}
344 accessibilityHint={_(msg`Navigates to the next screen`)}
345 variant="solid"
346 color="primary"
347 size="large"
348 onPress={onPressNext}>
349 <ButtonText>
350 <Trans>Next</Trans>
351 </ButtonText>
352 {isProcessing && <ButtonIcon icon={Loader} />}
353 </Button>
354 )}
355 </View>
356 </FormContainer>
357 )
358}