convert password reset flow
parent
f71ec52517
commit
a1fc95f30e
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" stroke="#000" stroke-linejoin="round" d="M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12c0-1.296.704-2.426 1.75-3.032a.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z"/></svg>
|
After Width: | Height: | Size: 772 B |
|
@ -300,6 +300,9 @@ export const atoms = {
|
||||||
/*
|
/*
|
||||||
* Padding
|
* Padding
|
||||||
*/
|
*/
|
||||||
|
p_0: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
p_2xs: {
|
p_2xs: {
|
||||||
padding: tokens.space._2xs,
|
padding: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -330,6 +333,10 @@ export const atoms = {
|
||||||
p_5xl: {
|
p_5xl: {
|
||||||
padding: tokens.space._5xl,
|
padding: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
px_0: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
},
|
||||||
px_2xs: {
|
px_2xs: {
|
||||||
paddingLeft: tokens.space._2xs,
|
paddingLeft: tokens.space._2xs,
|
||||||
paddingRight: tokens.space._2xs,
|
paddingRight: tokens.space._2xs,
|
||||||
|
@ -370,6 +377,10 @@ export const atoms = {
|
||||||
paddingLeft: tokens.space._5xl,
|
paddingLeft: tokens.space._5xl,
|
||||||
paddingRight: tokens.space._5xl,
|
paddingRight: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
py_0: {
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
},
|
||||||
py_2xs: {
|
py_2xs: {
|
||||||
paddingTop: tokens.space._2xs,
|
paddingTop: tokens.space._2xs,
|
||||||
paddingBottom: tokens.space._2xs,
|
paddingBottom: tokens.space._2xs,
|
||||||
|
@ -410,6 +421,9 @@ export const atoms = {
|
||||||
paddingTop: tokens.space._5xl,
|
paddingTop: tokens.space._5xl,
|
||||||
paddingBottom: tokens.space._5xl,
|
paddingBottom: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
pt_0: {
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
pt_2xs: {
|
pt_2xs: {
|
||||||
paddingTop: tokens.space._2xs,
|
paddingTop: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -440,6 +454,9 @@ export const atoms = {
|
||||||
pt_5xl: {
|
pt_5xl: {
|
||||||
paddingTop: tokens.space._5xl,
|
paddingTop: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
pb_0: {
|
||||||
|
paddingBottom: 0,
|
||||||
|
},
|
||||||
pb_2xs: {
|
pb_2xs: {
|
||||||
paddingBottom: tokens.space._2xs,
|
paddingBottom: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -470,6 +487,9 @@ export const atoms = {
|
||||||
pb_5xl: {
|
pb_5xl: {
|
||||||
paddingBottom: tokens.space._5xl,
|
paddingBottom: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
pl_0: {
|
||||||
|
paddingLeft: 0,
|
||||||
|
},
|
||||||
pl_2xs: {
|
pl_2xs: {
|
||||||
paddingLeft: tokens.space._2xs,
|
paddingLeft: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -500,6 +520,9 @@ export const atoms = {
|
||||||
pl_5xl: {
|
pl_5xl: {
|
||||||
paddingLeft: tokens.space._5xl,
|
paddingLeft: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
pr_0: {
|
||||||
|
paddingRight: 0,
|
||||||
|
},
|
||||||
pr_2xs: {
|
pr_2xs: {
|
||||||
paddingRight: tokens.space._2xs,
|
paddingRight: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -534,6 +557,9 @@ export const atoms = {
|
||||||
/*
|
/*
|
||||||
* Margin
|
* Margin
|
||||||
*/
|
*/
|
||||||
|
m_0: {
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
m_2xs: {
|
m_2xs: {
|
||||||
margin: tokens.space._2xs,
|
margin: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -564,6 +590,13 @@ export const atoms = {
|
||||||
m_5xl: {
|
m_5xl: {
|
||||||
margin: tokens.space._5xl,
|
margin: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
m_auto: {
|
||||||
|
margin: 'auto',
|
||||||
|
},
|
||||||
|
mx_0: {
|
||||||
|
marginLeft: 0,
|
||||||
|
marginRight: 0,
|
||||||
|
},
|
||||||
mx_2xs: {
|
mx_2xs: {
|
||||||
marginLeft: tokens.space._2xs,
|
marginLeft: tokens.space._2xs,
|
||||||
marginRight: tokens.space._2xs,
|
marginRight: tokens.space._2xs,
|
||||||
|
@ -604,6 +637,14 @@ export const atoms = {
|
||||||
marginLeft: tokens.space._5xl,
|
marginLeft: tokens.space._5xl,
|
||||||
marginRight: tokens.space._5xl,
|
marginRight: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
mx_auto: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
|
my_0: {
|
||||||
|
marginTop: 0,
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
my_2xs: {
|
my_2xs: {
|
||||||
marginTop: tokens.space._2xs,
|
marginTop: tokens.space._2xs,
|
||||||
marginBottom: tokens.space._2xs,
|
marginBottom: tokens.space._2xs,
|
||||||
|
@ -644,6 +685,13 @@ export const atoms = {
|
||||||
marginTop: tokens.space._5xl,
|
marginTop: tokens.space._5xl,
|
||||||
marginBottom: tokens.space._5xl,
|
marginBottom: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
my_auto: {
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
},
|
||||||
|
mt_0: {
|
||||||
|
marginTop: 0,
|
||||||
|
},
|
||||||
mt_2xs: {
|
mt_2xs: {
|
||||||
marginTop: tokens.space._2xs,
|
marginTop: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -674,6 +722,12 @@ export const atoms = {
|
||||||
mt_5xl: {
|
mt_5xl: {
|
||||||
marginTop: tokens.space._5xl,
|
marginTop: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
mt_auto: {
|
||||||
|
marginTop: 'auto',
|
||||||
|
},
|
||||||
|
mb_0: {
|
||||||
|
marginBottom: 0,
|
||||||
|
},
|
||||||
mb_2xs: {
|
mb_2xs: {
|
||||||
marginBottom: tokens.space._2xs,
|
marginBottom: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -704,6 +758,12 @@ export const atoms = {
|
||||||
mb_5xl: {
|
mb_5xl: {
|
||||||
marginBottom: tokens.space._5xl,
|
marginBottom: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
mb_auto: {
|
||||||
|
marginBottom: 'auto',
|
||||||
|
},
|
||||||
|
ml_0: {
|
||||||
|
marginLeft: 0,
|
||||||
|
},
|
||||||
ml_2xs: {
|
ml_2xs: {
|
||||||
marginLeft: tokens.space._2xs,
|
marginLeft: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -734,6 +794,12 @@ export const atoms = {
|
||||||
ml_5xl: {
|
ml_5xl: {
|
||||||
marginLeft: tokens.space._5xl,
|
marginLeft: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
ml_auto: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
},
|
||||||
|
mr_0: {
|
||||||
|
marginRight: 0,
|
||||||
|
},
|
||||||
mr_2xs: {
|
mr_2xs: {
|
||||||
marginRight: tokens.space._2xs,
|
marginRight: tokens.space._2xs,
|
||||||
},
|
},
|
||||||
|
@ -764,4 +830,7 @@ export const atoms = {
|
||||||
mr_5xl: {
|
mr_5xl: {
|
||||||
marginRight: tokens.space._5xl,
|
marginRight: tokens.space._5xl,
|
||||||
},
|
},
|
||||||
|
mr_auto: {
|
||||||
|
marginRight: 'auto',
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {TouchableOpacity, View} from 'react-native'
|
||||||
|
|
||||||
|
import {isAndroid} from '#/platform/detection'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
||||||
|
import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
|
||||||
|
import * as TextField from './TextField'
|
||||||
|
import {useDialogControl} from '../Dialog'
|
||||||
|
import {Text} from '../Typography'
|
||||||
|
import {ServerInputDialog} from '#/view/com/auth/server-input'
|
||||||
|
import {toNiceDomain} from '#/lib/strings/url-helpers'
|
||||||
|
|
||||||
|
export function HostingProvider({
|
||||||
|
serviceUrl,
|
||||||
|
onSelectServiceUrl,
|
||||||
|
onOpenDialog,
|
||||||
|
}: {
|
||||||
|
serviceUrl: string
|
||||||
|
onSelectServiceUrl: (provider: string) => void
|
||||||
|
onOpenDialog?: () => void
|
||||||
|
}) {
|
||||||
|
const serverInputControl = useDialogControl()
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
const onPressSelectService = React.useCallback(() => {
|
||||||
|
serverInputControl.open()
|
||||||
|
if (onOpenDialog) {
|
||||||
|
onOpenDialog()
|
||||||
|
}
|
||||||
|
}, [onOpenDialog, serverInputControl])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ServerInputDialog
|
||||||
|
control={serverInputControl}
|
||||||
|
onSelect={onSelectServiceUrl}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
accessibilityRole="button"
|
||||||
|
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>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {createSinglePathSVG} from './TEMPLATE'
|
||||||
|
|
||||||
|
export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||||
|
path: 'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12c0-1.296.704-2.426 1.75-3.032a.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
|
||||||
|
})
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {ScrollView, TouchableOpacity, View} from 'react-native'
|
import {TouchableOpacity, View} from 'react-native'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import flattenReactChildren from 'react-keyed-flatten-children'
|
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 {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {UserAvatar} from '../../view/com/util/UserAvatar'
|
import {UserAvatar} from '../../view/com/util/UserAvatar'
|
||||||
import {colors} from 'lib/styles'
|
import {colors} from 'lib/styles'
|
||||||
import {styles} from '../../view/com/auth/login/styles'
|
|
||||||
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
|
import {useSession, useSessionApi, SessionAccount} from '#/state/session'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {Button} from '#/components/Button'
|
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 {Text} from '#/components/Typography'
|
||||||
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
|
import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
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}) {
|
function Group({children}: {children: React.ReactNode}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
@ -106,7 +107,6 @@ export const ChooseAccountForm = ({
|
||||||
const {accounts, currentAccount} = useSession()
|
const {accounts, currentAccount} = useSession()
|
||||||
const {initSession} = useSessionApi()
|
const {initSession} = useSessionApi()
|
||||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
screen('Choose Account')
|
screen('Choose Account')
|
||||||
|
@ -133,12 +133,13 @@ export const ChooseAccountForm = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView testID="chooseAccountForm" style={styles.maxHeight}>
|
<FormContainer
|
||||||
<View style={!gtMobile && a.px_lg}>
|
testID="chooseAccountForm"
|
||||||
<Text
|
title={<Trans>Select account</Trans>}>
|
||||||
style={[a.mt_md, a.mb_lg, a.font_bold, t.atoms.text_contrast_medium]}>
|
<View>
|
||||||
|
<TextField.Label>
|
||||||
<Trans>Sign in as...</Trans>
|
<Trans>Sign in as...</Trans>
|
||||||
</Text>
|
</TextField.Label>
|
||||||
<Group>
|
<Group>
|
||||||
{accounts.map(account => (
|
{accounts.map(account => (
|
||||||
<AccountItem
|
<AccountItem
|
||||||
|
@ -171,7 +172,8 @@ export const ChooseAccountForm = ({
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</Group>
|
</Group>
|
||||||
<View style={[a.flex_row, a.mt_lg]}>
|
</View>
|
||||||
|
<View style={[a.flex_row]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Back`)}
|
label={_(msg`Back`)}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
@ -182,7 +184,6 @@ export const ChooseAccountForm = ({
|
||||||
</Button>
|
</Button>
|
||||||
<View style={[a.flex_1]} />
|
<View style={[a.flex_1]} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</FormContainer>
|
||||||
</ScrollView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -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,
|
||||||
|
},
|
||||||
|
})
|
|
@ -2,36 +2,29 @@ import React, {useState, useRef} from 'react'
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Keyboard,
|
Keyboard,
|
||||||
ScrollView,
|
|
||||||
TextInput,
|
TextInput,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
||||||
import {Trans, msg} from '@lingui/macro'
|
import {Trans, msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {createFullHandle} from 'lib/strings/handles'
|
import {createFullHandle} from 'lib/strings/handles'
|
||||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
|
||||||
import {isNetworkError} from 'lib/strings/errors'
|
import {isNetworkError} from 'lib/strings/errors'
|
||||||
import {useSessionApi} from '#/state/session'
|
import {useSessionApi} from '#/state/session'
|
||||||
import {cleanError} from 'lib/strings/errors'
|
import {cleanError} from 'lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
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 {Button, ButtonText} from '#/components/Button'
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At'
|
||||||
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
|
import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock'
|
||||||
import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe'
|
import {HostingProvider} from '#/components/forms/HostingProvider'
|
||||||
import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
|
import {FormContainer} from './FormContainer'
|
||||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
import {FormError} from './FormError'
|
||||||
|
|
||||||
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema
|
||||||
|
|
||||||
|
@ -64,14 +57,11 @@ export const LoginForm = ({
|
||||||
const passwordInputRef = useRef<TextInput>(null)
|
const passwordInputRef = useRef<TextInput>(null)
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {login} = useSessionApi()
|
const {login} = useSessionApi()
|
||||||
const serverInputControl = useDialogControl()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
|
|
||||||
const onPressSelectService = () => {
|
const onPressSelectService = React.useCallback(() => {
|
||||||
serverInputControl.open()
|
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
track('Signin:PressedSelectService')
|
track('Signin:PressedSelectService')
|
||||||
}
|
}, [track])
|
||||||
|
|
||||||
const onPressNext = async () => {
|
const onPressNext = async () => {
|
||||||
Keyboard.dismiss()
|
Keyboard.dismiss()
|
||||||
|
@ -131,46 +121,17 @@ export const LoginForm = ({
|
||||||
|
|
||||||
const isReady = !!serviceDescription && !!identifier && !!password
|
const isReady = !!serviceDescription && !!identifier && !!password
|
||||||
return (
|
return (
|
||||||
<ScrollView testID="loginForm" style={a.h_full}>
|
<FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}>
|
||||||
<View style={[a.gap_lg, !gtMobile && a.px_lg, a.flex_1]}>
|
|
||||||
<ServerInputDialog
|
|
||||||
control={serverInputControl}
|
|
||||||
onSelect={setServiceUrl}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<View>
|
<View>
|
||||||
<TextField.Label>
|
<TextField.Label>
|
||||||
<Trans>Hosting provider</Trans>
|
<Trans>Hosting provider</Trans>
|
||||||
</TextField.Label>
|
</TextField.Label>
|
||||||
<TouchableOpacity
|
<HostingProvider
|
||||||
accessibilityRole="button"
|
serviceUrl={serviceUrl}
|
||||||
style={[
|
onSelectServiceUrl={setServiceUrl}
|
||||||
a.w_full,
|
onOpenDialog={onPressSelectService}
|
||||||
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>
|
</View>
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
<View>
|
<View>
|
||||||
<TextField.Label>
|
<TextField.Label>
|
||||||
<Trans>Account</Trans>
|
<Trans>Account</Trans>
|
||||||
|
@ -245,14 +206,7 @@ export const LoginForm = ({
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</View>
|
</View>
|
||||||
{error ? (
|
<FormError error={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]}>
|
<View style={[a.flex_row, a.align_center]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Back`)}
|
label={_(msg`Back`)}
|
||||||
|
@ -260,9 +214,11 @@ export const LoginForm = ({
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
onPress={onPressBack}>
|
onPress={onPressBack}>
|
||||||
{_(msg`Back`)}
|
<ButtonText>
|
||||||
|
<Trans>Back</Trans>
|
||||||
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<View style={s.flex1} />
|
<View style={a.flex_1} />
|
||||||
{!serviceDescription && error ? (
|
{!serviceDescription && error ? (
|
||||||
<Button
|
<Button
|
||||||
testID="loginRetryButton"
|
testID="loginRetryButton"
|
||||||
|
@ -291,11 +247,12 @@ export const LoginForm = ({
|
||||||
color="primary"
|
color="primary"
|
||||||
size="small"
|
size="small"
|
||||||
onPress={onPressNext}>
|
onPress={onPressNext}>
|
||||||
{_(msg`Next`)}
|
<ButtonText>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</FormContainer>
|
||||||
</ScrollView>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -13,9 +13,9 @@ import {msg} from '@lingui/macro'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {ChooseAccountForm} from './ChooseAccountForm'
|
import {ChooseAccountForm} from './ChooseAccountForm'
|
||||||
import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm'
|
import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm'
|
||||||
import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm'
|
import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm'
|
||||||
import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm'
|
import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm'
|
||||||
import {LoginForm} from '#/screens/Login/LoginForm'
|
import {LoginForm} from '#/screens/Login/LoginForm'
|
||||||
|
|
||||||
enum Forms {
|
enum Forms {
|
||||||
|
|
|
@ -1,228 +0,0 @@
|
||||||
import React, {useState, useEffect} from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
Keyboard,
|
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {
|
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconStyle,
|
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {ComAtprotoServerDescribeServer} from '@atproto/api'
|
|
||||||
import * as EmailValidator from 'email-validator'
|
|
||||||
import {BskyAgent} from '@atproto/api'
|
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {toNiceDomain} from 'lib/strings/url-helpers'
|
|
||||||
import {isNetworkError} from 'lib/strings/errors'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import {styles} from './styles'
|
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
|
||||||
|
|
||||||
import {ServerInputDialog} from '../server-input'
|
|
||||||
|
|
||||||
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 pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [email, setEmail] = useState<string>('')
|
|
||||||
const {screen} = useAnalytics()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const serverInputControl = useDialogControl()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
screen('Signin:ForgotPassword')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
const onPressSelectService = React.useCallback(() => {
|
|
||||||
serverInputControl.open()
|
|
||||||
Keyboard.dismiss()
|
|
||||||
}, [serverInputControl])
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<ServerInputDialog
|
|
||||||
control={serverInputControl}
|
|
||||||
onSelect={setServiceUrl}
|
|
||||||
/>
|
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
|
||||||
<Trans>Reset password</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text type="md" style={[pal.text, styles.instructions]}>
|
|
||||||
<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
|
|
||||||
testID="forgotPasswordView"
|
|
||||||
style={[pal.borderDark, pal.view, styles.group]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="forgotPasswordSelectServiceButton"
|
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}
|
|
||||||
onPress={onPressSelectService}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Hosting provider`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Sets hosting provider for password reset`,
|
|
||||||
)}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="globe"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<Text style={[pal.text, styles.textInput]} numberOfLines={1}>
|
|
||||||
{toNiceDomain(serviceUrl)}
|
|
||||||
</Text>
|
|
||||||
<View style={[pal.btn, styles.textBtnFakeInnerBtn]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="pen"
|
|
||||||
size={12}
|
|
||||||
style={pal.text as FontAwesomeIconStyle}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="envelope"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="forgotPasswordEmail"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder={_(msg`Email address`)}
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoFocus
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
value={email}
|
|
||||||
onChangeText={setEmail}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessibilityLabel={_(msg`Email`)}
|
|
||||||
accessibilityHint={_(msg`Sets email for password reset`)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{error ? (
|
|
||||||
<View style={styles.error}>
|
|
||||||
<View style={styles.errorIcon}>
|
|
||||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
|
||||||
</View>
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
|
||||||
<Trans>Back</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{!serviceDescription || isProcessing ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : !email ? (
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
|
||||||
<Trans>Next</Trans>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="newPasswordButton"
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Go to next`)}
|
|
||||||
accessibilityHint={_(msg`Navigates to the next screen`)}>
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
<Trans>Next</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{!serviceDescription || isProcessing ? (
|
|
||||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
|
||||||
<Trans>Processing...</Trans>
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
s.flexRow,
|
|
||||||
s.alignCenter,
|
|
||||||
s.mt20,
|
|
||||||
s.mb20,
|
|
||||||
pal.border,
|
|
||||||
s.borderBottom1,
|
|
||||||
{alignSelf: 'center', width: '90%'},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<View style={[s.flexRow, s.justifyCenter]}>
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="skipSendEmailButton"
|
|
||||||
onPress={onEmailSent}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Go to next`)}
|
|
||||||
accessibilityHint={_(msg`Navigates to the next screen`)}>
|
|
||||||
<Text type="xl" style={[pal.link, s.pr5]}>
|
|
||||||
<Trans>Already have a code?</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
import React, {useEffect} from 'react'
|
|
||||||
import {TouchableOpacity, View} from 'react-native'
|
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {styles} from './styles'
|
|
||||||
import {msg, Trans} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
export const PasswordUpdatedForm = ({
|
|
||||||
onPressNext,
|
|
||||||
}: {
|
|
||||||
onPressNext: () => void
|
|
||||||
}) => {
|
|
||||||
const {screen} = useAnalytics()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
screen('Signin:PasswordUpdatedForm')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
|
||||||
<Trans>Password updated!</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
|
||||||
<Trans>You can now sign in with your new password.</Trans>
|
|
||||||
</Text>
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
<TouchableOpacity
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Close alert`)}
|
|
||||||
accessibilityHint={_(msg`Closes password update alert`)}>
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
<Trans>Okay</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,211 +0,0 @@
|
||||||
import React, {useState, useEffect} from 'react'
|
|
||||||
import {
|
|
||||||
ActivityIndicator,
|
|
||||||
TextInput,
|
|
||||||
TouchableOpacity,
|
|
||||||
View,
|
|
||||||
} from 'react-native'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {BskyAgent} from '@atproto/api'
|
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import {s} from 'lib/styles'
|
|
||||||
import {isNetworkError} from 'lib/strings/errors'
|
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
|
||||||
import {cleanError} from 'lib/strings/errors'
|
|
||||||
import {checkAndFormatResetCode} from 'lib/strings/password'
|
|
||||||
import {logger} from '#/logger'
|
|
||||||
import {styles} from './styles'
|
|
||||||
import {Trans, msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
export const SetNewPasswordForm = ({
|
|
||||||
error,
|
|
||||||
serviceUrl,
|
|
||||||
setError,
|
|
||||||
onPressBack,
|
|
||||||
onPasswordSet,
|
|
||||||
}: {
|
|
||||||
error: string
|
|
||||||
serviceUrl: string
|
|
||||||
setError: (v: string) => void
|
|
||||||
onPressBack: () => void
|
|
||||||
onPasswordSet: () => void
|
|
||||||
}) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const {screen} = useAnalytics()
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
screen('Signin:SetNewPasswordForm')
|
|
||||||
}, [screen])
|
|
||||||
|
|
||||||
const [isProcessing, setIsProcessing] = useState<boolean>(false)
|
|
||||||
const [resetCode, setResetCode] = useState<string>('')
|
|
||||||
const [password, setPassword] = useState<string>('')
|
|
||||||
|
|
||||||
const onPressNext = async () => {
|
|
||||||
// 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 (
|
|
||||||
<>
|
|
||||||
<View>
|
|
||||||
<Text type="title-lg" style={[pal.text, styles.screenTitle]}>
|
|
||||||
<Trans>Set new password</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text type="lg" style={[pal.text, styles.instructions]}>
|
|
||||||
<Trans>
|
|
||||||
You will receive an email with a "reset code." Enter that code here,
|
|
||||||
then enter your new password.
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
<View
|
|
||||||
testID="newPasswordView"
|
|
||||||
style={[pal.view, pal.borderDark, styles.group]}>
|
|
||||||
<View
|
|
||||||
style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="ticket"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="resetCodeInput"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder={_(msg`Reset code`)}
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
autoComplete="off"
|
|
||||||
value={resetCode}
|
|
||||||
onChangeText={setResetCode}
|
|
||||||
onFocus={() => setError('')}
|
|
||||||
onBlur={onBlur}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel={_(msg`Reset code`)}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Input code sent to your email for password reset`,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[pal.borderDark, styles.groupContent]}>
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="lock"
|
|
||||||
style={[pal.textLight, styles.groupContentIcon]}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
testID="newPasswordInput"
|
|
||||||
style={[pal.text, styles.textInput]}
|
|
||||||
placeholder={_(msg`New password`)}
|
|
||||||
placeholderTextColor={pal.colors.textLight}
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
autoComplete="new-password"
|
|
||||||
keyboardAppearance={theme.colorScheme}
|
|
||||||
secureTextEntry
|
|
||||||
value={password}
|
|
||||||
onChangeText={setPassword}
|
|
||||||
editable={!isProcessing}
|
|
||||||
accessible={true}
|
|
||||||
accessibilityLabel={_(msg`Password`)}
|
|
||||||
accessibilityHint={_(msg`Input new password`)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{error ? (
|
|
||||||
<View style={styles.error}>
|
|
||||||
<View style={styles.errorIcon}>
|
|
||||||
<FontAwesomeIcon icon="exclamation" style={s.white} size={10} />
|
|
||||||
</View>
|
|
||||||
<View style={s.flex1}>
|
|
||||||
<Text style={[s.white, s.bold]}>{error}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : undefined}
|
|
||||||
<View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}>
|
|
||||||
<TouchableOpacity onPress={onPressBack} accessibilityRole="button">
|
|
||||||
<Text type="xl" style={[pal.link, s.pl5]}>
|
|
||||||
<Trans>Back</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<View style={s.flex1} />
|
|
||||||
{isProcessing ? (
|
|
||||||
<ActivityIndicator />
|
|
||||||
) : !resetCode || !password ? (
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}>
|
|
||||||
<Trans>Next</Trans>
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID="setNewPasswordButton"
|
|
||||||
// Check the code before running the callback
|
|
||||||
onPress={onPressNext}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={_(msg`Go to next`)}
|
|
||||||
accessibilityHint={_(msg`Navigates to the next screen`)}>
|
|
||||||
<Text type="xl-bold" style={[pal.link, s.pr5]}>
|
|
||||||
<Trans>Next</Trans>
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
{isProcessing ? (
|
|
||||||
<Text type="xl" style={[pal.textLight, s.pl10]}>
|
|
||||||
<Trans>Updating...</Trans>
|
|
||||||
</Text>
|
|
||||||
) : undefined}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
import {StyleSheet} from 'react-native'
|
|
||||||
import {colors} from 'lib/styles'
|
|
||||||
import {isWeb} from '#/platform/detection'
|
|
||||||
|
|
||||||
export const styles = StyleSheet.create({
|
|
||||||
screenTitle: {
|
|
||||||
marginBottom: 10,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
instructions: {
|
|
||||||
marginBottom: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
group: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderRadius: 10,
|
|
||||||
marginBottom: 20,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
},
|
|
||||||
groupLabel: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingBottom: 5,
|
|
||||||
},
|
|
||||||
groupContent: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
noTopBorder: {
|
|
||||||
borderTopWidth: 0,
|
|
||||||
},
|
|
||||||
groupContentIcon: {
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
account: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingVertical: 4,
|
|
||||||
},
|
|
||||||
accountLast: {
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
marginBottom: 20,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
textInput: {
|
|
||||||
flex: 1,
|
|
||||||
width: '100%',
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
fontSize: 17,
|
|
||||||
letterSpacing: 0.25,
|
|
||||||
fontWeight: '400',
|
|
||||||
borderRadius: 10,
|
|
||||||
},
|
|
||||||
textInputInnerBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
marginHorizontal: 6,
|
|
||||||
},
|
|
||||||
textBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
flex: 1,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
textBtnLabel: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingHorizontal: 12,
|
|
||||||
},
|
|
||||||
textBtnFakeInnerBtn: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
borderRadius: 6,
|
|
||||||
paddingVertical: 6,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
marginHorizontal: 6,
|
|
||||||
},
|
|
||||||
accountText: {
|
|
||||||
flex: 1,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'baseline',
|
|
||||||
paddingVertical: 10,
|
|
||||||
},
|
|
||||||
accountTextOther: {
|
|
||||||
paddingLeft: 12,
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
backgroundColor: colors.red4,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginTop: -5,
|
|
||||||
marginHorizontal: 20,
|
|
||||||
marginBottom: 15,
|
|
||||||
borderRadius: 8,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
paddingVertical: 8,
|
|
||||||
},
|
|
||||||
errorIcon: {
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: colors.white,
|
|
||||||
color: colors.white,
|
|
||||||
borderRadius: 30,
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginRight: 5,
|
|
||||||
},
|
|
||||||
dimmed: {opacity: 0.5},
|
|
||||||
|
|
||||||
maxHeight: {
|
|
||||||
// @ts-ignore web only -prf
|
|
||||||
maxHeight: isWeb ? '100vh' : undefined,
|
|
||||||
height: !isWeb ? '100%' : undefined,
|
|
||||||
},
|
|
||||||
})
|
|
Loading…
Reference in New Issue