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