Bluesky app fork with some witchin' additions 💫
witchsky.app
bluesky
fork
client
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}