diff options
author | Samuel Newman <mozzius@protonmail.com> | 2024-03-15 13:49:13 +0000 |
---|---|---|
committer | Samuel Newman <mozzius@protonmail.com> | 2024-03-15 13:49:13 +0000 |
commit | a1fc95f30e8ff9f8903451b6f5d2daa318653167 (patch) | |
tree | 75c62e44b5771b42f183f45592d1c363e7197d04 | |
parent | f71ec52517fb32d0f3dd1a1a8aa1da1949a752d5 (diff) | |
download | voidsky-a1fc95f30e8ff9f8903451b6f5d2daa318653167.tar.zst |
convert password reset flow
-rw-r--r-- | assets/icons/ticket_stroke2_corner0_rounded.svg | 1 | ||||
-rw-r--r-- | src/alf/atoms.ts | 69 | ||||
-rw-r--r-- | src/components/forms/HostingProvider.tsx | 69 | ||||
-rw-r--r-- | src/components/icons/Ticket.tsx | 5 | ||||
-rw-r--r-- | src/screens/Login/ChooseAccountForm.tsx | 43 | ||||
-rw-r--r-- | src/screens/Login/ForgotPasswordForm.tsx | 183 | ||||
-rw-r--r-- | src/screens/Login/FormContainer.tsx | 52 | ||||
-rw-r--r-- | src/screens/Login/FormError.tsx | 34 | ||||
-rw-r--r-- | src/screens/Login/LoginForm.tsx | 297 | ||||
-rw-r--r-- | src/screens/Login/PasswordUpdatedForm.tsx | 49 | ||||
-rw-r--r-- | src/screens/Login/SetNewPasswordForm.tsx | 189 | ||||
-rw-r--r-- | src/screens/Login/index.tsx | 6 | ||||
-rw-r--r-- | src/view/com/auth/login/ForgotPasswordForm.tsx | 228 | ||||
-rw-r--r-- | src/view/com/auth/login/PasswordUpdatedForm.tsx | 48 | ||||
-rw-r--r-- | src/view/com/auth/login/SetNewPasswordForm.tsx | 211 | ||||
-rw-r--r-- | src/view/com/auth/login/styles.ts | 118 |
16 files changed, 803 insertions, 799 deletions
diff --git a/assets/icons/ticket_stroke2_corner0_rounded.svg b/assets/icons/ticket_stroke2_corner0_rounded.svg new file mode 100644 index 000000000..184addc8e --- /dev/null +++ b/assets/icons/ticket_stroke2_corner0_rounded.svg @@ -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> \ No newline at end of file diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index fef68ecab..33803b6fb 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -300,6 +300,9 @@ export const atoms = { /* * Padding */ + p_0: { + padding: 0, + }, p_2xs: { padding: tokens.space._2xs, }, @@ -330,6 +333,10 @@ export const atoms = { p_5xl: { padding: tokens.space._5xl, }, + px_0: { + paddingLeft: 0, + paddingRight: 0, + }, px_2xs: { paddingLeft: tokens.space._2xs, paddingRight: tokens.space._2xs, @@ -370,6 +377,10 @@ export const atoms = { paddingLeft: tokens.space._5xl, paddingRight: tokens.space._5xl, }, + py_0: { + paddingTop: 0, + paddingBottom: 0, + }, py_2xs: { paddingTop: tokens.space._2xs, paddingBottom: tokens.space._2xs, @@ -410,6 +421,9 @@ export const atoms = { paddingTop: tokens.space._5xl, paddingBottom: tokens.space._5xl, }, + pt_0: { + paddingTop: 0, + }, pt_2xs: { paddingTop: tokens.space._2xs, }, @@ -440,6 +454,9 @@ export const atoms = { pt_5xl: { paddingTop: tokens.space._5xl, }, + pb_0: { + paddingBottom: 0, + }, pb_2xs: { paddingBottom: tokens.space._2xs, }, @@ -470,6 +487,9 @@ export const atoms = { pb_5xl: { paddingBottom: tokens.space._5xl, }, + pl_0: { + paddingLeft: 0, + }, pl_2xs: { paddingLeft: tokens.space._2xs, }, @@ -500,6 +520,9 @@ export const atoms = { pl_5xl: { paddingLeft: tokens.space._5xl, }, + pr_0: { + paddingRight: 0, + }, pr_2xs: { paddingRight: tokens.space._2xs, }, @@ -534,6 +557,9 @@ export const atoms = { /* * Margin */ + m_0: { + margin: 0, + }, m_2xs: { margin: tokens.space._2xs, }, @@ -564,6 +590,13 @@ export const atoms = { m_5xl: { margin: tokens.space._5xl, }, + m_auto: { + margin: 'auto', + }, + mx_0: { + marginLeft: 0, + marginRight: 0, + }, mx_2xs: { marginLeft: tokens.space._2xs, marginRight: tokens.space._2xs, @@ -604,6 +637,14 @@ export const atoms = { marginLeft: tokens.space._5xl, marginRight: tokens.space._5xl, }, + mx_auto: { + marginLeft: 'auto', + marginRight: 'auto', + }, + my_0: { + marginTop: 0, + marginBottom: 0, + }, my_2xs: { marginTop: tokens.space._2xs, marginBottom: tokens.space._2xs, @@ -644,6 +685,13 @@ export const atoms = { marginTop: tokens.space._5xl, marginBottom: tokens.space._5xl, }, + my_auto: { + marginTop: 'auto', + marginBottom: 'auto', + }, + mt_0: { + marginTop: 0, + }, mt_2xs: { marginTop: tokens.space._2xs, }, @@ -674,6 +722,12 @@ export const atoms = { mt_5xl: { marginTop: tokens.space._5xl, }, + mt_auto: { + marginTop: 'auto', + }, + mb_0: { + marginBottom: 0, + }, mb_2xs: { marginBottom: tokens.space._2xs, }, @@ -704,6 +758,12 @@ export const atoms = { mb_5xl: { marginBottom: tokens.space._5xl, }, + mb_auto: { + marginBottom: 'auto', + }, + ml_0: { + marginLeft: 0, + }, ml_2xs: { marginLeft: tokens.space._2xs, }, @@ -734,6 +794,12 @@ export const atoms = { ml_5xl: { marginLeft: tokens.space._5xl, }, + ml_auto: { + marginLeft: 'auto', + }, + mr_0: { + marginRight: 0, + }, mr_2xs: { marginRight: tokens.space._2xs, }, @@ -764,4 +830,7 @@ export const atoms = { mr_5xl: { marginRight: tokens.space._5xl, }, + mr_auto: { + marginRight: 'auto', + }, } as const diff --git a/src/components/forms/HostingProvider.tsx b/src/components/forms/HostingProvider.tsx new file mode 100644 index 000000000..df506b77c --- /dev/null +++ b/src/components/forms/HostingProvider.tsx @@ -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> + </> + ) +} diff --git a/src/components/icons/Ticket.tsx b/src/components/icons/Ticket.tsx new file mode 100644 index 000000000..0df6b8120 --- /dev/null +++ b/src/components/icons/Ticket.tsx @@ -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', +}) diff --git a/src/screens/Login/ChooseAccountForm.tsx b/src/screens/Login/ChooseAccountForm.tsx index f5b3c2a86..7a3a4555b 100644 --- a/src/screens/Login/ChooseAccountForm.tsx +++ b/src/screens/Login/ChooseAccountForm.tsx @@ -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> ) } diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx new file mode 100644 index 000000000..fa674155a --- /dev/null +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -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> + ) +} diff --git a/src/screens/Login/FormContainer.tsx b/src/screens/Login/FormContainer.tsx new file mode 100644 index 000000000..a08aa05b0 --- /dev/null +++ b/src/screens/Login/FormContainer.tsx @@ -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, + }, +}) diff --git a/src/screens/Login/FormError.tsx b/src/screens/Login/FormError.tsx new file mode 100644 index 000000000..3c6a8649d --- /dev/null +++ b/src/screens/Login/FormError.tsx @@ -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, + }, +}) diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 3089b3887..580155281 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -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> ) } diff --git a/src/screens/Login/PasswordUpdatedForm.tsx b/src/screens/Login/PasswordUpdatedForm.tsx new file mode 100644 index 000000000..218cab539 --- /dev/null +++ b/src/screens/Login/PasswordUpdatedForm.tsx @@ -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> + ) +} diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx new file mode 100644 index 000000000..2685ad5ee --- /dev/null +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -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> + ) +} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 028a497d2..10edb3eb6 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -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 { diff --git a/src/view/com/auth/login/ForgotPasswordForm.tsx b/src/view/com/auth/login/ForgotPasswordForm.tsx deleted file mode 100644 index 322da2b8f..000000000 --- a/src/view/com/auth/login/ForgotPasswordForm.tsx +++ /dev/null @@ -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> - </> - ) -} diff --git a/src/view/com/auth/login/PasswordUpdatedForm.tsx b/src/view/com/auth/login/PasswordUpdatedForm.tsx deleted file mode 100644 index 71f750b14..000000000 --- a/src/view/com/auth/login/PasswordUpdatedForm.tsx +++ /dev/null @@ -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> - </> - ) -} diff --git a/src/view/com/auth/login/SetNewPasswordForm.tsx b/src/view/com/auth/login/SetNewPasswordForm.tsx deleted file mode 100644 index 6d1584c86..000000000 --- a/src/view/com/auth/login/SetNewPasswordForm.tsx +++ /dev/null @@ -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> - </> - ) -} diff --git a/src/view/com/auth/login/styles.ts b/src/view/com/auth/login/styles.ts deleted file mode 100644 index 9dccc2803..000000000 --- a/src/view/com/auth/login/styles.ts +++ /dev/null @@ -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, - }, -}) |