convert password reset flow

This commit is contained in:
Samuel Newman 2024-03-15 13:49:13 +00:00
parent f71ec52517
commit a1fc95f30e
16 changed files with 803 additions and 799 deletions

View file

@ -1,5 +1,5 @@
import React from 'react'
import {ScrollView, TouchableOpacity, View} from 'react-native'
import {TouchableOpacity, View} from 'react-native'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import flattenReactChildren from 'react-keyed-flatten-children'
@ -7,16 +7,17 @@ import flattenReactChildren from 'react-keyed-flatten-children'
import {useAnalytics} from 'lib/analytics/analytics'
import {UserAvatar} from '../../view/com/util/UserAvatar'
import {colors} from 'lib/styles'
import {styles} from '../../view/com/auth/login/styles'
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
import {useProfileQuery} from '#/state/queries/profile'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import * as Toast from '#/view/com/util/Toast'
import {Button} from '#/components/Button'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import * as TextField from '#/components/forms/TextField'
import {FormContainer} from './FormContainer'
function Group({children}: {children: React.ReactNode}) {
const t = useTheme()
@ -106,7 +107,6 @@ export const ChooseAccountForm = ({
const {accounts, currentAccount} = useSession()
const {initSession} = useSessionApi()
const {setShowLoggedOut} = useLoggedOutViewControls()
const {gtMobile} = useBreakpoints()
React.useEffect(() => {
screen('Choose Account')
@ -133,12 +133,13 @@ export const ChooseAccountForm = ({
)
return (
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
<View style={!gtMobile && a.px_lg}>
<Text
style={[a.mt_md, a.mb_lg, a.font_bold, t.atoms.text_contrast_medium]}>
<FormContainer
testID="chooseAccountForm"
title={<Trans>Select account</Trans>}>
<View>
<TextField.Label>
<Trans>Sign in as...</Trans>
</Text>
</TextField.Label>
<Group>
{accounts.map(account => (
<AccountItem
@ -171,18 +172,18 @@ export const ChooseAccountForm = ({
</View>
</TouchableOpacity>
</Group>
<View style={[a.flex_row, a.mt_lg]}>
<Button
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
{_(msg`Back`)}
</Button>
<View style={[a.flex_1]} />
</View>
</View>
</ScrollView>
<View style={[a.flex_row]}>
<Button
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
{_(msg`Back`)}
</Button>
<View style={[a.flex_1]} />
</View>
</FormContainer>
)
}

View file

@ -0,0 +1,183 @@
import React, {useState, useEffect} from 'react'
import {ActivityIndicator, Keyboard, View} from 'react-native'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import * as EmailValidator from 'email-validator'
import {BskyAgent} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import * as TextField from '#/components/forms/TextField'
import {HostingProvider} from '#/components/forms/HostingProvider'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {atoms as a, useTheme} from '#/alf'
import {useAnalytics} from 'lib/analytics/analytics'
import {isNetworkError} from 'lib/strings/errors'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {FormContainer} from './FormContainer'
import {FormError} from './FormError'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
export const ForgotPasswordForm = ({
error,
serviceUrl,
serviceDescription,
setError,
setServiceUrl,
onPressBack,
onEmailSent,
}: {
error: string
serviceUrl: string
serviceDescription: ServiceDescription | undefined
setError: (v: string) => void
setServiceUrl: (v: string) => void
onPressBack: () => void
onEmailSent: () => void
}) => {
const t = useTheme()
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [email, setEmail] = useState<string>('')
const {screen} = useAnalytics()
const {_} = useLingui()
useEffect(() => {
screen('Signin:ForgotPassword')
}, [screen])
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
}, [])
const onPressNext = async () => {
if (!EmailValidator.validate(email)) {
return setError(_(msg`Your email appears to be invalid.`))
}
setError('')
setIsProcessing(true)
try {
const agent = new BskyAgent({service: serviceUrl})
await agent.com.atproto.server.requestPasswordReset({email})
onEmailSent()
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to request password reset', {error: e})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
_(
msg`Unable to contact your service. Please check your Internet connection.`,
),
)
} else {
setError(cleanError(errMsg))
}
}
}
return (
<FormContainer
testID="forgotPasswordForm"
title={<Trans>Reset password</Trans>}>
<View>
<TextField.Label>
<Trans>Hosting provider</Trans>
</TextField.Label>
<HostingProvider
serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl}
onOpenDialog={onPressSelectService}
/>
</View>
<View>
<TextField.Label>
<Trans>Email address</Trans>
</TextField.Label>
<TextField.Root>
<TextField.Icon icon={At} />
<TextField.Input
testID="forgotPasswordEmail"
label={_(msg`Enter your email address`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="email"
value={email}
onChangeText={setEmail}
editable={!isProcessing}
accessibilityHint={_(msg`Sets email for password reset`)}
/>
</TextField.Root>
</View>
<View>
<Text style={[t.atoms.text_contrast_high, a.mb_md]}>
<Trans>
Enter the email you used to create your account. We'll send you a
"reset code" so you can set a new password.
</Trans>
</Text>
</View>
<FormError error={error} />
<View style={[a.flex_row, a.align_center]}>
<Button
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
<ButtonText>
<Trans>Back</Trans>
</ButtonText>
</Button>
<View style={a.flex_1} />
{!serviceDescription || isProcessing ? (
<ActivityIndicator />
) : (
<Button
label={_(msg`Next`)}
variant="solid"
color={email ? 'primary' : 'secondary'}
size="small"
onPress={onPressNext}
disabled={!email}>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
</Button>
)}
{!serviceDescription || isProcessing ? (
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
<Trans>Processing...</Trans>
</Text>
) : undefined}
</View>
<View
style={[
t.atoms.border_contrast_medium,
a.border_t,
a.pt_2xl,
a.mt_md,
a.flex_row,
a.justify_center,
]}>
<Button
testID="skipSendEmailButton"
onPress={onEmailSent}
label={_(msg`Go to next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}
size="small"
variant="ghost"
color="secondary">
<ButtonText>
<Trans>Already have a code?</Trans>
</ButtonText>
</Button>
</View>
</FormContainer>
)
}

View file

@ -0,0 +1,52 @@
import React from 'react'
import {
ScrollView,
StyleSheet,
View,
type StyleProp,
type ViewStyle,
} from 'react-native'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import {isWeb} from '#/platform/detection'
export function FormContainer({
testID,
title,
children,
style,
contentContainerStyle,
}: {
testID?: string
title?: React.ReactNode
children: React.ReactNode
style?: StyleProp<ViewStyle>
contentContainerStyle?: StyleProp<ViewStyle>
}) {
const {gtMobile} = useBreakpoints()
const t = useTheme()
return (
<ScrollView
testID={testID}
style={[styles.maxHeight, contentContainerStyle]}>
<View
style={[a.gap_lg, a.flex_1, !gtMobile && [a.px_lg, a.pt_md], style]}>
{title && !gtMobile && (
<Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}>
{title}
</Text>
)}
{children}
</View>
</ScrollView>
)
}
const styles = StyleSheet.create({
maxHeight: {
// @ts-ignore web only -prf
maxHeight: isWeb ? '100vh' : undefined,
height: !isWeb ? '100%' : undefined,
},
})

View file

@ -0,0 +1,34 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {Text} from '#/components/Typography'
import {atoms as a, useTheme} from '#/alf'
import {colors} from '#/lib/styles'
export function FormError({error}: {error?: string}) {
const t = useTheme()
if (!error) return null
return (
<View style={styles.error}>
<Warning fill={t.palette.white} size="sm" />
<View style={(a.flex_1, a.ml_sm)}>
<Text style={[{color: t.palette.white}, a.font_bold]}>{error}</Text>
</View>
</View>
)
}
const styles = StyleSheet.create({
error: {
backgroundColor: colors.red4,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 15,
borderRadius: 8,
paddingHorizontal: 8,
paddingVertical: 8,
},
})

View file

@ -2,36 +2,29 @@ import React, {useState, useRef} from 'react'
import {
ActivityIndicator,
Keyboard,
ScrollView,
TextInput,
TouchableOpacity,
View,
} from 'react-native'
import {ComAtprotoServerDescribeServer} from '@atproto/api'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useAnalytics} from 'lib/analytics/analytics'
import {s} from 'lib/styles'
import {createFullHandle} from 'lib/strings/handles'
import {toNiceDomain} from 'lib/strings/url-helpers'
import {isNetworkError} from 'lib/strings/errors'
import {useSessionApi} from '#/state/session'
import {cleanError} from 'lib/strings/errors'
import {logger} from '#/logger'
import {styles} from '../../view/com/auth/login/styles'
import {useLingui} from '@lingui/react'
import {useDialogControl} from '#/components/Dialog'
import {ServerInputDialog} from '../../view/com/auth/server-input'
import {Button, ButtonText} from '#/components/Button'
import {isAndroid} from '#/platform/detection'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import {Text} from '#/components/Typography'
import * as TextField from '#/components/forms/TextField'
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
import {HostingProvider} from '#/components/forms/HostingProvider'
import {FormContainer} from './FormContainer'
import {FormError} from './FormError'
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
@ -64,14 +57,11 @@ export const LoginForm = ({
const passwordInputRef = useRef<TextInput>(null)
const {_} = useLingui()
const {login} = useSessionApi()
const serverInputControl = useDialogControl()
const {gtMobile} = useBreakpoints()
const onPressSelectService = () => {
serverInputControl.open()
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
track('Signin:PressedSelectService')
}
}, [track])
const onPressNext = async () => {
Keyboard.dismiss()
@ -131,171 +121,138 @@ export const LoginForm = ({
const isReady = !!serviceDescription && !!identifier && !!password
return (
<ScrollView testID="loginForm" style={a.h_full}>
<View style={[a.gap_lg, !gtMobile && a.px_lg, a.flex_1]}>
<ServerInputDialog
control={serverInputControl}
onSelect={setServiceUrl}
<FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}>
<View>
<TextField.Label>
<Trans>Hosting provider</Trans>
</TextField.Label>
<HostingProvider
serviceUrl={serviceUrl}
onSelectServiceUrl={setServiceUrl}
onOpenDialog={onPressSelectService}
/>
<View>
<TextField.Label>
<Trans>Hosting provider</Trans>
</TextField.Label>
</View>
<View>
<TextField.Label>
<Trans>Account</Trans>
</TextField.Label>
<TextField.Root>
<TextField.Icon icon={At} />
<TextField.Input
testID="loginUsernameInput"
label={_(msg`Username or email address`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
textContentType="username"
onSubmitEditing={() => {
passwordInputRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing}
accessibilityHint={_(
msg`Input the username or email address you used at signup`,
)}
/>
</TextField.Root>
</View>
<View>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
testID="loginPasswordInput"
inputRef={passwordInputRef}
label={_(msg`Password`)}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityHint={
identifier === ''
? _(msg`Input your password`)
: _(msg`Input the password tied to ${identifier}`)
}
/>
<TouchableOpacity
testID="forgotPasswordButton"
onPress={onPressForgotPassword}
accessibilityRole="button"
accessibilityLabel={_(msg`Forgot password`)}
accessibilityHint={_(msg`Opens password reset form`)}
style={[
a.w_full,
a.flex_row,
a.align_center,
a.rounded_sm,
a.px_md,
a.gap_xs,
{paddingVertical: isAndroid ? 14 : 9},
t.atoms.bg_contrast_25,
]}
onPress={onPressSelectService}>
<TextField.Icon icon={Globe} />
<Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text>
<View
style={[
a.rounded_sm,
t.atoms.bg_contrast_100,
{marginLeft: 'auto', left: 6, padding: 6},
]}>
<Pencil
style={{color: t.palette.contrast_500}}
height={18}
width={18}
/>
</View>
t.atoms.bg_contrast_100,
{marginLeft: 'auto', left: 6, padding: 6},
a.z_10,
]}>
<ButtonText style={t.atoms.text_contrast_medium}>
<Trans>Forgot?</Trans>
</ButtonText>
</TouchableOpacity>
</View>
<View>
<TextField.Label>
<Trans>Account</Trans>
</TextField.Label>
<TextField.Root>
<TextField.Icon icon={At} />
<TextField.Input
testID="loginUsernameInput"
label={_(msg`Username or email address`)}
autoCapitalize="none"
autoFocus
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
textContentType="username"
onSubmitEditing={() => {
passwordInputRef.current?.focus()
}}
blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field
value={identifier}
onChangeText={str =>
setIdentifier((str || '').toLowerCase().trim())
}
editable={!isProcessing}
accessibilityHint={_(
msg`Input the username or email address you used at signup`,
)}
/>
</TextField.Root>
</View>
<View>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
testID="loginPasswordInput"
inputRef={passwordInputRef}
label={_(msg`Password`)}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
enablesReturnKeyAutomatically={true}
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onSubmitEditing={onPressNext}
blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing
editable={!isProcessing}
accessibilityHint={
identifier === ''
? _(msg`Input your password`)
: _(msg`Input the password tied to ${identifier}`)
}
/>
<TouchableOpacity
testID="forgotPasswordButton"
onPress={onPressForgotPassword}
accessibilityRole="button"
accessibilityLabel={_(msg`Forgot password`)}
accessibilityHint={_(msg`Opens password reset form`)}
style={[
a.rounded_sm,
t.atoms.bg_contrast_100,
{marginLeft: 'auto', left: 6, padding: 6},
a.z_10,
]}>
<ButtonText style={t.atoms.text_contrast_medium}>
<Trans>Forgot?</Trans>
</ButtonText>
</TouchableOpacity>
</TextField.Root>
</View>
{error ? (
<View style={[styles.error, {marginHorizontal: 0}]}>
<Warning style={s.white} size="sm" />
<View style={(a.flex_1, a.ml_sm)}>
<Text style={[s.white, s.bold]}>{error}</Text>
</View>
</View>
) : undefined}
<View style={[a.flex_row, a.align_center]}>
</TextField.Root>
</View>
<FormError error={error} />
<View style={[a.flex_row, a.align_center]}>
<Button
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
<ButtonText>
<Trans>Back</Trans>
</ButtonText>
</Button>
<View style={a.flex_1} />
{!serviceDescription && error ? (
<Button
label={_(msg`Back`)}
testID="loginRetryButton"
label={_(msg`Retry`)}
accessibilityHint={_(msg`Retries login`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
{_(msg`Back`)}
onPress={onPressRetryConnect}>
{_(msg`Retry`)}
</Button>
<View style={s.flex1} />
{!serviceDescription && error ? (
<Button
testID="loginRetryButton"
label={_(msg`Retry`)}
accessibilityHint={_(msg`Retries login`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressRetryConnect}>
{_(msg`Retry`)}
</Button>
) : !serviceDescription ? (
<>
<ActivityIndicator />
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : isProcessing ? (
) : !serviceDescription ? (
<>
<ActivityIndicator />
) : isReady ? (
<Button
label={_(msg`Next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid"
color="primary"
size="small"
onPress={onPressNext}>
{_(msg`Next`)}
</Button>
) : undefined}
</View>
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : isProcessing ? (
<ActivityIndicator />
) : isReady ? (
<Button
label={_(msg`Next`)}
accessibilityHint={_(msg`Navigates to the next screen`)}
variant="solid"
color="primary"
size="small"
onPress={onPressNext}>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
</Button>
) : undefined}
</View>
</ScrollView>
</FormContainer>
)
}

View file

@ -0,0 +1,49 @@
import React, {useEffect} from 'react'
import {View} from 'react-native'
import {useAnalytics} from 'lib/analytics/analytics'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FormContainer} from './FormContainer'
import {Button, ButtonText} from '#/components/Button'
import {Text} from '#/components/Typography'
import {atoms as a, useBreakpoints} from '#/alf'
export const PasswordUpdatedForm = ({
onPressNext,
}: {
onPressNext: () => void
}) => {
const {screen} = useAnalytics()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
useEffect(() => {
screen('Signin:PasswordUpdatedForm')
}, [screen])
return (
<FormContainer
testID="passwordUpdatedForm"
style={[a.gap_2xl, !gtMobile && a.mt_5xl]}>
<Text style={[a.text_3xl, a.font_bold, a.text_center]}>
<Trans>Password updated!</Trans>
</Text>
<Text style={[a.text_center, a.mx_auto, {maxWidth: '80%'}]}>
<Trans>You can now sign in with your new password.</Trans>
</Text>
<View style={[a.flex_row, a.justify_center]}>
<Button
onPress={onPressNext}
label={_(msg`Close alert`)}
accessibilityHint={_(msg`Closes password update alert`)}
variant="solid"
color="primary"
size="medium">
<ButtonText>
<Trans>Okay</Trans>
</ButtonText>
</Button>
</View>
</FormContainer>
)
}

View file

@ -0,0 +1,189 @@
import React, {useState, useEffect} from 'react'
import {ActivityIndicator, View} from 'react-native'
import {BskyAgent} from '@atproto/api'
import {useAnalytics} from 'lib/analytics/analytics'
import {isNetworkError} from 'lib/strings/errors'
import {cleanError} from 'lib/strings/errors'
import {checkAndFormatResetCode} from 'lib/strings/password'
import {logger} from '#/logger'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FormContainer} from './FormContainer'
import {Text} from '#/components/Typography'
import * as TextField from '#/components/forms/TextField'
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket'
import {Button, ButtonText} from '#/components/Button'
import {useTheme, atoms as a} from '#/alf'
import {FormError} from './FormError'
export const SetNewPasswordForm = ({
error,
serviceUrl,
setError,
onPressBack,
onPasswordSet,
}: {
error: string
serviceUrl: string
setError: (v: string) => void
onPressBack: () => void
onPasswordSet: () => void
}) => {
const {screen} = useAnalytics()
const {_} = useLingui()
const t = useTheme()
useEffect(() => {
screen('Signin:SetNewPasswordForm')
}, [screen])
const [isProcessing, setIsProcessing] = useState<boolean>(false)
const [resetCode, setResetCode] = useState<string>('')
const [password, setPassword] = useState<string>('')
const onPressNext = async () => {
onPasswordSet()
if (Math.random() > 0) return
// Check that the code is correct. We do this again just incase the user enters the code after their pw and we
// don't get to call onBlur first
const formattedCode = checkAndFormatResetCode(resetCode)
// TODO Better password strength check
if (!formattedCode || !password) {
setError(
_(
msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
),
)
return
}
setError('')
setIsProcessing(true)
try {
const agent = new BskyAgent({service: serviceUrl})
await agent.com.atproto.server.resetPassword({
token: formattedCode,
password,
})
onPasswordSet()
} catch (e: any) {
const errMsg = e.toString()
logger.warn('Failed to set new password', {error: e})
setIsProcessing(false)
if (isNetworkError(e)) {
setError(
'Unable to contact your service. Please check your Internet connection.',
)
} else {
setError(cleanError(errMsg))
}
}
}
const onBlur = () => {
const formattedCode = checkAndFormatResetCode(resetCode)
if (!formattedCode) {
setError(
_(
msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`,
),
)
return
}
setResetCode(formattedCode)
}
return (
<FormContainer
testID="setNewPasswordForm"
title={<Trans>Set new password</Trans>}>
<Text>
<Trans>
You will receive an email with a "reset code." Enter that code here,
then enter your new password.
</Trans>
</Text>
<View>
<TextField.Label>Reset code</TextField.Label>
<TextField.Root>
<TextField.Icon icon={Ticket} />
<TextField.Input
testID="resetCodeInput"
label={_(msg`Looks like XXXXX-XXXXX`)}
autoCapitalize="none"
autoCorrect={false}
autoComplete="off"
value={resetCode}
onChangeText={setResetCode}
onFocus={() => setError('')}
onBlur={onBlur}
editable={!isProcessing}
accessibilityHint={_(
msg`Input code sent to your email for password reset`,
)}
/>
</TextField.Root>
</View>
<View>
<TextField.Label>New password</TextField.Label>
<TextField.Root>
<TextField.Icon icon={Lock} />
<TextField.Input
testID="newPasswordInput"
label={_(msg`Enter a password`)}
autoCapitalize="none"
autoCorrect={false}
autoComplete="password"
returnKeyType="done"
secureTextEntry={true}
textContentType="password"
clearButtonMode="while-editing"
value={password}
onChangeText={setPassword}
onSubmitEditing={onPressNext}
editable={!isProcessing}
accessibilityHint={_(msg`Input new password`)}
/>
</TextField.Root>
</View>
<FormError error={error} />
<View style={[a.flex_row, a.align_center]}>
<Button
label={_(msg`Back`)}
variant="solid"
color="secondary"
size="small"
onPress={onPressBack}>
<ButtonText>
<Trans>Back</Trans>
</ButtonText>
</Button>
<View style={a.flex_1} />
{isProcessing ? (
<ActivityIndicator />
) : (
<Button
label={_(msg`Next`)}
variant="solid"
color="primary"
size="small"
onPress={onPressNext}>
<ButtonText>
<Trans>Next</Trans>
</ButtonText>
</Button>
)}
{isProcessing ? (
<Text style={[t.atoms.text_contrast_high, a.pl_md]}>
<Trans>Updating...</Trans>
</Text>
) : undefined}
</View>
</FormContainer>
)
}

View file

@ -13,9 +13,9 @@ import {msg} from '@lingui/macro'
import {logger} from '#/logger'
import {atoms as a} from '#/alf'
import {ChooseAccountForm} from './ChooseAccountForm'
import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm'
import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm'
import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm'
import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
import {LoginForm} from '#/screens/Login/LoginForm'
enum Forms {