mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {useState} from 'react'
2import {
3 ActivityIndicator,
4 SafeAreaView,
5 StyleSheet,
6 TouchableOpacity,
7 View,
8} from 'react-native'
9import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
10import {msg, Trans} from '@lingui/macro'
11import {useLingui} from '@lingui/react'
12import * as EmailValidator from 'email-validator'
13
14import {logger} from '#/logger'
15import {useModalControls} from '#/state/modals'
16import {useAgent, useSession} from '#/state/session'
17import {usePalette} from 'lib/hooks/usePalette'
18import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
19import {cleanError, isNetworkError} from 'lib/strings/errors'
20import {checkAndFormatResetCode} from 'lib/strings/password'
21import {colors, s} from 'lib/styles'
22import {isAndroid, isWeb} from 'platform/detection'
23import {ErrorMessage} from '../util/error/ErrorMessage'
24import {Button} from '../util/forms/Button'
25import {Text} from '../util/text/Text'
26import {ScrollView} from './util'
27import {TextInput} from './util'
28
29enum Stages {
30 RequestCode,
31 ChangePassword,
32 Done,
33}
34
35export const snapPoints = isAndroid ? ['90%'] : ['45%']
36
37export function Component() {
38 const pal = usePalette('default')
39 const {currentAccount} = useSession()
40 const agent = useAgent()
41 const {_} = useLingui()
42 const [stage, setStage] = useState<Stages>(Stages.RequestCode)
43 const [isProcessing, setIsProcessing] = useState<boolean>(false)
44 const [resetCode, setResetCode] = useState<string>('')
45 const [newPassword, setNewPassword] = useState<string>('')
46 const [error, setError] = useState<string>('')
47 const {isMobile} = useWebMediaQueries()
48 const {closeModal} = useModalControls()
49
50 const onRequestCode = async () => {
51 if (
52 !currentAccount?.email ||
53 !EmailValidator.validate(currentAccount.email)
54 ) {
55 return setError(_(msg`Your email appears to be invalid.`))
56 }
57
58 setError('')
59 setIsProcessing(true)
60 try {
61 await agent.com.atproto.server.requestPasswordReset({
62 email: currentAccount.email,
63 })
64 setStage(Stages.ChangePassword)
65 } catch (e: any) {
66 const errMsg = e.toString()
67 logger.warn('Failed to request password reset', {error: e})
68 if (isNetworkError(e)) {
69 setError(
70 _(
71 msg`Unable to contact your service. Please check your Internet connection.`,
72 ),
73 )
74 } else {
75 setError(cleanError(errMsg))
76 }
77 } finally {
78 setIsProcessing(false)
79 }
80 }
81
82 const onChangePassword = async () => {
83 const formattedCode = checkAndFormatResetCode(resetCode)
84 // TODO Better password strength check
85 if (!formattedCode || !newPassword) {
86 setError(
87 _(
88 msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
89 ),
90 )
91 return
92 }
93
94 setError('')
95 setIsProcessing(true)
96 try {
97 await agent.com.atproto.server.resetPassword({
98 token: formattedCode,
99 password: newPassword,
100 })
101 setStage(Stages.Done)
102 } catch (e: any) {
103 const errMsg = e.toString()
104 logger.warn('Failed to set new password', {error: e})
105 if (isNetworkError(e)) {
106 setError(
107 'Unable to contact your service. Please check your Internet connection.',
108 )
109 } else {
110 setError(cleanError(errMsg))
111 }
112 } finally {
113 setIsProcessing(false)
114 }
115 }
116
117 const onBlur = () => {
118 const formattedCode = checkAndFormatResetCode(resetCode)
119 if (!formattedCode) {
120 setError(
121 _(
122 msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
123 ),
124 )
125 return
126 }
127 setResetCode(formattedCode)
128 }
129
130 return (
131 <SafeAreaView style={[pal.view, s.flex1]}>
132 <ScrollView
133 contentContainerStyle={[
134 styles.container,
135 isMobile && styles.containerMobile,
136 ]}
137 keyboardShouldPersistTaps="handled">
138 <View>
139 <View style={styles.titleSection}>
140 <Text type="title-lg" style={[pal.text, styles.title]}>
141 {stage !== Stages.Done
142 ? _(msg`Change Password`)
143 : _(msg`Password Changed`)}
144 </Text>
145 </View>
146
147 <Text type="lg" style={[pal.textLight, {marginBottom: 10}]}>
148 {stage === Stages.RequestCode ? (
149 <Trans>
150 If you want to change your password, we will send you a code to
151 verify that this is your account.
152 </Trans>
153 ) : stage === Stages.ChangePassword ? (
154 <Trans>
155 Enter the code you received to change your password.
156 </Trans>
157 ) : (
158 <Trans>Your password has been changed successfully!</Trans>
159 )}
160 </Text>
161
162 {stage === Stages.RequestCode && (
163 <View style={[s.flexRow, s.justifyCenter, s.mt10]}>
164 <TouchableOpacity
165 testID="skipSendEmailButton"
166 onPress={() => setStage(Stages.ChangePassword)}
167 accessibilityRole="button"
168 accessibilityLabel={_(msg`Go to next`)}
169 accessibilityHint={_(msg`Navigates to the next screen`)}>
170 <Text type="xl" style={[pal.link, s.pr5]}>
171 <Trans>Already have a code?</Trans>
172 </Text>
173 </TouchableOpacity>
174 </View>
175 )}
176 {stage === Stages.ChangePassword && (
177 <View style={[pal.border, styles.group]}>
178 <View style={[styles.groupContent]}>
179 <FontAwesomeIcon
180 icon="ticket"
181 style={[pal.textLight, styles.groupContentIcon]}
182 />
183 <TextInput
184 testID="codeInput"
185 style={[pal.text, styles.textInput]}
186 placeholder={_(msg`Reset code`)}
187 placeholderTextColor={pal.colors.textLight}
188 value={resetCode}
189 onChangeText={setResetCode}
190 onFocus={() => setError('')}
191 onBlur={onBlur}
192 accessible={true}
193 accessibilityLabel={_(msg`Reset Code`)}
194 accessibilityHint=""
195 autoCapitalize="none"
196 autoCorrect={false}
197 autoComplete="off"
198 />
199 </View>
200 <View
201 style={[
202 pal.borderDark,
203 styles.groupContent,
204 styles.groupBottom,
205 ]}>
206 <FontAwesomeIcon
207 icon="lock"
208 style={[pal.textLight, styles.groupContentIcon]}
209 />
210 <TextInput
211 testID="codeInput"
212 style={[pal.text, styles.textInput]}
213 placeholder={_(msg`New password`)}
214 placeholderTextColor={pal.colors.textLight}
215 onChangeText={setNewPassword}
216 secureTextEntry
217 accessible={true}
218 accessibilityLabel={_(msg`New Password`)}
219 accessibilityHint=""
220 autoCapitalize="none"
221 autoComplete="new-password"
222 />
223 </View>
224 </View>
225 )}
226 {error ? (
227 <ErrorMessage message={error} style={styles.error} />
228 ) : undefined}
229 </View>
230 <View style={[styles.btnContainer]}>
231 {isProcessing ? (
232 <View style={styles.btn}>
233 <ActivityIndicator color="#fff" />
234 </View>
235 ) : (
236 <View style={{gap: 6}}>
237 {stage === Stages.RequestCode && (
238 <Button
239 testID="requestChangeBtn"
240 type="primary"
241 onPress={onRequestCode}
242 accessibilityLabel={_(msg`Request Code`)}
243 accessibilityHint=""
244 label={_(msg`Request Code`)}
245 labelContainerStyle={{justifyContent: 'center', padding: 4}}
246 labelStyle={[s.f18]}
247 />
248 )}
249 {stage === Stages.ChangePassword && (
250 <Button
251 testID="confirmBtn"
252 type="primary"
253 onPress={onChangePassword}
254 accessibilityLabel={_(msg`Next`)}
255 accessibilityHint=""
256 label={_(msg`Next`)}
257 labelContainerStyle={{justifyContent: 'center', padding: 4}}
258 labelStyle={[s.f18]}
259 />
260 )}
261 <Button
262 testID="cancelBtn"
263 type={stage !== Stages.Done ? 'default' : 'primary'}
264 onPress={() => {
265 closeModal()
266 }}
267 accessibilityLabel={
268 stage !== Stages.Done ? _(msg`Cancel`) : _(msg`Close`)
269 }
270 accessibilityHint=""
271 label={stage !== Stages.Done ? _(msg`Cancel`) : _(msg`Close`)}
272 labelContainerStyle={{justifyContent: 'center', padding: 4}}
273 labelStyle={[s.f18]}
274 />
275 </View>
276 )}
277 </View>
278 </ScrollView>
279 </SafeAreaView>
280 )
281}
282
283const styles = StyleSheet.create({
284 container: {
285 justifyContent: 'space-between',
286 },
287 containerMobile: {
288 paddingHorizontal: 18,
289 paddingBottom: 35,
290 },
291 titleSection: {
292 paddingTop: isWeb ? 0 : 4,
293 paddingBottom: isWeb ? 14 : 10,
294 },
295 title: {
296 textAlign: 'center',
297 fontWeight: '600',
298 marginBottom: 5,
299 },
300 error: {
301 borderRadius: 6,
302 },
303 textInput: {
304 width: '100%',
305 paddingHorizontal: 14,
306 paddingVertical: 10,
307 fontSize: 16,
308 },
309 btn: {
310 flexDirection: 'row',
311 alignItems: 'center',
312 justifyContent: 'center',
313 borderRadius: 32,
314 padding: 14,
315 backgroundColor: colors.blue3,
316 },
317 btnContainer: {
318 paddingTop: 20,
319 },
320 group: {
321 borderWidth: 1,
322 borderRadius: 10,
323 marginVertical: 20,
324 },
325 groupLabel: {
326 paddingHorizontal: 20,
327 paddingBottom: 5,
328 },
329 groupContent: {
330 flexDirection: 'row',
331 alignItems: 'center',
332 },
333 groupBottom: {
334 borderTopWidth: 1,
335 },
336 groupContentIcon: {
337 marginLeft: 10,
338 },
339})