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