Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
at main 258 lines 9.0 kB view raw
1import {useEffect, useReducer, useState} from 'react' 2import {AppState, type AppStateStatus, View} from 'react-native' 3import ReactNativeDeviceAttest from 'react-native-device-attest' 4import Animated, {FadeIn, LayoutAnimationConfig} from 'react-native-reanimated' 5import {AppBskyGraphStarterpack} from '@atproto/api' 6import {msg} from '@lingui/core/macro' 7import {useLingui} from '@lingui/react' 8import {Trans} from '@lingui/react/macro' 9 10import {FEEDBACK_FORM_URL} from '#/lib/constants' 11import {logger} from '#/logger' 12import {useServiceQuery} from '#/state/queries/service' 13import {useStarterPackQuery} from '#/state/queries/starter-packs' 14import {useActiveStarterPack} from '#/state/shell/starter-pack' 15import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' 16import { 17 initialState, 18 reducer, 19 SignupContext, 20 SignupStep, 21 useSubmitSignup, 22} from '#/screens/Signup/state' 23import {StepCaptcha} from '#/screens/Signup/StepCaptcha' 24import {StepHandle} from '#/screens/Signup/StepHandle' 25import {StepInfo} from '#/screens/Signup/StepInfo' 26import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' 27import {AppLanguageDropdown} from '#/components/AppLanguageDropdown' 28import {Divider} from '#/components/Divider' 29import {LinearGradientBackground} from '#/components/LinearGradientBackground' 30import {InlineLinkText} from '#/components/Link' 31import {ScreenTransition} from '#/components/ScreenTransition' 32import {Text} from '#/components/Typography' 33import {useAnalytics} from '#/analytics' 34import {GCP_PROJECT_ID, IS_ANDROID} from '#/env' 35import * as bsky from '#/types/bsky' 36 37export function Signup({ 38 onPressBack, 39 onPressSignIn, 40}: { 41 onPressBack: () => void 42 onPressSignIn: () => void 43}) { 44 const ax = useAnalytics() 45 const {_} = useLingui() 46 const t = useTheme() 47 const [state, dispatch] = useReducer(reducer, { 48 ...initialState, 49 analytics: ax, 50 }) 51 const {gtMobile} = useBreakpoints() 52 const submit = useSubmitSignup() 53 54 useEffect(() => { 55 dispatch({ 56 type: 'setAnalytics', 57 value: ax, 58 }) 59 }, [ax]) 60 61 const activeStarterPack = useActiveStarterPack() 62 const { 63 data: starterPack, 64 isFetching: isFetchingStarterPack, 65 isError: isErrorStarterPack, 66 } = useStarterPackQuery({ 67 uri: activeStarterPack?.uri, 68 }) 69 70 const [isFetchedAtMount] = useState(starterPack != null) 71 const showStarterPackCard = 72 activeStarterPack?.uri && !isFetchingStarterPack && starterPack 73 74 const { 75 data: serviceInfo, 76 isFetching, 77 isError, 78 refetch, 79 } = useServiceQuery(state.serviceUrl) 80 81 useEffect(() => { 82 if (isFetching) { 83 dispatch({type: 'setIsLoading', value: true}) 84 } else if (!isFetching) { 85 dispatch({type: 'setIsLoading', value: false}) 86 } 87 }, [isFetching]) 88 89 useEffect(() => { 90 if (isError) { 91 dispatch({type: 'setServiceDescription', value: undefined}) 92 dispatch({ 93 type: 'setError', 94 value: _( 95 msg`Unable to contact your service. Please check your Internet connection.`, 96 ), 97 }) 98 } else if (serviceInfo) { 99 dispatch({type: 'setServiceDescription', value: serviceInfo}) 100 dispatch({type: 'setError', value: ''}) 101 } 102 }, [_, serviceInfo, isError]) 103 104 useEffect(() => { 105 if (state.pendingSubmit) { 106 if (!state.pendingSubmit.mutableProcessed) { 107 state.pendingSubmit.mutableProcessed = true 108 submit(state, dispatch) 109 } 110 } 111 }, [state, dispatch, submit]) 112 113 // Track app backgrounding during signup 114 useEffect(() => { 115 const subscription = AppState.addEventListener( 116 'change', 117 (nextAppState: AppStateStatus) => { 118 if (nextAppState === 'background') { 119 dispatch({type: 'incrementBackgroundCount'}) 120 } 121 }, 122 ) 123 124 return () => subscription.remove() 125 }, []) 126 127 // On Android, warmup the Play Integrity API on the signup screen so it is ready by the time we get to the gate screen. 128 useEffect(() => { 129 if (!IS_ANDROID) { 130 return 131 } 132 ReactNativeDeviceAttest.warmupIntegrity(GCP_PROJECT_ID).catch(err => 133 logger.error(err), 134 ) 135 }, []) 136 137 return ( 138 <Animated.View exiting={native(FadeIn.duration(90))} style={a.flex_1}> 139 <SignupContext.Provider value={{state, dispatch}}> 140 <LoggedOutLayout 141 leadin="" 142 title={_(msg`Create Account`)} 143 description={_(msg`Welcome to the Atmosphere!`)} 144 scrollable> 145 <View testID="createAccount" style={a.flex_1}> 146 {showStarterPackCard && 147 bsky.dangerousIsType<AppBskyGraphStarterpack.Record>( 148 starterPack.record, 149 AppBskyGraphStarterpack.isRecord, 150 ) ? ( 151 <Animated.View entering={!isFetchedAtMount ? FadeIn : undefined}> 152 <LinearGradientBackground 153 style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}> 154 <Text style={[a.font_semi_bold, a.text_xl, {color: 'white'}]}> 155 {starterPack.record.name} 156 </Text> 157 <Text style={[{color: 'white'}]}> 158 {starterPack.feeds?.length ? ( 159 <Trans> 160 You'll follow the suggested users and feeds once you 161 finish creating your account! 162 </Trans> 163 ) : ( 164 <Trans> 165 You'll follow the suggested users once you finish 166 creating your account! 167 </Trans> 168 )} 169 </Text> 170 </LinearGradientBackground> 171 </Animated.View> 172 ) : null} 173 <LayoutAnimationConfig skipEntering> 174 <ScreenTransition 175 key={state.activeStep} 176 direction={state.screenTransitionDirection}> 177 <View 178 style={[ 179 a.flex_1, 180 a.px_xl, 181 a.pt_2xl, 182 !gtMobile && {paddingBottom: 100}, 183 ]}> 184 <View style={[a.gap_sm, a.pb_3xl]}> 185 <Text 186 style={[a.font_semi_bold, t.atoms.text_contrast_medium]}> 187 <Trans> 188 Step {state.activeStep + 1} of{' '} 189 {state.serviceDescription && 190 !state.serviceDescription.phoneVerificationRequired 191 ? '2' 192 : '3'} 193 </Trans> 194 </Text> 195 <Text style={[a.text_3xl, a.font_semi_bold]}> 196 {state.activeStep === SignupStep.INFO ? ( 197 <Trans>The Atmosphere </Trans> 198 ) : state.activeStep === SignupStep.HANDLE ? ( 199 <Trans>Choose your username</Trans> 200 ) : ( 201 <Trans>Complete the challenge</Trans> 202 )} 203 </Text> 204 </View> 205 206 <LayoutAnimationConfig skipEntering skipExiting> 207 {state.activeStep === SignupStep.INFO ? ( 208 <StepInfo 209 onPressBack={onPressBack} 210 onPressSignIn={onPressSignIn} 211 isLoadingStarterPack={ 212 isFetchingStarterPack && !isErrorStarterPack 213 } 214 isServerError={isError} 215 refetchServer={refetch} 216 /> 217 ) : state.activeStep === SignupStep.HANDLE ? ( 218 <StepHandle /> 219 ) : ( 220 <StepCaptcha /> 221 )} 222 </LayoutAnimationConfig> 223 224 <Divider /> 225 226 <View 227 style={[ 228 a.w_full, 229 a.py_lg, 230 a.flex_row, 231 a.gap_md, 232 a.align_center, 233 ]}> 234 <AppLanguageDropdown /> 235 <Text 236 style={[ 237 a.flex_1, 238 t.atoms.text_contrast_medium, 239 !gtMobile && a.text_md, 240 ]}> 241 <Trans>Having trouble?</Trans>{' '} 242 <InlineLinkText 243 label={_(msg`Contact support`)} 244 to={FEEDBACK_FORM_URL({email: state.email})} 245 style={[!gtMobile && a.text_md]}> 246 <Trans>Open a Tangled Issue</Trans> 247 </InlineLinkText> 248 </Text> 249 </View> 250 </View> 251 </ScreenTransition> 252 </LayoutAnimationConfig> 253 </View> 254 </LoggedOutLayout> 255 </SignupContext.Provider> 256 </Animated.View> 257 ) 258}