Bluesky app fork with some witchin' additions 💫

Better screen transitions for auth flow (#7803)

authored by samuel.fm and committed by

GitHub 7916c73b 6d85fe05

+293 -244
+40
src/components/ScreenTransition.tsx
···
··· 1 + import {type StyleProp, type ViewStyle} from 'react-native' 2 + import Animated, { 3 + Easing, 4 + FadeIn, 5 + FadeOut, 6 + SlideInLeft, 7 + SlideInRight, 8 + } from 'react-native-reanimated' 9 + import type React from 'react' 10 + 11 + import {isWeb} from '#/platform/detection' 12 + 13 + export function ScreenTransition({ 14 + direction, 15 + style, 16 + children, 17 + enabledWeb, 18 + }: { 19 + direction: 'Backward' | 'Forward' 20 + style?: StyleProp<ViewStyle> 21 + children: React.ReactNode 22 + enabledWeb?: boolean 23 + }) { 24 + const entering = 25 + direction === 'Forward' 26 + ? SlideInRight.easing(Easing.out(Easing.exp)) 27 + : SlideInLeft.easing(Easing.out(Easing.exp)) 28 + const webEntering = enabledWeb ? FadeIn.duration(90) : undefined 29 + const exiting = FadeOut.duration(90) // Totally vibes based 30 + const webExiting = enabledWeb ? FadeOut.duration(90) : undefined 31 + 32 + return ( 33 + <Animated.View 34 + entering={isWeb ? webEntering : entering} 35 + exiting={isWeb ? webExiting : exiting} 36 + style={style}> 37 + {children} 38 + </Animated.View> 39 + ) 40 + }
-31
src/components/StarterPack/Wizard/ScreenTransition.tsx
··· 1 - import {type StyleProp, type ViewStyle} from 'react-native' 2 - import Animated, { 3 - FadeIn, 4 - FadeOut, 5 - SlideInLeft, 6 - SlideInRight, 7 - } from 'react-native-reanimated' 8 - import type React from 'react' 9 - 10 - import {isWeb} from '#/platform/detection' 11 - 12 - export function ScreenTransition({ 13 - direction, 14 - style, 15 - children, 16 - }: { 17 - direction: 'Backward' | 'Forward' 18 - style?: StyleProp<ViewStyle> 19 - children: React.ReactNode 20 - }) { 21 - const entering = direction === 'Forward' ? SlideInRight : SlideInLeft 22 - 23 - return ( 24 - <Animated.View 25 - entering={isWeb ? FadeIn.duration(90) : entering} 26 - exiting={FadeOut.duration(90)} // Totally vibes based 27 - style={style}> 28 - {children} 29 - </Animated.View> 30 - ) 31 - }
···
-17
src/screens/Login/ScreenTransition.tsx
··· 1 - import {type StyleProp, type ViewStyle} from 'react-native' 2 - import Animated, {FadeInRight, FadeOutLeft} from 'react-native-reanimated' 3 - import type React from 'react' 4 - 5 - export function ScreenTransition({ 6 - style, 7 - children, 8 - }: { 9 - style?: StyleProp<ViewStyle> 10 - children: React.ReactNode 11 - }) { 12 - return ( 13 - <Animated.View style={style} entering={FadeInRight} exiting={FadeOutLeft}> 14 - {children} 15 - </Animated.View> 16 - ) 17 - }
···
-1
src/screens/Login/ScreenTransition.web.tsx
··· 1 - export {Fragment as ScreenTransition} from 'react'
···
+43 -23
src/screens/Login/index.tsx
··· 1 - import React, {useRef} from 'react' 2 import {KeyboardAvoidingView} from 'react-native' 3 - import {LayoutAnimationConfig} from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 ··· 15 import {LoginForm} from '#/screens/Login/LoginForm' 16 import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' 17 import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' 18 - import {atoms as a} from '#/alf' 19 import {ChooseAccountForm} from './ChooseAccountForm' 20 - import {ScreenTransition} from './ScreenTransition' 21 22 enum Forms { 23 Login, ··· 27 PasswordUpdated, 28 } 29 30 export const Login = ({onPressBack}: {onPressBack: () => void}) => { 31 const {_} = useLingui() 32 const failedAttemptCountRef = useRef(0) ··· 38 acc => acc.did === requestedAccountSwitchTo, 39 ) 40 41 - const [error, setError] = React.useState<string>('') 42 - const [serviceUrl, setServiceUrl] = React.useState<string>( 43 requestedAccount?.service || DEFAULT_SERVICE, 44 ) 45 - const [initialHandle, setInitialHandle] = React.useState<string>( 46 requestedAccount?.handle || '', 47 ) 48 - const [currentForm, setCurrentForm] = React.useState<Forms>( 49 requestedAccount 50 ? Forms.Login 51 : accounts.length 52 ? Forms.ChooseAccount 53 : Forms.Login, 54 ) 55 56 const { 57 data: serviceDescription, ··· 64 setServiceUrl(account.service) 65 } 66 setInitialHandle(account?.handle || '') 67 - setCurrentForm(Forms.Login) 68 } 69 70 const gotoForm = (form: Forms) => { 71 setError('') 72 setCurrentForm(form) 73 } 74 75 - React.useEffect(() => { 76 if (serviceError) { 77 setError( 78 _( ··· 89 }, [serviceError, serviceUrl, _]) 90 91 const onPressForgotPassword = () => { 92 - setCurrentForm(Forms.ForgotPassword) 93 logEvent('signin:forgotPasswordPressed', {}) 94 } 95 96 const handlePressBack = () => { 97 onPressBack() 98 logEvent('signin:backPressed', { 99 failedAttemptsCount: failedAttemptCountRef.current, 100 }) ··· 106 timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), 107 failedAttemptsCount: failedAttemptCountRef.current, 108 }) 109 - setCurrentForm(Forms.Login) 110 } 111 112 const onAttemptFailed = () => { ··· 187 } 188 189 return ( 190 - <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}> 191 - <LoggedOutLayout 192 - leadin="" 193 - title={title} 194 - description={description} 195 - scrollable> 196 - <LayoutAnimationConfig skipEntering skipExiting> 197 - <ScreenTransition key={currentForm}>{content}</ScreenTransition> 198 - </LayoutAnimationConfig> 199 - </LoggedOutLayout> 200 - </KeyboardAvoidingView> 201 ) 202 }
··· 1 + import {useEffect, useRef, useState} from 'react' 2 import {KeyboardAvoidingView} from 'react-native' 3 + import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' 4 import {msg} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 ··· 15 import {LoginForm} from '#/screens/Login/LoginForm' 16 import {PasswordUpdatedForm} from '#/screens/Login/PasswordUpdatedForm' 17 import {SetNewPasswordForm} from '#/screens/Login/SetNewPasswordForm' 18 + import {atoms as a, native} from '#/alf' 19 + import {ScreenTransition} from '#/components/ScreenTransition' 20 import {ChooseAccountForm} from './ChooseAccountForm' 21 22 enum Forms { 23 Login, ··· 27 PasswordUpdated, 28 } 29 30 + const OrderedForms = [ 31 + Forms.ChooseAccount, 32 + Forms.Login, 33 + Forms.ForgotPassword, 34 + Forms.SetNewPassword, 35 + Forms.PasswordUpdated, 36 + ] as const 37 + 38 export const Login = ({onPressBack}: {onPressBack: () => void}) => { 39 const {_} = useLingui() 40 const failedAttemptCountRef = useRef(0) ··· 46 acc => acc.did === requestedAccountSwitchTo, 47 ) 48 49 + const [error, setError] = useState('') 50 + const [serviceUrl, setServiceUrl] = useState( 51 requestedAccount?.service || DEFAULT_SERVICE, 52 ) 53 + const [initialHandle, setInitialHandle] = useState( 54 requestedAccount?.handle || '', 55 ) 56 + const [currentForm, setCurrentForm] = useState<Forms>( 57 requestedAccount 58 ? Forms.Login 59 : accounts.length 60 ? Forms.ChooseAccount 61 : Forms.Login, 62 ) 63 + const [screenTransitionDirection, setScreenTransitionDirection] = useState< 64 + 'Forward' | 'Backward' 65 + >('Forward') 66 67 const { 68 data: serviceDescription, ··· 75 setServiceUrl(account.service) 76 } 77 setInitialHandle(account?.handle || '') 78 + gotoForm(Forms.Login) 79 } 80 81 const gotoForm = (form: Forms) => { 82 setError('') 83 + const index = OrderedForms.indexOf(currentForm) 84 + const nextIndex = OrderedForms.indexOf(form) 85 + setScreenTransitionDirection(index < nextIndex ? 'Forward' : 'Backward') 86 setCurrentForm(form) 87 } 88 89 + useEffect(() => { 90 if (serviceError) { 91 setError( 92 _( ··· 103 }, [serviceError, serviceUrl, _]) 104 105 const onPressForgotPassword = () => { 106 + gotoForm(Forms.ForgotPassword) 107 logEvent('signin:forgotPasswordPressed', {}) 108 } 109 110 const handlePressBack = () => { 111 onPressBack() 112 + setScreenTransitionDirection('Backward') 113 logEvent('signin:backPressed', { 114 failedAttemptsCount: failedAttemptCountRef.current, 115 }) ··· 121 timeTakenSeconds: Math.round((Date.now() - startTimeRef.current) / 1000), 122 failedAttemptsCount: failedAttemptCountRef.current, 123 }) 124 } 125 126 const onAttemptFailed = () => { ··· 201 } 202 203 return ( 204 + <Animated.View style={a.flex_1} entering={native(FadeIn.duration(90))}> 205 + <KeyboardAvoidingView testID="signIn" behavior="padding" style={a.flex_1}> 206 + <LoggedOutLayout 207 + leadin="" 208 + title={title} 209 + description={description} 210 + scrollable> 211 + <LayoutAnimationConfig skipEntering> 212 + <ScreenTransition 213 + key={currentForm} 214 + direction={screenTransitionDirection}> 215 + {content} 216 + </ScreenTransition> 217 + </LayoutAnimationConfig> 218 + </LoggedOutLayout> 219 + </KeyboardAvoidingView> 220 + </Animated.View> 221 ) 222 }
+2 -3
src/screens/Signup/StepCaptcha/index.tsx
··· 8 import {createFullHandle} from '#/lib/strings/handles' 9 import {logger} from '#/logger' 10 import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 11 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 12 import {useSignupContext} from '#/screens/Signup/state' 13 import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 14 import {atoms as a, useTheme} from '#/alf' ··· 143 }, [dispatch, state.handle]) 144 145 return ( 146 - <ScreenTransition> 147 <View style={[a.gap_lg, a.pt_lg]}> 148 <View 149 style={[ ··· 171 isLoading={state.isLoading} 172 onBackPress={onBackPress} 173 /> 174 - </ScreenTransition> 175 ) 176 } 177
··· 8 import {createFullHandle} from '#/lib/strings/handles' 9 import {logger} from '#/logger' 10 import {isAndroid, isIOS, isNative, isWeb} from '#/platform/detection' 11 import {useSignupContext} from '#/screens/Signup/state' 12 import {CaptchaWebView} from '#/screens/Signup/StepCaptcha/CaptchaWebView' 13 import {atoms as a, useTheme} from '#/alf' ··· 142 }, [dispatch, state.handle]) 143 144 return ( 145 + <> 146 <View style={[a.gap_lg, a.pt_lg]}> 147 <View 148 style={[ ··· 170 isLoading={state.isLoading} 171 onBackPress={onBackPress} 172 /> 173 + </> 174 ) 175 } 176
+2 -3
src/screens/Signup/StepHandle/index.tsx
··· 19 checkHandleAvailability, 20 useHandleAvailabilityQuery, 21 } from '#/state/queries/handle-availability' 22 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 23 import {useSignupContext} from '#/screens/Signup/state' 24 import {atoms as a, native, useTheme} from '#/alf' 25 import * as TextField from '#/components/forms/TextField' ··· 141 !validCheck.totalLength 142 143 return ( 144 - <ScreenTransition> 145 <View style={[a.gap_sm, a.pt_lg, a.z_10]}> 146 <View> 147 <TextField.Root isInvalid={textFieldInvalid}> ··· 252 onNextPress={onNextPress} 253 /> 254 </Animated.View> 255 - </ScreenTransition> 256 ) 257 } 258
··· 19 checkHandleAvailability, 20 useHandleAvailabilityQuery, 21 } from '#/state/queries/handle-availability' 22 import {useSignupContext} from '#/screens/Signup/state' 23 import {atoms as a, native, useTheme} from '#/alf' 24 import * as TextField from '#/components/forms/TextField' ··· 140 !validCheck.totalLength 141 142 return ( 143 + <> 144 <View style={[a.gap_sm, a.pt_lg, a.z_10]}> 145 <View> 146 <TextField.Root isInvalid={textFieldInvalid}> ··· 251 onNextPress={onNextPress} 252 /> 253 </Animated.View> 254 + </> 255 ) 256 } 257
+2 -3
src/screens/Signup/StepInfo/index.tsx
··· 7 8 import {isEmailMaybeInvalid} from '#/lib/strings/email' 9 import {logger} from '#/logger' 10 - import {ScreenTransition} from '#/screens/Login/ScreenTransition' 11 import {is13, is18, useSignupContext} from '#/screens/Signup/state' 12 import {Policies} from '#/screens/Signup/StepInfo/Policies' 13 import {atoms as a, native} from '#/alf' ··· 147 } 148 149 return ( 150 - <ScreenTransition> 151 <View style={[a.gap_md, a.pt_lg]}> 152 <FormError error={state.error} /> 153 <HostingProvider ··· 292 onRetryPress={refetchServer} 293 overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined} 294 /> 295 - </ScreenTransition> 296 ) 297 }
··· 7 8 import {isEmailMaybeInvalid} from '#/lib/strings/email' 9 import {logger} from '#/logger' 10 import {is13, is18, useSignupContext} from '#/screens/Signup/state' 11 import {Policies} from '#/screens/Signup/StepInfo/Policies' 12 import {atoms as a, native} from '#/alf' ··· 146 } 147 148 return ( 149 + <> 150 <View style={[a.gap_md, a.pt_lg]}> 151 <FormError error={state.error} /> 152 <HostingProvider ··· 291 onRetryPress={refetchServer} 292 overrideNextText={hasWarnedEmail ? _(msg`It's correct`) : undefined} 293 /> 294 + </> 295 ) 296 }
+112 -100
src/screens/Signup/index.tsx
··· 23 import {StepCaptcha} from '#/screens/Signup/StepCaptcha' 24 import {StepHandle} from '#/screens/Signup/StepHandle' 25 import {StepInfo} from '#/screens/Signup/StepInfo' 26 - import {atoms as a, useBreakpoints, useTheme} from '#/alf' 27 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 28 import {Divider} from '#/components/Divider' 29 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 30 import {InlineLinkText} from '#/components/Link' 31 import {Text} from '#/components/Typography' 32 import {GCP_PROJECT_ID} from '#/env' 33 import * as bsky from '#/types/bsky' ··· 116 }, []) 117 118 return ( 119 - <SignupContext.Provider value={{state, dispatch}}> 120 - <LoggedOutLayout 121 - leadin="" 122 - title={_(msg`Create Account`)} 123 - description={_(msg`We're so excited to have you join us!`)} 124 - scrollable> 125 - <View testID="createAccount" style={a.flex_1}> 126 - {showStarterPackCard && 127 - bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 128 - starterPack.record, 129 - AppBskyGraphStarterpack.isRecord, 130 - ) ? ( 131 - <Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}> 132 - <LinearGradientBackground 133 - style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}> 134 - <Text style={[a.font_bold, a.text_xl, {color: 'white'}]}> 135 - {starterPack.record.name} 136 - </Text> 137 - <Text style={[{color: 'white'}]}> 138 - {starterPack.feeds?.length ? ( 139 - <Trans> 140 - You'll follow the suggested users and feeds once you 141 - finish creating your account! 142 - </Trans> 143 ) : ( 144 - <Trans> 145 - You'll follow the suggested users once you finish creating 146 - your account! 147 - </Trans> 148 )} 149 - </Text> 150 - </LinearGradientBackground> 151 - </Animated.View> 152 - ) : null} 153 - <View 154 - style={[ 155 - a.flex_1, 156 - a.px_xl, 157 - a.pt_2xl, 158 - !gtMobile && {paddingBottom: 100}, 159 - ]}> 160 - <View style={[a.gap_sm, a.pb_sm]}> 161 - <Text 162 - style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}> 163 - <Trans> 164 - Step {state.activeStep + 1} of{' '} 165 - {state.serviceDescription && 166 - !state.serviceDescription.phoneVerificationRequired 167 - ? '2' 168 - : '3'} 169 - </Trans> 170 - </Text> 171 - <Text style={[a.text_3xl, a.font_heavy]}> 172 - {state.activeStep === SignupStep.INFO ? ( 173 - <Trans>Your account</Trans> 174 - ) : state.activeStep === SignupStep.HANDLE ? ( 175 - <Trans>Choose your username</Trans> 176 - ) : ( 177 - <Trans>Complete the challenge</Trans> 178 - )} 179 - </Text> 180 - </View> 181 - 182 - <LayoutAnimationConfig skipEntering skipExiting> 183 - {state.activeStep === SignupStep.INFO ? ( 184 - <StepInfo 185 - onPressBack={onPressBack} 186 - isLoadingStarterPack={ 187 - isFetchingStarterPack && !isErrorStarterPack 188 - } 189 - isServerError={isError} 190 - refetchServer={refetch} 191 - /> 192 - ) : state.activeStep === SignupStep.HANDLE ? ( 193 - <StepHandle /> 194 - ) : ( 195 - <StepCaptcha /> 196 - )} 197 - </LayoutAnimationConfig> 198 199 - <Divider /> 200 201 - <View 202 - style={[a.w_full, a.py_lg, a.flex_row, a.gap_md, a.align_center]}> 203 - <AppLanguageDropdown /> 204 - <Text 205 - style={[ 206 - a.flex_1, 207 - t.atoms.text_contrast_medium, 208 - !gtMobile && a.text_md, 209 - ]}> 210 - <Trans>Having trouble?</Trans>{' '} 211 - <InlineLinkText 212 - label={_(msg`Contact support`)} 213 - to={FEEDBACK_FORM_URL({email: state.email})} 214 - style={[!gtMobile && a.text_md]}> 215 - <Trans>Contact support</Trans> 216 - </InlineLinkText> 217 - </Text> 218 - </View> 219 </View> 220 - </View> 221 - </LoggedOutLayout> 222 - </SignupContext.Provider> 223 ) 224 }
··· 23 import {StepCaptcha} from '#/screens/Signup/StepCaptcha' 24 import {StepHandle} from '#/screens/Signup/StepHandle' 25 import {StepInfo} from '#/screens/Signup/StepInfo' 26 + import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' 27 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 28 import {Divider} from '#/components/Divider' 29 import {LinearGradientBackground} from '#/components/LinearGradientBackground' 30 import {InlineLinkText} from '#/components/Link' 31 + import {ScreenTransition} from '#/components/ScreenTransition' 32 import {Text} from '#/components/Typography' 33 import {GCP_PROJECT_ID} from '#/env' 34 import * as bsky from '#/types/bsky' ··· 117 }, []) 118 119 return ( 120 + <Animated.View exiting={native(FadeIn.duration(90))} style={a.flex_1}> 121 + <SignupContext.Provider value={{state, dispatch}}> 122 + <LoggedOutLayout 123 + leadin="" 124 + title={_(msg`Create Account`)} 125 + description={_(msg`We're so excited to have you join us!`)} 126 + scrollable> 127 + <View testID="createAccount" style={a.flex_1}> 128 + {showStarterPackCard && 129 + bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 130 + starterPack.record, 131 + AppBskyGraphStarterpack.isRecord, 132 + ) ? ( 133 + <Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}> 134 + <LinearGradientBackground 135 + style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}> 136 + <Text style={[a.font_bold, a.text_xl, {color: 'white'}]}> 137 + {starterPack.record.name} 138 + </Text> 139 + <Text style={[{color: 'white'}]}> 140 + {starterPack.feeds?.length ? ( 141 + <Trans> 142 + You'll follow the suggested users and feeds once you 143 + finish creating your account! 144 + </Trans> 145 + ) : ( 146 + <Trans> 147 + You'll follow the suggested users once you finish 148 + creating your account! 149 + </Trans> 150 + )} 151 + </Text> 152 + </LinearGradientBackground> 153 + </Animated.View> 154 + ) : null} 155 + <LayoutAnimationConfig skipEntering> 156 + <ScreenTransition 157 + key={state.activeStep} 158 + direction={state.screenTransitionDirection}> 159 + <View 160 + style={[ 161 + a.flex_1, 162 + a.px_xl, 163 + a.pt_2xl, 164 + !gtMobile && {paddingBottom: 100}, 165 + ]}> 166 + <View style={[a.gap_sm, a.pb_3xl]}> 167 + <Text style={[a.font_bold, t.atoms.text_contrast_medium]}> 168 + <Trans> 169 + Step {state.activeStep + 1} of{' '} 170 + {state.serviceDescription && 171 + !state.serviceDescription.phoneVerificationRequired 172 + ? '2' 173 + : '3'} 174 + </Trans> 175 + </Text> 176 + <Text style={[a.text_3xl, a.font_bold]}> 177 + {state.activeStep === SignupStep.INFO ? ( 178 + <Trans>Your account</Trans> 179 + ) : state.activeStep === SignupStep.HANDLE ? ( 180 + <Trans>Choose your username</Trans> 181 + ) : ( 182 + <Trans>Complete the challenge</Trans> 183 + )} 184 + </Text> 185 + </View> 186 + 187 + {state.activeStep === SignupStep.INFO ? ( 188 + <StepInfo 189 + onPressBack={onPressBack} 190 + isLoadingStarterPack={ 191 + isFetchingStarterPack && !isErrorStarterPack 192 + } 193 + isServerError={isError} 194 + refetchServer={refetch} 195 + /> 196 + ) : state.activeStep === SignupStep.HANDLE ? ( 197 + <StepHandle /> 198 ) : ( 199 + <StepCaptcha /> 200 )} 201 202 + <Divider /> 203 204 + <View 205 + style={[ 206 + a.w_full, 207 + a.py_lg, 208 + a.flex_row, 209 + a.gap_md, 210 + a.align_center, 211 + ]}> 212 + <AppLanguageDropdown /> 213 + <Text 214 + style={[ 215 + a.flex_1, 216 + t.atoms.text_contrast_medium, 217 + !gtMobile && a.text_md, 218 + ]}> 219 + <Trans>Having trouble?</Trans>{' '} 220 + <InlineLinkText 221 + label={_(msg`Contact support`)} 222 + to={FEEDBACK_FORM_URL({email: state.email})} 223 + style={[!gtMobile && a.text_md]}> 224 + <Trans>Contact support</Trans> 225 + </InlineLinkText> 226 + </Text> 227 + </View> 228 + </View> 229 + </ScreenTransition> 230 + </LayoutAnimationConfig> 231 </View> 232 + </LoggedOutLayout> 233 + </SignupContext.Provider> 234 + </Animated.View> 235 ) 236 }
+4 -2
src/screens/Signup/state.ts
··· 41 export type SignupState = { 42 hasPrev: boolean 43 activeStep: SignupStep 44 45 serviceUrl: string 46 serviceDescription?: ServiceDescription ··· 84 export const initialState: SignupState = { 85 hasPrev: false, 86 activeStep: SignupStep.INFO, 87 88 serviceUrl: DEFAULT_SERVICE, 89 serviceDescription: undefined, ··· 126 switch (a.type) { 127 case 'prev': { 128 if (s.activeStep !== SignupStep.INFO) { 129 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 130 next.activeStep-- 131 next.error = '' 132 next.errorField = undefined ··· 135 } 136 case 'next': { 137 if (s.activeStep !== SignupStep.CAPTCHA) { 138 - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) 139 next.activeStep++ 140 next.error = '' 141 next.errorField = undefined
··· 41 export type SignupState = { 42 hasPrev: boolean 43 activeStep: SignupStep 44 + screenTransitionDirection: 'Forward' | 'Backward' 45 46 serviceUrl: string 47 serviceDescription?: ServiceDescription ··· 85 export const initialState: SignupState = { 86 hasPrev: false, 87 activeStep: SignupStep.INFO, 88 + screenTransitionDirection: 'Forward', 89 90 serviceUrl: DEFAULT_SERVICE, 91 serviceDescription: undefined, ··· 128 switch (a.type) { 129 case 'prev': { 130 if (s.activeStep !== SignupStep.INFO) { 131 + next.screenTransitionDirection = 'Backward' 132 next.activeStep-- 133 next.error = '' 134 next.errorField = undefined ··· 137 } 138 case 'next': { 139 if (s.activeStep !== SignupStep.CAPTCHA) { 140 + next.screenTransitionDirection = 'Forward' 141 next.activeStep++ 142 next.error = '' 143 next.errorField = undefined
+2 -2
src/screens/StarterPack/Wizard/StepDetails.tsx
··· 8 import {atoms as a, useTheme} from '#/alf' 9 import * as TextField from '#/components/forms/TextField' 10 import {StarterPack} from '#/components/icons/StarterPack' 11 - import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' 12 import {Text} from '#/components/Typography' 13 14 export function StepDetails() { ··· 23 }) 24 25 return ( 26 - <ScreenTransition direction={state.transitionDirection}> 27 <View style={[a.px_xl, a.gap_xl, a.mt_4xl]}> 28 <View style={[a.gap_md, a.align_center, a.px_md, a.mb_md]}> 29 <StarterPack width={90} gradient="sky" />
··· 8 import {atoms as a, useTheme} from '#/alf' 9 import * as TextField from '#/components/forms/TextField' 10 import {StarterPack} from '#/components/icons/StarterPack' 11 + import {ScreenTransition} from '#/components/ScreenTransition' 12 import {Text} from '#/components/Typography' 13 14 export function StepDetails() { ··· 23 }) 24 25 return ( 26 + <ScreenTransition direction={state.transitionDirection} enabledWeb> 27 <View style={[a.px_xl, a.gap_xl, a.mt_4xl]}> 28 <View style={[a.gap_md, a.align_center, a.px_md, a.mb_md]}> 29 <StarterPack width={90} gradient="sky" />
+5 -2
src/screens/StarterPack/Wizard/StepFeeds.tsx
··· 17 import {SearchInput} from '#/components/forms/SearchInput' 18 import {useThrottledValue} from '#/components/hooks/useThrottledValue' 19 import {Loader} from '#/components/Loader' 20 - import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' 21 import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard' 22 import {Text} from '#/components/Typography' 23 ··· 79 } 80 81 return ( 82 - <ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}> 83 <View style={[a.border_b, t.atoms.border_contrast_medium]}> 84 <View style={[a.py_sm, a.px_md, {height: 60}]}> 85 <SearchInput
··· 17 import {SearchInput} from '#/components/forms/SearchInput' 18 import {useThrottledValue} from '#/components/hooks/useThrottledValue' 19 import {Loader} from '#/components/Loader' 20 + import {ScreenTransition} from '#/components/ScreenTransition' 21 import {WizardFeedCard} from '#/components/StarterPack/Wizard/WizardListCard' 22 import {Text} from '#/components/Typography' 23 ··· 79 } 80 81 return ( 82 + <ScreenTransition 83 + style={[a.flex_1]} 84 + direction={state.transitionDirection} 85 + enabledWeb> 86 <View style={[a.border_b, t.atoms.border_contrast_medium]}> 87 <View style={[a.py_sm, a.px_md, {height: 60}]}> 88 <SearchInput
+5 -2
src/screens/StarterPack/Wizard/StepProfiles.tsx
··· 13 import {atoms as a, useTheme} from '#/alf' 14 import {SearchInput} from '#/components/forms/SearchInput' 15 import {Loader} from '#/components/Loader' 16 - import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition' 17 import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard' 18 import {Text} from '#/components/Typography' 19 import type * as bsky from '#/types/bsky' ··· 64 } 65 66 return ( 67 - <ScreenTransition style={[a.flex_1]} direction={state.transitionDirection}> 68 <View style={[a.border_b, t.atoms.border_contrast_medium]}> 69 <View style={[a.py_sm, a.px_md, {height: 60}]}> 70 <SearchInput
··· 13 import {atoms as a, useTheme} from '#/alf' 14 import {SearchInput} from '#/components/forms/SearchInput' 15 import {Loader} from '#/components/Loader' 16 + import {ScreenTransition} from '#/components/ScreenTransition' 17 import {WizardProfileCard} from '#/components/StarterPack/Wizard/WizardListCard' 18 import {Text} from '#/components/Typography' 19 import type * as bsky from '#/types/bsky' ··· 64 } 65 66 return ( 67 + <ScreenTransition 68 + style={[a.flex_1]} 69 + direction={state.transitionDirection} 70 + enabledWeb> 71 <View style={[a.border_b, t.atoms.border_contrast_medium]}> 72 <View style={[a.py_sm, a.px_md, {height: 60}]}> 73 <SearchInput
+76 -55
src/view/com/auth/SplashScreen.tsx
··· 1 import {View} from 'react-native' 2 import {useSafeAreaInsets} from 'react-native-safe-area-context' 3 import {msg, Trans} from '@lingui/macro' 4 import {useLingui} from '@lingui/react' 5 6 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 7 import {Logo} from '#/view/icons/Logo' 8 import {Logotype} from '#/view/icons/Logotype' 9 import {atoms as a, useTheme} from '#/alf' 10 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 11 import {Button, ButtonText} from '#/components/Button' 12 import {Text} from '#/components/Typography' 13 - import {CenteredView} from '../util/Views' 14 15 export const SplashScreen = ({ 16 onPressSignin, ··· 22 const t = useTheme() 23 const {_} = useLingui() 24 25 const insets = useSafeAreaInsets() 26 27 return ( 28 <CenteredView style={[a.h_full, a.flex_1]}> 29 - <ErrorBoundary> 30 - <View style={[{flex: 1}, a.justify_center, a.align_center]}> 31 - <Logo width={92} fill="sky" /> 32 33 - <View style={[a.pb_sm, a.pt_5xl]}> 34 - <Logotype width={161} fill={t.atoms.text.color} /> 35 </View> 36 37 - <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> 38 - <Trans>What's up?</Trans> 39 - </Text> 40 - </View> 41 - <View 42 - testID="signinOrCreateAccount" 43 - style={[a.px_xl, a.gap_md, a.pb_2xl]}> 44 - <Button 45 - testID="createAccountButton" 46 - onPress={onPressCreateAccount} 47 - label={_(msg`Create new account`)} 48 - accessibilityHint={_( 49 - msg`Opens flow to create a new Bluesky account`, 50 - )} 51 - size="large" 52 - variant="solid" 53 - color="primary"> 54 - <ButtonText> 55 - <Trans>Create account</Trans> 56 - </ButtonText> 57 - </Button> 58 - <Button 59 - testID="signInButton" 60 - onPress={onPressSignin} 61 - label={_(msg`Sign in`)} 62 - accessibilityHint={_( 63 - msg`Opens flow to sign in to your existing Bluesky account`, 64 - )} 65 - size="large" 66 - variant="solid" 67 - color="secondary"> 68 - <ButtonText> 69 - <Trans>Sign in</Trans> 70 - </ButtonText> 71 - </Button> 72 - </View> 73 - <View 74 - style={[ 75 - a.px_lg, 76 - a.pt_md, 77 - a.pb_2xl, 78 - a.justify_center, 79 - a.align_center, 80 - ]}> 81 - <View> 82 - <AppLanguageDropdown /> 83 </View> 84 - </View> 85 - <View style={{height: insets.bottom}} /> 86 - </ErrorBoundary> 87 </CenteredView> 88 ) 89 }
··· 1 import {View} from 'react-native' 2 + import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' 3 import {useSafeAreaInsets} from 'react-native-safe-area-context' 4 import {msg, Trans} from '@lingui/macro' 5 import {useLingui} from '@lingui/react' 6 7 + import {useHaptics} from '#/lib/haptics' 8 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' 9 + import {CenteredView} from '#/view/com/util/Views' 10 import {Logo} from '#/view/icons/Logo' 11 import {Logotype} from '#/view/icons/Logotype' 12 import {atoms as a, useTheme} from '#/alf' 13 import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 14 import {Button, ButtonText} from '#/components/Button' 15 import {Text} from '#/components/Typography' 16 17 export const SplashScreen = ({ 18 onPressSignin, ··· 24 const t = useTheme() 25 const {_} = useLingui() 26 27 + const playHaptic = useHaptics() 28 const insets = useSafeAreaInsets() 29 30 return ( 31 <CenteredView style={[a.h_full, a.flex_1]}> 32 + <Animated.View 33 + entering={FadeIn.duration(90)} 34 + exiting={FadeOut.duration(90)} 35 + style={[a.flex_1]}> 36 + <ErrorBoundary> 37 + <View style={[a.flex_1, a.justify_center, a.align_center]}> 38 + <Logo width={92} fill="sky" /> 39 + 40 + <View style={[a.pb_sm, a.pt_5xl]}> 41 + <Logotype width={161} fill={t.atoms.text.color} /> 42 + </View> 43 44 + <Text 45 + style={[ 46 + a.text_md, 47 + a.font_bold, 48 + t.atoms.text_contrast_medium, 49 + a.text_center, 50 + ]}> 51 + <Trans>What's up?</Trans> 52 + </Text> 53 </View> 54 55 + <View 56 + testID="signinOrCreateAccount" 57 + style={[a.px_xl, a.gap_md, a.pb_2xl]}> 58 + <Button 59 + testID="createAccountButton" 60 + onPress={() => { 61 + onPressCreateAccount() 62 + playHaptic('Light') 63 + }} 64 + label={_(msg`Create new account`)} 65 + accessibilityHint={_( 66 + msg`Opens flow to create a new Bluesky account`, 67 + )} 68 + size="large" 69 + variant="solid" 70 + color="primary"> 71 + <ButtonText> 72 + <Trans>Create account</Trans> 73 + </ButtonText> 74 + </Button> 75 + <Button 76 + testID="signInButton" 77 + onPress={() => { 78 + onPressSignin() 79 + playHaptic('Light') 80 + }} 81 + label={_(msg`Sign in`)} 82 + accessibilityHint={_( 83 + msg`Opens flow to sign in to your existing Bluesky account`, 84 + )} 85 + size="large" 86 + variant="solid" 87 + color="secondary"> 88 + <ButtonText> 89 + <Trans>Sign in</Trans> 90 + </ButtonText> 91 + </Button> 92 + </View> 93 + <View 94 + style={[ 95 + a.px_lg, 96 + a.pt_md, 97 + a.pb_2xl, 98 + a.justify_center, 99 + a.align_center, 100 + ]}> 101 + <View> 102 + <AppLanguageDropdown /> 103 + </View> 104 </View> 105 + <View style={{height: insets.bottom}} /> 106 + </ErrorBoundary> 107 + </Animated.View> 108 </CenteredView> 109 ) 110 }