···11+import React, {useEffect} from 'react'
22+import {View} from 'react-native'
33+import {useAnalytics} from 'lib/analytics/analytics'
44+import {msg, Trans} from '@lingui/macro'
55+import {useLingui} from '@lingui/react'
66+import {FormContainer} from './FormContainer'
77+import {Button, ButtonText} from '#/components/Button'
88+import {Text} from '#/components/Typography'
99+import {atoms as a, useBreakpoints} from '#/alf'
1010+1111+export const PasswordUpdatedForm = ({
1212+ onPressNext,
1313+}: {
1414+ onPressNext: () => void
1515+}) => {
1616+ const {screen} = useAnalytics()
1717+ const {_} = useLingui()
1818+ const {gtMobile} = useBreakpoints()
1919+2020+ useEffect(() => {
2121+ screen('Signin:PasswordUpdatedForm')
2222+ }, [screen])
2323+2424+ return (
2525+ <FormContainer
2626+ testID="passwordUpdatedForm"
2727+ style={[a.gap_2xl, !gtMobile && a.mt_5xl]}>
2828+ <Text style={[a.text_3xl, a.font_bold, a.text_center]}>
2929+ <Trans>Password updated!</Trans>
3030+ </Text>
3131+ <Text style={[a.text_center, a.mx_auto, {maxWidth: '80%'}]}>
3232+ <Trans>You can now sign in with your new password.</Trans>
3333+ </Text>
3434+ <View style={[a.flex_row, a.justify_center]}>
3535+ <Button
3636+ onPress={onPressNext}
3737+ label={_(msg`Close alert`)}
3838+ accessibilityHint={_(msg`Closes password update alert`)}
3939+ variant="solid"
4040+ color="primary"
4141+ size="medium">
4242+ <ButtonText>
4343+ <Trans>Okay</Trans>
4444+ </ButtonText>
4545+ </Button>
4646+ </View>
4747+ </FormContainer>
4848+ )
4949+}
+189
src/screens/Login/SetNewPasswordForm.tsx
···11+import React, {useState, useEffect} from 'react'
22+import {ActivityIndicator, View} from 'react-native'
33+import {BskyAgent} from '@atproto/api'
44+import {useAnalytics} from 'lib/analytics/analytics'
55+66+import {isNetworkError} from 'lib/strings/errors'
77+import {cleanError} from 'lib/strings/errors'
88+import {checkAndFormatResetCode} from 'lib/strings/password'
99+import {logger} from '#/logger'
1010+import {Trans, msg} from '@lingui/macro'
1111+import {useLingui} from '@lingui/react'
1212+import {FormContainer} from './FormContainer'
1313+import {Text} from '#/components/Typography'
1414+import * as TextField from '#/components/forms/TextField'
1515+import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
1616+import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
1717+import {Button, ButtonText} from '#/components/Button'
1818+import {useTheme, atoms as a} from '#/alf'
1919+import {FormError} from './FormError'
2020+2121+export const SetNewPasswordForm = ({
2222+ error,
2323+ serviceUrl,
2424+ setError,
2525+ onPressBack,
2626+ onPasswordSet,
2727+}: {
2828+ error: string
2929+ serviceUrl: string
3030+ setError: (v: string) => void
3131+ onPressBack: () => void
3232+ onPasswordSet: () => void
3333+}) => {
3434+ const {screen} = useAnalytics()
3535+ const {_} = useLingui()
3636+ const t = useTheme()
3737+3838+ useEffect(() => {
3939+ screen('Signin:SetNewPasswordForm')
4040+ }, [screen])
4141+4242+ const [isProcessing, setIsProcessing] = useState<boolean>(false)
4343+ const [resetCode, setResetCode] = useState<string>('')
4444+ const [password, setPassword] = useState<string>('')
4545+4646+ const onPressNext = async () => {
4747+ onPasswordSet()
4848+ if (Math.random() > 0) return
4949+ // Check that the code is correct. We do this again just incase the user enters the code after their pw and we
5050+ // don't get to call onBlur first
5151+ const formattedCode = checkAndFormatResetCode(resetCode)
5252+ // TODO Better password strength check
5353+ if (!formattedCode || !password) {
5454+ setError(
5555+ _(
5656+ msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
5757+ ),
5858+ )
5959+ return
6060+ }
6161+6262+ setError('')
6363+ setIsProcessing(true)
6464+6565+ try {
6666+ const agent = new BskyAgent({service: serviceUrl})
6767+ await agent.com.atproto.server.resetPassword({
6868+ token: formattedCode,
6969+ password,
7070+ })
7171+ onPasswordSet()
7272+ } catch (e: any) {
7373+ const errMsg = e.toString()
7474+ logger.warn('Failed to set new password', {error: e})
7575+ setIsProcessing(false)
7676+ if (isNetworkError(e)) {
7777+ setError(
7878+ 'Unable to contact your service. Please check your Internet connection.',
7979+ )
8080+ } else {
8181+ setError(cleanError(errMsg))
8282+ }
8383+ }
8484+ }
8585+8686+ const onBlur = () => {
8787+ const formattedCode = checkAndFormatResetCode(resetCode)
8888+ if (!formattedCode) {
8989+ setError(
9090+ _(
9191+ msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
9292+ ),
9393+ )
9494+ return
9595+ }
9696+ setResetCode(formattedCode)
9797+ }
9898+9999+ return (
100100+ <FormContainer
101101+ testID="setNewPasswordForm"
102102+ title={<Trans>Set new password</Trans>}>
103103+ <Text>
104104+ <Trans>
105105+ You will receive an email with a "reset code." Enter that code here,
106106+ then enter your new password.
107107+ </Trans>
108108+ </Text>
109109+110110+ <View>
111111+ <TextField.Label>Reset code</TextField.Label>
112112+ <TextField.Root>
113113+ <TextField.Icon icon={Ticket} />
114114+ <TextField.Input
115115+ testID="resetCodeInput"
116116+ label={_(msg`Looks like XXXXX-XXXXX`)}
117117+ autoCapitalize="none"
118118+ autoCorrect={false}
119119+ autoComplete="off"
120120+ value={resetCode}
121121+ onChangeText={setResetCode}
122122+ onFocus={() => setError('')}
123123+ onBlur={onBlur}
124124+ editable={!isProcessing}
125125+ accessibilityHint={_(
126126+ msg`Input code sent to your email for password reset`,
127127+ )}
128128+ />
129129+ </TextField.Root>
130130+ </View>
131131+132132+ <View>
133133+ <TextField.Label>New password</TextField.Label>
134134+ <TextField.Root>
135135+ <TextField.Icon icon={Lock} />
136136+ <TextField.Input
137137+ testID="newPasswordInput"
138138+ label={_(msg`Enter a password`)}
139139+ autoCapitalize="none"
140140+ autoCorrect={false}
141141+ autoComplete="password"
142142+ returnKeyType="done"
143143+ secureTextEntry={true}
144144+ textContentType="password"
145145+ clearButtonMode="while-editing"
146146+ value={password}
147147+ onChangeText={setPassword}
148148+ onSubmitEditing={onPressNext}
149149+ editable={!isProcessing}
150150+ accessibilityHint={_(msg`Input new password`)}
151151+ />
152152+ </TextField.Root>
153153+ </View>
154154+ <FormError error={error} />
155155+ <View style={[a.flex_row, a.align_center]}>
156156+ <Button
157157+ label={_(msg`Back`)}
158158+ variant="solid"
159159+ color="secondary"
160160+ size="small"
161161+ onPress={onPressBack}>
162162+ <ButtonText>
163163+ <Trans>Back</Trans>
164164+ </ButtonText>
165165+ </Button>
166166+ <View style={a.flex_1} />
167167+ {isProcessing ? (
168168+ <ActivityIndicator />
169169+ ) : (
170170+ <Button
171171+ label={_(msg`Next`)}
172172+ variant="solid"
173173+ color="primary"
174174+ size="small"
175175+ onPress={onPressNext}>
176176+ <ButtonText>
177177+ <Trans>Next</Trans>
178178+ </ButtonText>
179179+ </Button>
180180+ )}
181181+ {isProcessing ? (
182182+ <Text style={[t.atoms.text_contrast_high, a.pl_md]}>
183183+ <Trans>Updating...</Trans>
184184+ </Text>
185185+ ) : undefined}
186186+ </View>
187187+ </FormContainer>
188188+ )
189189+}
+3-3
src/screens/Login/index.tsx
···1313import {logger} from '#/logger'
1414import {atoms as a} from '#/alf'
1515import {ChooseAccountForm} from './ChooseAccountForm'
1616-import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm'
1717-import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm'
1818-import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm'
1616+import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
1717+import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
1818+import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
1919import {LoginForm} from '#/screens/Login/LoginForm'
20202121enum Forms {