Bluesky app fork with some witchin' additions 💫

convert password reset flow

+803 -799
+1
assets/icons/ticket_stroke2_corner0_rounded.svg
··· 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>
+69
src/alf/atoms.ts
··· 300 300 /* 301 301 * Padding 302 302 */ 303 + p_0: { 304 + padding: 0, 305 + }, 303 306 p_2xs: { 304 307 padding: tokens.space._2xs, 305 308 }, ··· 329 332 }, 330 333 p_5xl: { 331 334 padding: tokens.space._5xl, 335 + }, 336 + px_0: { 337 + paddingLeft: 0, 338 + paddingRight: 0, 332 339 }, 333 340 px_2xs: { 334 341 paddingLeft: tokens.space._2xs, ··· 370 377 paddingLeft: tokens.space._5xl, 371 378 paddingRight: tokens.space._5xl, 372 379 }, 380 + py_0: { 381 + paddingTop: 0, 382 + paddingBottom: 0, 383 + }, 373 384 py_2xs: { 374 385 paddingTop: tokens.space._2xs, 375 386 paddingBottom: tokens.space._2xs, ··· 409 420 py_5xl: { 410 421 paddingTop: tokens.space._5xl, 411 422 paddingBottom: tokens.space._5xl, 423 + }, 424 + pt_0: { 425 + paddingTop: 0, 412 426 }, 413 427 pt_2xs: { 414 428 paddingTop: tokens.space._2xs, ··· 440 454 pt_5xl: { 441 455 paddingTop: tokens.space._5xl, 442 456 }, 457 + pb_0: { 458 + paddingBottom: 0, 459 + }, 443 460 pb_2xs: { 444 461 paddingBottom: tokens.space._2xs, 445 462 }, ··· 470 487 pb_5xl: { 471 488 paddingBottom: tokens.space._5xl, 472 489 }, 490 + pl_0: { 491 + paddingLeft: 0, 492 + }, 473 493 pl_2xs: { 474 494 paddingLeft: tokens.space._2xs, 475 495 }, ··· 499 519 }, 500 520 pl_5xl: { 501 521 paddingLeft: tokens.space._5xl, 522 + }, 523 + pr_0: { 524 + paddingRight: 0, 502 525 }, 503 526 pr_2xs: { 504 527 paddingRight: tokens.space._2xs, ··· 534 557 /* 535 558 * Margin 536 559 */ 560 + m_0: { 561 + margin: 0, 562 + }, 537 563 m_2xs: { 538 564 margin: tokens.space._2xs, 539 565 }, ··· 564 590 m_5xl: { 565 591 margin: tokens.space._5xl, 566 592 }, 593 + m_auto: { 594 + margin: 'auto', 595 + }, 596 + mx_0: { 597 + marginLeft: 0, 598 + marginRight: 0, 599 + }, 567 600 mx_2xs: { 568 601 marginLeft: tokens.space._2xs, 569 602 marginRight: tokens.space._2xs, ··· 604 637 marginLeft: tokens.space._5xl, 605 638 marginRight: tokens.space._5xl, 606 639 }, 640 + mx_auto: { 641 + marginLeft: 'auto', 642 + marginRight: 'auto', 643 + }, 644 + my_0: { 645 + marginTop: 0, 646 + marginBottom: 0, 647 + }, 607 648 my_2xs: { 608 649 marginTop: tokens.space._2xs, 609 650 marginBottom: tokens.space._2xs, ··· 644 685 marginTop: tokens.space._5xl, 645 686 marginBottom: tokens.space._5xl, 646 687 }, 688 + my_auto: { 689 + marginTop: 'auto', 690 + marginBottom: 'auto', 691 + }, 692 + mt_0: { 693 + marginTop: 0, 694 + }, 647 695 mt_2xs: { 648 696 marginTop: tokens.space._2xs, 649 697 }, ··· 674 722 mt_5xl: { 675 723 marginTop: tokens.space._5xl, 676 724 }, 725 + mt_auto: { 726 + marginTop: 'auto', 727 + }, 728 + mb_0: { 729 + marginBottom: 0, 730 + }, 677 731 mb_2xs: { 678 732 marginBottom: tokens.space._2xs, 679 733 }, ··· 704 758 mb_5xl: { 705 759 marginBottom: tokens.space._5xl, 706 760 }, 761 + mb_auto: { 762 + marginBottom: 'auto', 763 + }, 764 + ml_0: { 765 + marginLeft: 0, 766 + }, 707 767 ml_2xs: { 708 768 marginLeft: tokens.space._2xs, 709 769 }, ··· 734 794 ml_5xl: { 735 795 marginLeft: tokens.space._5xl, 736 796 }, 797 + ml_auto: { 798 + marginLeft: 'auto', 799 + }, 800 + mr_0: { 801 + marginRight: 0, 802 + }, 737 803 mr_2xs: { 738 804 marginRight: tokens.space._2xs, 739 805 }, ··· 763 829 }, 764 830 mr_5xl: { 765 831 marginRight: tokens.space._5xl, 832 + }, 833 + mr_auto: { 834 + marginRight: 'auto', 766 835 }, 767 836 } as const
+69
src/components/forms/HostingProvider.tsx
··· 1 + import React from 'react' 2 + import {TouchableOpacity, View} from 'react-native' 3 + 4 + import {isAndroid} from '#/platform/detection' 5 + import {atoms as a, useTheme} from '#/alf' 6 + import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 7 + import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' 8 + import * as TextField from './TextField' 9 + import {useDialogControl} from '../Dialog' 10 + import {Text} from '../Typography' 11 + import {ServerInputDialog} from '#/view/com/auth/server-input' 12 + import {toNiceDomain} from '#/lib/strings/url-helpers' 13 + 14 + export function HostingProvider({ 15 + serviceUrl, 16 + onSelectServiceUrl, 17 + onOpenDialog, 18 + }: { 19 + serviceUrl: string 20 + onSelectServiceUrl: (provider: string) => void 21 + onOpenDialog?: () => void 22 + }) { 23 + const serverInputControl = useDialogControl() 24 + const t = useTheme() 25 + 26 + const onPressSelectService = React.useCallback(() => { 27 + serverInputControl.open() 28 + if (onOpenDialog) { 29 + onOpenDialog() 30 + } 31 + }, [onOpenDialog, serverInputControl]) 32 + 33 + return ( 34 + <> 35 + <ServerInputDialog 36 + control={serverInputControl} 37 + onSelect={onSelectServiceUrl} 38 + /> 39 + <TouchableOpacity 40 + accessibilityRole="button" 41 + style={[ 42 + a.w_full, 43 + a.flex_row, 44 + a.align_center, 45 + a.rounded_sm, 46 + a.px_md, 47 + a.gap_xs, 48 + {paddingVertical: isAndroid ? 14 : 9}, 49 + t.atoms.bg_contrast_25, 50 + ]} 51 + onPress={onPressSelectService}> 52 + <TextField.Icon icon={Globe} /> 53 + <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text> 54 + <View 55 + style={[ 56 + a.rounded_sm, 57 + t.atoms.bg_contrast_100, 58 + {marginLeft: 'auto', left: 6, padding: 6}, 59 + ]}> 60 + <Pencil 61 + style={{color: t.palette.contrast_500}} 62 + height={18} 63 + width={18} 64 + /> 65 + </View> 66 + </TouchableOpacity> 67 + </> 68 + ) 69 + }
+5
src/components/icons/Ticket.tsx
··· 1 + import {createSinglePathSVG} from './TEMPLATE' 2 + 3 + export const Ticket_Stroke2_Corner0_Rounded = createSinglePathSVG({ 4 + 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', 5 + })
+22 -21
src/screens/Login/ChooseAccountForm.tsx
··· 1 1 import React from 'react' 2 - import {ScrollView, TouchableOpacity, View} from 'react-native' 2 + import {TouchableOpacity, View} from 'react-native' 3 3 import {Trans, msg} from '@lingui/macro' 4 4 import {useLingui} from '@lingui/react' 5 5 import flattenReactChildren from 'react-keyed-flatten-children' ··· 7 7 import {useAnalytics} from 'lib/analytics/analytics' 8 8 import {UserAvatar} from '../../view/com/util/UserAvatar' 9 9 import {colors} from 'lib/styles' 10 - import {styles} from '../../view/com/auth/login/styles' 11 10 import {useSession, useSessionApi, SessionAccount} from '#/state/session' 12 11 import {useProfileQuery} from '#/state/queries/profile' 13 12 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 14 13 import * as Toast from '#/view/com/util/Toast' 15 14 import {Button} from '#/components/Button' 16 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 15 + import {atoms as a, useTheme} from '#/alf' 17 16 import {Text} from '#/components/Typography' 18 17 import {ChevronRight_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' 19 18 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' 19 + import * as TextField from '#/components/forms/TextField' 20 + import {FormContainer} from './FormContainer' 20 21 21 22 function Group({children}: {children: React.ReactNode}) { 22 23 const t = useTheme() ··· 106 107 const {accounts, currentAccount} = useSession() 107 108 const {initSession} = useSessionApi() 108 109 const {setShowLoggedOut} = useLoggedOutViewControls() 109 - const {gtMobile} = useBreakpoints() 110 110 111 111 React.useEffect(() => { 112 112 screen('Choose Account') ··· 133 133 ) 134 134 135 135 return ( 136 - <ScrollView testID="chooseAccountForm" style={styles.maxHeight}> 137 - <View style={!gtMobile && a.px_lg}> 138 - <Text 139 - style={[a.mt_md, a.mb_lg, a.font_bold, t.atoms.text_contrast_medium]}> 136 + <FormContainer 137 + testID="chooseAccountForm" 138 + title={<Trans>Select account</Trans>}> 139 + <View> 140 + <TextField.Label> 140 141 <Trans>Sign in as...</Trans> 141 - </Text> 142 + </TextField.Label> 142 143 <Group> 143 144 {accounts.map(account => ( 144 145 <AccountItem ··· 171 172 </View> 172 173 </TouchableOpacity> 173 174 </Group> 174 - <View style={[a.flex_row, a.mt_lg]}> 175 - <Button 176 - label={_(msg`Back`)} 177 - variant="solid" 178 - color="secondary" 179 - size="small" 180 - onPress={onPressBack}> 181 - {_(msg`Back`)} 182 - </Button> 183 - <View style={[a.flex_1]} /> 184 - </View> 175 + </View> 176 + <View style={[a.flex_row]}> 177 + <Button 178 + label={_(msg`Back`)} 179 + variant="solid" 180 + color="secondary" 181 + size="small" 182 + onPress={onPressBack}> 183 + {_(msg`Back`)} 184 + </Button> 185 + <View style={[a.flex_1]} /> 185 186 </View> 186 - </ScrollView> 187 + </FormContainer> 187 188 ) 188 189 }
+183
src/screens/Login/ForgotPasswordForm.tsx
··· 1 + import React, {useState, useEffect} from 'react' 2 + import {ActivityIndicator, Keyboard, View} from 'react-native' 3 + import {ComAtprotoServerDescribeServer} from '@atproto/api' 4 + import * as EmailValidator from 'email-validator' 5 + import {BskyAgent} from '@atproto/api' 6 + import {Trans, msg} from '@lingui/macro' 7 + import {useLingui} from '@lingui/react' 8 + 9 + import * as TextField from '#/components/forms/TextField' 10 + import {HostingProvider} from '#/components/forms/HostingProvider' 11 + import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' 12 + import {atoms as a, useTheme} from '#/alf' 13 + import {useAnalytics} from 'lib/analytics/analytics' 14 + import {isNetworkError} from 'lib/strings/errors' 15 + import {cleanError} from 'lib/strings/errors' 16 + import {logger} from '#/logger' 17 + import {Button, ButtonText} from '#/components/Button' 18 + import {Text} from '#/components/Typography' 19 + import {FormContainer} from './FormContainer' 20 + import {FormError} from './FormError' 21 + 22 + type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 23 + 24 + export const ForgotPasswordForm = ({ 25 + error, 26 + serviceUrl, 27 + serviceDescription, 28 + setError, 29 + setServiceUrl, 30 + onPressBack, 31 + onEmailSent, 32 + }: { 33 + error: string 34 + serviceUrl: string 35 + serviceDescription: ServiceDescription | undefined 36 + setError: (v: string) => void 37 + setServiceUrl: (v: string) => void 38 + onPressBack: () => void 39 + onEmailSent: () => void 40 + }) => { 41 + const t = useTheme() 42 + const [isProcessing, setIsProcessing] = useState<boolean>(false) 43 + const [email, setEmail] = useState<string>('') 44 + const {screen} = useAnalytics() 45 + const {_} = useLingui() 46 + 47 + useEffect(() => { 48 + screen('Signin:ForgotPassword') 49 + }, [screen]) 50 + 51 + const onPressSelectService = React.useCallback(() => { 52 + Keyboard.dismiss() 53 + }, []) 54 + 55 + const onPressNext = async () => { 56 + if (!EmailValidator.validate(email)) { 57 + return setError(_(msg`Your email appears to be invalid.`)) 58 + } 59 + 60 + setError('') 61 + setIsProcessing(true) 62 + 63 + try { 64 + const agent = new BskyAgent({service: serviceUrl}) 65 + await agent.com.atproto.server.requestPasswordReset({email}) 66 + onEmailSent() 67 + } catch (e: any) { 68 + const errMsg = e.toString() 69 + logger.warn('Failed to request password reset', {error: e}) 70 + setIsProcessing(false) 71 + if (isNetworkError(e)) { 72 + setError( 73 + _( 74 + msg`Unable to contact your service. Please check your Internet connection.`, 75 + ), 76 + ) 77 + } else { 78 + setError(cleanError(errMsg)) 79 + } 80 + } 81 + } 82 + 83 + return ( 84 + <FormContainer 85 + testID="forgotPasswordForm" 86 + title={<Trans>Reset password</Trans>}> 87 + <View> 88 + <TextField.Label> 89 + <Trans>Hosting provider</Trans> 90 + </TextField.Label> 91 + <HostingProvider 92 + serviceUrl={serviceUrl} 93 + onSelectServiceUrl={setServiceUrl} 94 + onOpenDialog={onPressSelectService} 95 + /> 96 + </View> 97 + <View> 98 + <TextField.Label> 99 + <Trans>Email address</Trans> 100 + </TextField.Label> 101 + <TextField.Root> 102 + <TextField.Icon icon={At} /> 103 + <TextField.Input 104 + testID="forgotPasswordEmail" 105 + label={_(msg`Enter your email address`)} 106 + autoCapitalize="none" 107 + autoFocus 108 + autoCorrect={false} 109 + autoComplete="email" 110 + value={email} 111 + onChangeText={setEmail} 112 + editable={!isProcessing} 113 + accessibilityHint={_(msg`Sets email for password reset`)} 114 + /> 115 + </TextField.Root> 116 + </View> 117 + <View> 118 + <Text style={[t.atoms.text_contrast_high, a.mb_md]}> 119 + <Trans> 120 + Enter the email you used to create your account. We'll send you a 121 + "reset code" so you can set a new password. 122 + </Trans> 123 + </Text> 124 + </View> 125 + <FormError error={error} /> 126 + <View style={[a.flex_row, a.align_center]}> 127 + <Button 128 + label={_(msg`Back`)} 129 + variant="solid" 130 + color="secondary" 131 + size="small" 132 + onPress={onPressBack}> 133 + <ButtonText> 134 + <Trans>Back</Trans> 135 + </ButtonText> 136 + </Button> 137 + <View style={a.flex_1} /> 138 + {!serviceDescription || isProcessing ? ( 139 + <ActivityIndicator /> 140 + ) : ( 141 + <Button 142 + label={_(msg`Next`)} 143 + variant="solid" 144 + color={email ? 'primary' : 'secondary'} 145 + size="small" 146 + onPress={onPressNext} 147 + disabled={!email}> 148 + <ButtonText> 149 + <Trans>Next</Trans> 150 + </ButtonText> 151 + </Button> 152 + )} 153 + {!serviceDescription || isProcessing ? ( 154 + <Text style={[t.atoms.text_contrast_high, a.pl_md]}> 155 + <Trans>Processing...</Trans> 156 + </Text> 157 + ) : undefined} 158 + </View> 159 + <View 160 + style={[ 161 + t.atoms.border_contrast_medium, 162 + a.border_t, 163 + a.pt_2xl, 164 + a.mt_md, 165 + a.flex_row, 166 + a.justify_center, 167 + ]}> 168 + <Button 169 + testID="skipSendEmailButton" 170 + onPress={onEmailSent} 171 + label={_(msg`Go to next`)} 172 + accessibilityHint={_(msg`Navigates to the next screen`)} 173 + size="small" 174 + variant="ghost" 175 + color="secondary"> 176 + <ButtonText> 177 + <Trans>Already have a code?</Trans> 178 + </ButtonText> 179 + </Button> 180 + </View> 181 + </FormContainer> 182 + ) 183 + }
+52
src/screens/Login/FormContainer.tsx
··· 1 + import React from 'react' 2 + import { 3 + ScrollView, 4 + StyleSheet, 5 + View, 6 + type StyleProp, 7 + type ViewStyle, 8 + } from 'react-native' 9 + 10 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 11 + import {Text} from '#/components/Typography' 12 + import {isWeb} from '#/platform/detection' 13 + 14 + export function FormContainer({ 15 + testID, 16 + title, 17 + children, 18 + style, 19 + contentContainerStyle, 20 + }: { 21 + testID?: string 22 + title?: React.ReactNode 23 + children: React.ReactNode 24 + style?: StyleProp<ViewStyle> 25 + contentContainerStyle?: StyleProp<ViewStyle> 26 + }) { 27 + const {gtMobile} = useBreakpoints() 28 + const t = useTheme() 29 + return ( 30 + <ScrollView 31 + testID={testID} 32 + style={[styles.maxHeight, contentContainerStyle]}> 33 + <View 34 + style={[a.gap_lg, a.flex_1, !gtMobile && [a.px_lg, a.pt_md], style]}> 35 + {title && !gtMobile && ( 36 + <Text style={[a.text_xl, a.font_bold, t.atoms.text_contrast_high]}> 37 + {title} 38 + </Text> 39 + )} 40 + {children} 41 + </View> 42 + </ScrollView> 43 + ) 44 + } 45 + 46 + const styles = StyleSheet.create({ 47 + maxHeight: { 48 + // @ts-ignore web only -prf 49 + maxHeight: isWeb ? '100vh' : undefined, 50 + height: !isWeb ? '100%' : undefined, 51 + }, 52 + })
+34
src/screens/Login/FormError.tsx
··· 1 + import React from 'react' 2 + import {StyleSheet, View} from 'react-native' 3 + 4 + import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' 5 + import {Text} from '#/components/Typography' 6 + import {atoms as a, useTheme} from '#/alf' 7 + import {colors} from '#/lib/styles' 8 + 9 + export function FormError({error}: {error?: string}) { 10 + const t = useTheme() 11 + 12 + if (!error) return null 13 + 14 + return ( 15 + <View style={styles.error}> 16 + <Warning fill={t.palette.white} size="sm" /> 17 + <View style={(a.flex_1, a.ml_sm)}> 18 + <Text style={[{color: t.palette.white}, a.font_bold]}>{error}</Text> 19 + </View> 20 + </View> 21 + ) 22 + } 23 + 24 + const styles = StyleSheet.create({ 25 + error: { 26 + backgroundColor: colors.red4, 27 + flexDirection: 'row', 28 + alignItems: 'center', 29 + marginBottom: 15, 30 + borderRadius: 8, 31 + paddingHorizontal: 8, 32 + paddingVertical: 8, 33 + }, 34 + })
+127 -170
src/screens/Login/LoginForm.tsx
··· 2 2 import { 3 3 ActivityIndicator, 4 4 Keyboard, 5 - ScrollView, 6 5 TextInput, 7 6 TouchableOpacity, 8 7 View, 9 8 } from 'react-native' 10 9 import {ComAtprotoServerDescribeServer} from '@atproto/api' 11 10 import {Trans, msg} from '@lingui/macro' 11 + import {useLingui} from '@lingui/react' 12 12 13 13 import {useAnalytics} from 'lib/analytics/analytics' 14 - import {s} from 'lib/styles' 15 14 import {createFullHandle} from 'lib/strings/handles' 16 - import {toNiceDomain} from 'lib/strings/url-helpers' 17 15 import {isNetworkError} from 'lib/strings/errors' 18 16 import {useSessionApi} from '#/state/session' 19 17 import {cleanError} from 'lib/strings/errors' 20 18 import {logger} from '#/logger' 21 - import {styles} from '../../view/com/auth/login/styles' 22 - import {useLingui} from '@lingui/react' 23 - import {useDialogControl} from '#/components/Dialog' 24 - import {ServerInputDialog} from '../../view/com/auth/server-input' 25 19 import {Button, ButtonText} from '#/components/Button' 26 - import {isAndroid} from '#/platform/detection' 27 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 20 + import {atoms as a, useTheme} from '#/alf' 28 21 import {Text} from '#/components/Typography' 29 22 import * as TextField from '#/components/forms/TextField' 30 23 import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' 31 24 import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 32 - import {Globe_Stroke2_Corner0_Rounded as Globe} from '#/components/icons/Globe' 33 - import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil' 34 - import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' 25 + import {HostingProvider} from '#/components/forms/HostingProvider' 26 + import {FormContainer} from './FormContainer' 27 + import {FormError} from './FormError' 35 28 36 29 type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 37 30 ··· 64 57 const passwordInputRef = useRef<TextInput>(null) 65 58 const {_} = useLingui() 66 59 const {login} = useSessionApi() 67 - const serverInputControl = useDialogControl() 68 - const {gtMobile} = useBreakpoints() 69 60 70 - const onPressSelectService = () => { 71 - serverInputControl.open() 61 + const onPressSelectService = React.useCallback(() => { 72 62 Keyboard.dismiss() 73 63 track('Signin:PressedSelectService') 74 - } 64 + }, [track]) 75 65 76 66 const onPressNext = async () => { 77 67 Keyboard.dismiss() ··· 131 121 132 122 const isReady = !!serviceDescription && !!identifier && !!password 133 123 return ( 134 - <ScrollView testID="loginForm" style={a.h_full}> 135 - <View style={[a.gap_lg, !gtMobile && a.px_lg, a.flex_1]}> 136 - <ServerInputDialog 137 - control={serverInputControl} 138 - onSelect={setServiceUrl} 124 + <FormContainer testID="loginForm" title={<Trans>Sign in</Trans>}> 125 + <View> 126 + <TextField.Label> 127 + <Trans>Hosting provider</Trans> 128 + </TextField.Label> 129 + <HostingProvider 130 + serviceUrl={serviceUrl} 131 + onSelectServiceUrl={setServiceUrl} 132 + onOpenDialog={onPressSelectService} 139 133 /> 140 - 141 - <View> 142 - <TextField.Label> 143 - <Trans>Hosting provider</Trans> 144 - </TextField.Label> 134 + </View> 135 + <View> 136 + <TextField.Label> 137 + <Trans>Account</Trans> 138 + </TextField.Label> 139 + <TextField.Root> 140 + <TextField.Icon icon={At} /> 141 + <TextField.Input 142 + testID="loginUsernameInput" 143 + label={_(msg`Username or email address`)} 144 + autoCapitalize="none" 145 + autoFocus 146 + autoCorrect={false} 147 + autoComplete="username" 148 + returnKeyType="next" 149 + textContentType="username" 150 + onSubmitEditing={() => { 151 + passwordInputRef.current?.focus() 152 + }} 153 + blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 154 + value={identifier} 155 + onChangeText={str => 156 + setIdentifier((str || '').toLowerCase().trim()) 157 + } 158 + editable={!isProcessing} 159 + accessibilityHint={_( 160 + msg`Input the username or email address you used at signup`, 161 + )} 162 + /> 163 + </TextField.Root> 164 + </View> 165 + <View> 166 + <TextField.Root> 167 + <TextField.Icon icon={Lock} /> 168 + <TextField.Input 169 + testID="loginPasswordInput" 170 + inputRef={passwordInputRef} 171 + label={_(msg`Password`)} 172 + autoCapitalize="none" 173 + autoCorrect={false} 174 + autoComplete="password" 175 + returnKeyType="done" 176 + enablesReturnKeyAutomatically={true} 177 + secureTextEntry={true} 178 + textContentType="password" 179 + clearButtonMode="while-editing" 180 + value={password} 181 + onChangeText={setPassword} 182 + onSubmitEditing={onPressNext} 183 + blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing 184 + editable={!isProcessing} 185 + accessibilityHint={ 186 + identifier === '' 187 + ? _(msg`Input your password`) 188 + : _(msg`Input the password tied to ${identifier}`) 189 + } 190 + /> 145 191 <TouchableOpacity 192 + testID="forgotPasswordButton" 193 + onPress={onPressForgotPassword} 146 194 accessibilityRole="button" 195 + accessibilityLabel={_(msg`Forgot password`)} 196 + accessibilityHint={_(msg`Opens password reset form`)} 147 197 style={[ 148 - a.w_full, 149 - a.flex_row, 150 - a.align_center, 151 198 a.rounded_sm, 152 - a.px_md, 153 - a.gap_xs, 154 - {paddingVertical: isAndroid ? 14 : 9}, 155 - t.atoms.bg_contrast_25, 156 - ]} 157 - onPress={onPressSelectService}> 158 - <TextField.Icon icon={Globe} /> 159 - <Text style={[a.text_md]}>{toNiceDomain(serviceUrl)}</Text> 160 - <View 161 - style={[ 162 - a.rounded_sm, 163 - t.atoms.bg_contrast_100, 164 - {marginLeft: 'auto', left: 6, padding: 6}, 165 - ]}> 166 - <Pencil 167 - style={{color: t.palette.contrast_500}} 168 - height={18} 169 - width={18} 170 - /> 171 - </View> 199 + t.atoms.bg_contrast_100, 200 + {marginLeft: 'auto', left: 6, padding: 6}, 201 + a.z_10, 202 + ]}> 203 + <ButtonText style={t.atoms.text_contrast_medium}> 204 + <Trans>Forgot?</Trans> 205 + </ButtonText> 172 206 </TouchableOpacity> 173 - </View> 174 - <View> 175 - <TextField.Label> 176 - <Trans>Account</Trans> 177 - </TextField.Label> 178 - <TextField.Root> 179 - <TextField.Icon icon={At} /> 180 - <TextField.Input 181 - testID="loginUsernameInput" 182 - label={_(msg`Username or email address`)} 183 - autoCapitalize="none" 184 - autoFocus 185 - autoCorrect={false} 186 - autoComplete="username" 187 - returnKeyType="next" 188 - textContentType="username" 189 - onSubmitEditing={() => { 190 - passwordInputRef.current?.focus() 191 - }} 192 - blurOnSubmit={false} // prevents flickering due to onSubmitEditing going to next field 193 - value={identifier} 194 - onChangeText={str => 195 - setIdentifier((str || '').toLowerCase().trim()) 196 - } 197 - editable={!isProcessing} 198 - accessibilityHint={_( 199 - msg`Input the username or email address you used at signup`, 200 - )} 201 - /> 202 - </TextField.Root> 203 - </View> 204 - <View> 205 - <TextField.Root> 206 - <TextField.Icon icon={Lock} /> 207 - <TextField.Input 208 - testID="loginPasswordInput" 209 - inputRef={passwordInputRef} 210 - label={_(msg`Password`)} 211 - autoCapitalize="none" 212 - autoCorrect={false} 213 - autoComplete="password" 214 - returnKeyType="done" 215 - enablesReturnKeyAutomatically={true} 216 - secureTextEntry={true} 217 - textContentType="password" 218 - clearButtonMode="while-editing" 219 - value={password} 220 - onChangeText={setPassword} 221 - onSubmitEditing={onPressNext} 222 - blurOnSubmit={false} // HACK: https://github.com/facebook/react-native/issues/21911#issuecomment-558343069 Keyboard blur behavior is now handled in onSubmitEditing 223 - editable={!isProcessing} 224 - accessibilityHint={ 225 - identifier === '' 226 - ? _(msg`Input your password`) 227 - : _(msg`Input the password tied to ${identifier}`) 228 - } 229 - /> 230 - <TouchableOpacity 231 - testID="forgotPasswordButton" 232 - onPress={onPressForgotPassword} 233 - accessibilityRole="button" 234 - accessibilityLabel={_(msg`Forgot password`)} 235 - accessibilityHint={_(msg`Opens password reset form`)} 236 - style={[ 237 - a.rounded_sm, 238 - t.atoms.bg_contrast_100, 239 - {marginLeft: 'auto', left: 6, padding: 6}, 240 - a.z_10, 241 - ]}> 242 - <ButtonText style={t.atoms.text_contrast_medium}> 243 - <Trans>Forgot?</Trans> 244 - </ButtonText> 245 - </TouchableOpacity> 246 - </TextField.Root> 247 - </View> 248 - {error ? ( 249 - <View style={[styles.error, {marginHorizontal: 0}]}> 250 - <Warning style={s.white} size="sm" /> 251 - <View style={(a.flex_1, a.ml_sm)}> 252 - <Text style={[s.white, s.bold]}>{error}</Text> 253 - </View> 254 - </View> 255 - ) : undefined} 256 - <View style={[a.flex_row, a.align_center]}> 207 + </TextField.Root> 208 + </View> 209 + <FormError error={error} /> 210 + <View style={[a.flex_row, a.align_center]}> 211 + <Button 212 + label={_(msg`Back`)} 213 + variant="solid" 214 + color="secondary" 215 + size="small" 216 + onPress={onPressBack}> 217 + <ButtonText> 218 + <Trans>Back</Trans> 219 + </ButtonText> 220 + </Button> 221 + <View style={a.flex_1} /> 222 + {!serviceDescription && error ? ( 257 223 <Button 258 - label={_(msg`Back`)} 224 + testID="loginRetryButton" 225 + label={_(msg`Retry`)} 226 + accessibilityHint={_(msg`Retries login`)} 259 227 variant="solid" 260 228 color="secondary" 261 229 size="small" 262 - onPress={onPressBack}> 263 - {_(msg`Back`)} 230 + onPress={onPressRetryConnect}> 231 + {_(msg`Retry`)} 264 232 </Button> 265 - <View style={s.flex1} /> 266 - {!serviceDescription && error ? ( 267 - <Button 268 - testID="loginRetryButton" 269 - label={_(msg`Retry`)} 270 - accessibilityHint={_(msg`Retries login`)} 271 - variant="solid" 272 - color="secondary" 273 - size="small" 274 - onPress={onPressRetryConnect}> 275 - {_(msg`Retry`)} 276 - </Button> 277 - ) : !serviceDescription ? ( 278 - <> 279 - <ActivityIndicator /> 280 - <Text style={[t.atoms.text_contrast_high, a.pl_md]}> 281 - <Trans>Connecting...</Trans> 282 - </Text> 283 - </> 284 - ) : isProcessing ? ( 233 + ) : !serviceDescription ? ( 234 + <> 285 235 <ActivityIndicator /> 286 - ) : isReady ? ( 287 - <Button 288 - label={_(msg`Next`)} 289 - accessibilityHint={_(msg`Navigates to the next screen`)} 290 - variant="solid" 291 - color="primary" 292 - size="small" 293 - onPress={onPressNext}> 294 - {_(msg`Next`)} 295 - </Button> 296 - ) : undefined} 297 - </View> 236 + <Text style={[t.atoms.text_contrast_high, a.pl_md]}> 237 + <Trans>Connecting...</Trans> 238 + </Text> 239 + </> 240 + ) : isProcessing ? ( 241 + <ActivityIndicator /> 242 + ) : isReady ? ( 243 + <Button 244 + label={_(msg`Next`)} 245 + accessibilityHint={_(msg`Navigates to the next screen`)} 246 + variant="solid" 247 + color="primary" 248 + size="small" 249 + onPress={onPressNext}> 250 + <ButtonText> 251 + <Trans>Next</Trans> 252 + </ButtonText> 253 + </Button> 254 + ) : undefined} 298 255 </View> 299 - </ScrollView> 256 + </FormContainer> 300 257 ) 301 258 }
+49
src/screens/Login/PasswordUpdatedForm.tsx
··· 1 + import React, {useEffect} from 'react' 2 + import {View} from 'react-native' 3 + import {useAnalytics} from 'lib/analytics/analytics' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + import {FormContainer} from './FormContainer' 7 + import {Button, ButtonText} from '#/components/Button' 8 + import {Text} from '#/components/Typography' 9 + import {atoms as a, useBreakpoints} from '#/alf' 10 + 11 + export const PasswordUpdatedForm = ({ 12 + onPressNext, 13 + }: { 14 + onPressNext: () => void 15 + }) => { 16 + const {screen} = useAnalytics() 17 + const {_} = useLingui() 18 + const {gtMobile} = useBreakpoints() 19 + 20 + useEffect(() => { 21 + screen('Signin:PasswordUpdatedForm') 22 + }, [screen]) 23 + 24 + return ( 25 + <FormContainer 26 + testID="passwordUpdatedForm" 27 + style={[a.gap_2xl, !gtMobile && a.mt_5xl]}> 28 + <Text style={[a.text_3xl, a.font_bold, a.text_center]}> 29 + <Trans>Password updated!</Trans> 30 + </Text> 31 + <Text style={[a.text_center, a.mx_auto, {maxWidth: '80%'}]}> 32 + <Trans>You can now sign in with your new password.</Trans> 33 + </Text> 34 + <View style={[a.flex_row, a.justify_center]}> 35 + <Button 36 + onPress={onPressNext} 37 + label={_(msg`Close alert`)} 38 + accessibilityHint={_(msg`Closes password update alert`)} 39 + variant="solid" 40 + color="primary" 41 + size="medium"> 42 + <ButtonText> 43 + <Trans>Okay</Trans> 44 + </ButtonText> 45 + </Button> 46 + </View> 47 + </FormContainer> 48 + ) 49 + }
+189
src/screens/Login/SetNewPasswordForm.tsx
··· 1 + import React, {useState, useEffect} from 'react' 2 + import {ActivityIndicator, View} from 'react-native' 3 + import {BskyAgent} from '@atproto/api' 4 + import {useAnalytics} from 'lib/analytics/analytics' 5 + 6 + import {isNetworkError} from 'lib/strings/errors' 7 + import {cleanError} from 'lib/strings/errors' 8 + import {checkAndFormatResetCode} from 'lib/strings/password' 9 + import {logger} from '#/logger' 10 + import {Trans, msg} from '@lingui/macro' 11 + import {useLingui} from '@lingui/react' 12 + import {FormContainer} from './FormContainer' 13 + import {Text} from '#/components/Typography' 14 + import * as TextField from '#/components/forms/TextField' 15 + import {Lock_Stroke2_Corner0_Rounded as Lock} from '#/components/icons/Lock' 16 + import {Ticket_Stroke2_Corner0_Rounded as Ticket} from '#/components/icons/Ticket' 17 + import {Button, ButtonText} from '#/components/Button' 18 + import {useTheme, atoms as a} from '#/alf' 19 + import {FormError} from './FormError' 20 + 21 + export const SetNewPasswordForm = ({ 22 + error, 23 + serviceUrl, 24 + setError, 25 + onPressBack, 26 + onPasswordSet, 27 + }: { 28 + error: string 29 + serviceUrl: string 30 + setError: (v: string) => void 31 + onPressBack: () => void 32 + onPasswordSet: () => void 33 + }) => { 34 + const {screen} = useAnalytics() 35 + const {_} = useLingui() 36 + const t = useTheme() 37 + 38 + useEffect(() => { 39 + screen('Signin:SetNewPasswordForm') 40 + }, [screen]) 41 + 42 + const [isProcessing, setIsProcessing] = useState<boolean>(false) 43 + const [resetCode, setResetCode] = useState<string>('') 44 + const [password, setPassword] = useState<string>('') 45 + 46 + const onPressNext = async () => { 47 + onPasswordSet() 48 + if (Math.random() > 0) return 49 + // Check that the code is correct. We do this again just incase the user enters the code after their pw and we 50 + // don't get to call onBlur first 51 + const formattedCode = checkAndFormatResetCode(resetCode) 52 + // TODO Better password strength check 53 + if (!formattedCode || !password) { 54 + setError( 55 + _( 56 + msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, 57 + ), 58 + ) 59 + return 60 + } 61 + 62 + setError('') 63 + setIsProcessing(true) 64 + 65 + try { 66 + const agent = new BskyAgent({service: serviceUrl}) 67 + await agent.com.atproto.server.resetPassword({ 68 + token: formattedCode, 69 + password, 70 + }) 71 + onPasswordSet() 72 + } catch (e: any) { 73 + const errMsg = e.toString() 74 + logger.warn('Failed to set new password', {error: e}) 75 + setIsProcessing(false) 76 + if (isNetworkError(e)) { 77 + setError( 78 + 'Unable to contact your service. Please check your Internet connection.', 79 + ) 80 + } else { 81 + setError(cleanError(errMsg)) 82 + } 83 + } 84 + } 85 + 86 + const onBlur = () => { 87 + const formattedCode = checkAndFormatResetCode(resetCode) 88 + if (!formattedCode) { 89 + setError( 90 + _( 91 + msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, 92 + ), 93 + ) 94 + return 95 + } 96 + setResetCode(formattedCode) 97 + } 98 + 99 + return ( 100 + <FormContainer 101 + testID="setNewPasswordForm" 102 + title={<Trans>Set new password</Trans>}> 103 + <Text> 104 + <Trans> 105 + You will receive an email with a "reset code." Enter that code here, 106 + then enter your new password. 107 + </Trans> 108 + </Text> 109 + 110 + <View> 111 + <TextField.Label>Reset code</TextField.Label> 112 + <TextField.Root> 113 + <TextField.Icon icon={Ticket} /> 114 + <TextField.Input 115 + testID="resetCodeInput" 116 + label={_(msg`Looks like XXXXX-XXXXX`)} 117 + autoCapitalize="none" 118 + autoCorrect={false} 119 + autoComplete="off" 120 + value={resetCode} 121 + onChangeText={setResetCode} 122 + onFocus={() => setError('')} 123 + onBlur={onBlur} 124 + editable={!isProcessing} 125 + accessibilityHint={_( 126 + msg`Input code sent to your email for password reset`, 127 + )} 128 + /> 129 + </TextField.Root> 130 + </View> 131 + 132 + <View> 133 + <TextField.Label>New password</TextField.Label> 134 + <TextField.Root> 135 + <TextField.Icon icon={Lock} /> 136 + <TextField.Input 137 + testID="newPasswordInput" 138 + label={_(msg`Enter a password`)} 139 + autoCapitalize="none" 140 + autoCorrect={false} 141 + autoComplete="password" 142 + returnKeyType="done" 143 + secureTextEntry={true} 144 + textContentType="password" 145 + clearButtonMode="while-editing" 146 + value={password} 147 + onChangeText={setPassword} 148 + onSubmitEditing={onPressNext} 149 + editable={!isProcessing} 150 + accessibilityHint={_(msg`Input new password`)} 151 + /> 152 + </TextField.Root> 153 + </View> 154 + <FormError error={error} /> 155 + <View style={[a.flex_row, a.align_center]}> 156 + <Button 157 + label={_(msg`Back`)} 158 + variant="solid" 159 + color="secondary" 160 + size="small" 161 + onPress={onPressBack}> 162 + <ButtonText> 163 + <Trans>Back</Trans> 164 + </ButtonText> 165 + </Button> 166 + <View style={a.flex_1} /> 167 + {isProcessing ? ( 168 + <ActivityIndicator /> 169 + ) : ( 170 + <Button 171 + label={_(msg`Next`)} 172 + variant="solid" 173 + color="primary" 174 + size="small" 175 + onPress={onPressNext}> 176 + <ButtonText> 177 + <Trans>Next</Trans> 178 + </ButtonText> 179 + </Button> 180 + )} 181 + {isProcessing ? ( 182 + <Text style={[t.atoms.text_contrast_high, a.pl_md]}> 183 + <Trans>Updating...</Trans> 184 + </Text> 185 + ) : undefined} 186 + </View> 187 + </FormContainer> 188 + ) 189 + }
+3 -3
src/screens/Login/index.tsx
··· 13 13 import {logger} from '#/logger' 14 14 import {atoms as a} from '#/alf' 15 15 import {ChooseAccountForm} from './ChooseAccountForm' 16 - import {ForgotPasswordForm} from '#/view/com/auth/login/ForgotPasswordForm' 17 - import {SetNewPasswordForm} from '#/view/com/auth/login/SetNewPasswordForm' 18 - import {PasswordUpdatedForm} from '#/view/com/auth/login/PasswordUpdatedForm' 16 + import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' 17 + import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' 18 + import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' 19 19 import {LoginForm} from '#/screens/Login/LoginForm' 20 20 21 21 enum Forms {
-228
src/view/com/auth/login/ForgotPasswordForm.tsx
··· 1 - import React, {useState, useEffect} from 'react' 2 - import { 3 - ActivityIndicator, 4 - Keyboard, 5 - TextInput, 6 - TouchableOpacity, 7 - View, 8 - } from 'react-native' 9 - import { 10 - FontAwesomeIcon, 11 - FontAwesomeIconStyle, 12 - } from '@fortawesome/react-native-fontawesome' 13 - import {ComAtprotoServerDescribeServer} from '@atproto/api' 14 - import * as EmailValidator from 'email-validator' 15 - import {BskyAgent} from '@atproto/api' 16 - import {useAnalytics} from 'lib/analytics/analytics' 17 - import {Text} from '../../util/text/Text' 18 - import {s} from 'lib/styles' 19 - import {toNiceDomain} from 'lib/strings/url-helpers' 20 - import {isNetworkError} from 'lib/strings/errors' 21 - import {usePalette} from 'lib/hooks/usePalette' 22 - import {useTheme} from 'lib/ThemeContext' 23 - import {cleanError} from 'lib/strings/errors' 24 - import {logger} from '#/logger' 25 - import {Trans, msg} from '@lingui/macro' 26 - import {useLingui} from '@lingui/react' 27 - import {styles} from './styles' 28 - import {useDialogControl} from '#/components/Dialog' 29 - 30 - import {ServerInputDialog} from '../server-input' 31 - 32 - type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema 33 - 34 - export const ForgotPasswordForm = ({ 35 - error, 36 - serviceUrl, 37 - serviceDescription, 38 - setError, 39 - setServiceUrl, 40 - onPressBack, 41 - onEmailSent, 42 - }: { 43 - error: string 44 - serviceUrl: string 45 - serviceDescription: ServiceDescription | undefined 46 - setError: (v: string) => void 47 - setServiceUrl: (v: string) => void 48 - onPressBack: () => void 49 - onEmailSent: () => void 50 - }) => { 51 - const pal = usePalette('default') 52 - const theme = useTheme() 53 - const [isProcessing, setIsProcessing] = useState<boolean>(false) 54 - const [email, setEmail] = useState<string>('') 55 - const {screen} = useAnalytics() 56 - const {_} = useLingui() 57 - const serverInputControl = useDialogControl() 58 - 59 - useEffect(() => { 60 - screen('Signin:ForgotPassword') 61 - }, [screen]) 62 - 63 - const onPressSelectService = React.useCallback(() => { 64 - serverInputControl.open() 65 - Keyboard.dismiss() 66 - }, [serverInputControl]) 67 - 68 - const onPressNext = async () => { 69 - if (!EmailValidator.validate(email)) { 70 - return setError(_(msg`Your email appears to be invalid.`)) 71 - } 72 - 73 - setError('') 74 - setIsProcessing(true) 75 - 76 - try { 77 - const agent = new BskyAgent({service: serviceUrl}) 78 - await agent.com.atproto.server.requestPasswordReset({email}) 79 - onEmailSent() 80 - } catch (e: any) { 81 - const errMsg = e.toString() 82 - logger.warn('Failed to request password reset', {error: e}) 83 - setIsProcessing(false) 84 - if (isNetworkError(e)) { 85 - setError( 86 - _( 87 - msg`Unable to contact your service. Please check your Internet connection.`, 88 - ), 89 - ) 90 - } else { 91 - setError(cleanError(errMsg)) 92 - } 93 - } 94 - } 95 - 96 - return ( 97 - <> 98 - <View> 99 - <ServerInputDialog 100 - control={serverInputControl} 101 - onSelect={setServiceUrl} 102 - /> 103 - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> 104 - <Trans>Reset password</Trans> 105 - </Text> 106 - <Text type="md" style={[pal.text, styles.instructions]}> 107 - <Trans> 108 - Enter the email you used to create your account. We'll send you a 109 - "reset code" so you can set a new password. 110 - </Trans> 111 - </Text> 112 - <View 113 - testID="forgotPasswordView" 114 - style={[pal.borderDark, pal.view, styles.group]}> 115 - <TouchableOpacity 116 - testID="forgotPasswordSelectServiceButton" 117 - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]} 118 - onPress={onPressSelectService} 119 - accessibilityRole="button" 120 - accessibilityLabel={_(msg`Hosting provider`)} 121 - accessibilityHint={_( 122 - msg`Sets hosting provider for password reset`, 123 - )}> 124 - <FontAwesomeIcon 125 - icon="globe" 126 - style={[pal.textLight, styles.groupContentIcon]} 127 - /> 128 - <Text style={[pal.text, styles.textInput]} numberOfLines={1}> 129 - {toNiceDomain(serviceUrl)} 130 - </Text> 131 - <View style={[pal.btn, styles.textBtnFakeInnerBtn]}> 132 - <FontAwesomeIcon 133 - icon="pen" 134 - size={12} 135 - style={pal.text as FontAwesomeIconStyle} 136 - /> 137 - </View> 138 - </TouchableOpacity> 139 - <View style={[pal.borderDark, styles.groupContent]}> 140 - <FontAwesomeIcon 141 - icon="envelope" 142 - style={[pal.textLight, styles.groupContentIcon]} 143 - /> 144 - <TextInput 145 - testID="forgotPasswordEmail" 146 - style={[pal.text, styles.textInput]} 147 - placeholder={_(msg`Email address`)} 148 - placeholderTextColor={pal.colors.textLight} 149 - autoCapitalize="none" 150 - autoFocus 151 - autoCorrect={false} 152 - keyboardAppearance={theme.colorScheme} 153 - value={email} 154 - onChangeText={setEmail} 155 - editable={!isProcessing} 156 - accessibilityLabel={_(msg`Email`)} 157 - accessibilityHint={_(msg`Sets email for password reset`)} 158 - /> 159 - </View> 160 - </View> 161 - {error ? ( 162 - <View style={styles.error}> 163 - <View style={styles.errorIcon}> 164 - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> 165 - </View> 166 - <View style={s.flex1}> 167 - <Text style={[s.white, s.bold]}>{error}</Text> 168 - </View> 169 - </View> 170 - ) : undefined} 171 - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> 172 - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> 173 - <Text type="xl" style={[pal.link, s.pl5]}> 174 - <Trans>Back</Trans> 175 - </Text> 176 - </TouchableOpacity> 177 - <View style={s.flex1} /> 178 - {!serviceDescription || isProcessing ? ( 179 - <ActivityIndicator /> 180 - ) : !email ? ( 181 - <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> 182 - <Trans>Next</Trans> 183 - </Text> 184 - ) : ( 185 - <TouchableOpacity 186 - testID="newPasswordButton" 187 - onPress={onPressNext} 188 - accessibilityRole="button" 189 - accessibilityLabel={_(msg`Go to next`)} 190 - accessibilityHint={_(msg`Navigates to the next screen`)}> 191 - <Text type="xl-bold" style={[pal.link, s.pr5]}> 192 - <Trans>Next</Trans> 193 - </Text> 194 - </TouchableOpacity> 195 - )} 196 - {!serviceDescription || isProcessing ? ( 197 - <Text type="xl" style={[pal.textLight, s.pl10]}> 198 - <Trans>Processing...</Trans> 199 - </Text> 200 - ) : undefined} 201 - </View> 202 - <View 203 - style={[ 204 - s.flexRow, 205 - s.alignCenter, 206 - s.mt20, 207 - s.mb20, 208 - pal.border, 209 - s.borderBottom1, 210 - {alignSelf: 'center', width: '90%'}, 211 - ]} 212 - /> 213 - <View style={[s.flexRow, s.justifyCenter]}> 214 - <TouchableOpacity 215 - testID="skipSendEmailButton" 216 - onPress={onEmailSent} 217 - accessibilityRole="button" 218 - accessibilityLabel={_(msg`Go to next`)} 219 - accessibilityHint={_(msg`Navigates to the next screen`)}> 220 - <Text type="xl" style={[pal.link, s.pr5]}> 221 - <Trans>Already have a code?</Trans> 222 - </Text> 223 - </TouchableOpacity> 224 - </View> 225 - </View> 226 - </> 227 - ) 228 - }
-48
src/view/com/auth/login/PasswordUpdatedForm.tsx
··· 1 - import React, {useEffect} from 'react' 2 - import {TouchableOpacity, View} from 'react-native' 3 - import {useAnalytics} from 'lib/analytics/analytics' 4 - import {Text} from '../../util/text/Text' 5 - import {s} from 'lib/styles' 6 - import {usePalette} from 'lib/hooks/usePalette' 7 - import {styles} from './styles' 8 - import {msg, Trans} from '@lingui/macro' 9 - import {useLingui} from '@lingui/react' 10 - 11 - export const PasswordUpdatedForm = ({ 12 - onPressNext, 13 - }: { 14 - onPressNext: () => void 15 - }) => { 16 - const {screen} = useAnalytics() 17 - const pal = usePalette('default') 18 - const {_} = useLingui() 19 - 20 - useEffect(() => { 21 - screen('Signin:PasswordUpdatedForm') 22 - }, [screen]) 23 - 24 - return ( 25 - <> 26 - <View> 27 - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> 28 - <Trans>Password updated!</Trans> 29 - </Text> 30 - <Text type="lg" style={[pal.text, styles.instructions]}> 31 - <Trans>You can now sign in with your new password.</Trans> 32 - </Text> 33 - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> 34 - <View style={s.flex1} /> 35 - <TouchableOpacity 36 - onPress={onPressNext} 37 - accessibilityRole="button" 38 - accessibilityLabel={_(msg`Close alert`)} 39 - accessibilityHint={_(msg`Closes password update alert`)}> 40 - <Text type="xl-bold" style={[pal.link, s.pr5]}> 41 - <Trans>Okay</Trans> 42 - </Text> 43 - </TouchableOpacity> 44 - </View> 45 - </View> 46 - </> 47 - ) 48 - }
-211
src/view/com/auth/login/SetNewPasswordForm.tsx
··· 1 - import React, {useState, useEffect} from 'react' 2 - import { 3 - ActivityIndicator, 4 - TextInput, 5 - TouchableOpacity, 6 - View, 7 - } from 'react-native' 8 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 9 - import {BskyAgent} from '@atproto/api' 10 - import {useAnalytics} from 'lib/analytics/analytics' 11 - import {Text} from '../../util/text/Text' 12 - import {s} from 'lib/styles' 13 - import {isNetworkError} from 'lib/strings/errors' 14 - import {usePalette} from 'lib/hooks/usePalette' 15 - import {useTheme} from 'lib/ThemeContext' 16 - import {cleanError} from 'lib/strings/errors' 17 - import {checkAndFormatResetCode} from 'lib/strings/password' 18 - import {logger} from '#/logger' 19 - import {styles} from './styles' 20 - import {Trans, msg} from '@lingui/macro' 21 - import {useLingui} from '@lingui/react' 22 - 23 - export const SetNewPasswordForm = ({ 24 - error, 25 - serviceUrl, 26 - setError, 27 - onPressBack, 28 - onPasswordSet, 29 - }: { 30 - error: string 31 - serviceUrl: string 32 - setError: (v: string) => void 33 - onPressBack: () => void 34 - onPasswordSet: () => void 35 - }) => { 36 - const pal = usePalette('default') 37 - const theme = useTheme() 38 - const {screen} = useAnalytics() 39 - const {_} = useLingui() 40 - 41 - useEffect(() => { 42 - screen('Signin:SetNewPasswordForm') 43 - }, [screen]) 44 - 45 - const [isProcessing, setIsProcessing] = useState<boolean>(false) 46 - const [resetCode, setResetCode] = useState<string>('') 47 - const [password, setPassword] = useState<string>('') 48 - 49 - const onPressNext = async () => { 50 - // Check that the code is correct. We do this again just incase the user enters the code after their pw and we 51 - // don't get to call onBlur first 52 - const formattedCode = checkAndFormatResetCode(resetCode) 53 - // TODO Better password strength check 54 - if (!formattedCode || !password) { 55 - setError( 56 - _( 57 - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, 58 - ), 59 - ) 60 - return 61 - } 62 - 63 - setError('') 64 - setIsProcessing(true) 65 - 66 - try { 67 - const agent = new BskyAgent({service: serviceUrl}) 68 - await agent.com.atproto.server.resetPassword({ 69 - token: formattedCode, 70 - password, 71 - }) 72 - onPasswordSet() 73 - } catch (e: any) { 74 - const errMsg = e.toString() 75 - logger.warn('Failed to set new password', {error: e}) 76 - setIsProcessing(false) 77 - if (isNetworkError(e)) { 78 - setError( 79 - 'Unable to contact your service. Please check your Internet connection.', 80 - ) 81 - } else { 82 - setError(cleanError(errMsg)) 83 - } 84 - } 85 - } 86 - 87 - const onBlur = () => { 88 - const formattedCode = checkAndFormatResetCode(resetCode) 89 - if (!formattedCode) { 90 - setError( 91 - _( 92 - msg`You have entered an invalid code. It should look like XXXXX-XXXXX.`, 93 - ), 94 - ) 95 - return 96 - } 97 - setResetCode(formattedCode) 98 - } 99 - 100 - return ( 101 - <> 102 - <View> 103 - <Text type="title-lg" style={[pal.text, styles.screenTitle]}> 104 - <Trans>Set new password</Trans> 105 - </Text> 106 - <Text type="lg" style={[pal.text, styles.instructions]}> 107 - <Trans> 108 - You will receive an email with a "reset code." Enter that code here, 109 - then enter your new password. 110 - </Trans> 111 - </Text> 112 - <View 113 - testID="newPasswordView" 114 - style={[pal.view, pal.borderDark, styles.group]}> 115 - <View 116 - style={[pal.borderDark, styles.groupContent, styles.noTopBorder]}> 117 - <FontAwesomeIcon 118 - icon="ticket" 119 - style={[pal.textLight, styles.groupContentIcon]} 120 - /> 121 - <TextInput 122 - testID="resetCodeInput" 123 - style={[pal.text, styles.textInput]} 124 - placeholder={_(msg`Reset code`)} 125 - placeholderTextColor={pal.colors.textLight} 126 - autoCapitalize="none" 127 - autoCorrect={false} 128 - keyboardAppearance={theme.colorScheme} 129 - autoComplete="off" 130 - value={resetCode} 131 - onChangeText={setResetCode} 132 - onFocus={() => setError('')} 133 - onBlur={onBlur} 134 - editable={!isProcessing} 135 - accessible={true} 136 - accessibilityLabel={_(msg`Reset code`)} 137 - accessibilityHint={_( 138 - msg`Input code sent to your email for password reset`, 139 - )} 140 - /> 141 - </View> 142 - <View style={[pal.borderDark, styles.groupContent]}> 143 - <FontAwesomeIcon 144 - icon="lock" 145 - style={[pal.textLight, styles.groupContentIcon]} 146 - /> 147 - <TextInput 148 - testID="newPasswordInput" 149 - style={[pal.text, styles.textInput]} 150 - placeholder={_(msg`New password`)} 151 - placeholderTextColor={pal.colors.textLight} 152 - autoCapitalize="none" 153 - autoCorrect={false} 154 - autoComplete="new-password" 155 - keyboardAppearance={theme.colorScheme} 156 - secureTextEntry 157 - value={password} 158 - onChangeText={setPassword} 159 - editable={!isProcessing} 160 - accessible={true} 161 - accessibilityLabel={_(msg`Password`)} 162 - accessibilityHint={_(msg`Input new password`)} 163 - /> 164 - </View> 165 - </View> 166 - {error ? ( 167 - <View style={styles.error}> 168 - <View style={styles.errorIcon}> 169 - <FontAwesomeIcon icon="exclamation" style={s.white} size={10} /> 170 - </View> 171 - <View style={s.flex1}> 172 - <Text style={[s.white, s.bold]}>{error}</Text> 173 - </View> 174 - </View> 175 - ) : undefined} 176 - <View style={[s.flexRow, s.alignCenter, s.pl20, s.pr20]}> 177 - <TouchableOpacity onPress={onPressBack} accessibilityRole="button"> 178 - <Text type="xl" style={[pal.link, s.pl5]}> 179 - <Trans>Back</Trans> 180 - </Text> 181 - </TouchableOpacity> 182 - <View style={s.flex1} /> 183 - {isProcessing ? ( 184 - <ActivityIndicator /> 185 - ) : !resetCode || !password ? ( 186 - <Text type="xl-bold" style={[pal.link, s.pr5, styles.dimmed]}> 187 - <Trans>Next</Trans> 188 - </Text> 189 - ) : ( 190 - <TouchableOpacity 191 - testID="setNewPasswordButton" 192 - // Check the code before running the callback 193 - onPress={onPressNext} 194 - accessibilityRole="button" 195 - accessibilityLabel={_(msg`Go to next`)} 196 - accessibilityHint={_(msg`Navigates to the next screen`)}> 197 - <Text type="xl-bold" style={[pal.link, s.pr5]}> 198 - <Trans>Next</Trans> 199 - </Text> 200 - </TouchableOpacity> 201 - )} 202 - {isProcessing ? ( 203 - <Text type="xl" style={[pal.textLight, s.pl10]}> 204 - <Trans>Updating...</Trans> 205 - </Text> 206 - ) : undefined} 207 - </View> 208 - </View> 209 - </> 210 - ) 211 - }
-118
src/view/com/auth/login/styles.ts
··· 1 - import {StyleSheet} from 'react-native' 2 - import {colors} from 'lib/styles' 3 - import {isWeb} from '#/platform/detection' 4 - 5 - export const styles = StyleSheet.create({ 6 - screenTitle: { 7 - marginBottom: 10, 8 - marginHorizontal: 20, 9 - }, 10 - instructions: { 11 - marginBottom: 20, 12 - marginHorizontal: 20, 13 - }, 14 - group: { 15 - borderWidth: 1, 16 - borderRadius: 10, 17 - marginBottom: 20, 18 - marginHorizontal: 20, 19 - }, 20 - groupLabel: { 21 - paddingHorizontal: 20, 22 - paddingBottom: 5, 23 - }, 24 - groupContent: { 25 - borderTopWidth: 1, 26 - flexDirection: 'row', 27 - alignItems: 'center', 28 - }, 29 - noTopBorder: { 30 - borderTopWidth: 0, 31 - }, 32 - groupContentIcon: { 33 - marginLeft: 10, 34 - }, 35 - account: { 36 - borderTopWidth: 1, 37 - paddingHorizontal: 20, 38 - paddingVertical: 4, 39 - }, 40 - accountLast: { 41 - borderBottomWidth: 1, 42 - marginBottom: 20, 43 - paddingVertical: 8, 44 - }, 45 - textInput: { 46 - flex: 1, 47 - width: '100%', 48 - paddingVertical: 10, 49 - paddingHorizontal: 12, 50 - fontSize: 17, 51 - letterSpacing: 0.25, 52 - fontWeight: '400', 53 - borderRadius: 10, 54 - }, 55 - textInputInnerBtn: { 56 - flexDirection: 'row', 57 - alignItems: 'center', 58 - paddingVertical: 6, 59 - paddingHorizontal: 8, 60 - marginHorizontal: 6, 61 - }, 62 - textBtn: { 63 - flexDirection: 'row', 64 - flex: 1, 65 - alignItems: 'center', 66 - }, 67 - textBtnLabel: { 68 - flex: 1, 69 - paddingVertical: 10, 70 - paddingHorizontal: 12, 71 - }, 72 - textBtnFakeInnerBtn: { 73 - flexDirection: 'row', 74 - alignItems: 'center', 75 - borderRadius: 6, 76 - paddingVertical: 6, 77 - paddingHorizontal: 8, 78 - marginHorizontal: 6, 79 - }, 80 - accountText: { 81 - flex: 1, 82 - flexDirection: 'row', 83 - alignItems: 'baseline', 84 - paddingVertical: 10, 85 - }, 86 - accountTextOther: { 87 - paddingLeft: 12, 88 - }, 89 - error: { 90 - backgroundColor: colors.red4, 91 - flexDirection: 'row', 92 - alignItems: 'center', 93 - marginTop: -5, 94 - marginHorizontal: 20, 95 - marginBottom: 15, 96 - borderRadius: 8, 97 - paddingHorizontal: 8, 98 - paddingVertical: 8, 99 - }, 100 - errorIcon: { 101 - borderWidth: 1, 102 - borderColor: colors.white, 103 - color: colors.white, 104 - borderRadius: 30, 105 - width: 16, 106 - height: 16, 107 - alignItems: 'center', 108 - justifyContent: 'center', 109 - marginRight: 5, 110 - }, 111 - dimmed: {opacity: 0.5}, 112 - 113 - maxHeight: { 114 - // @ts-ignore web only -prf 115 - maxHeight: isWeb ? '100vh' : undefined, 116 - height: !isWeb ? '100%' : undefined, 117 - }, 118 - })