this repo has no description
1import {useCallback, useEffect, useState} from 'react'
2import {Modal, ScrollView, View} from 'react-native'
3import {SystemBars} from 'react-native-edge-to-edge'
4import {useSafeAreaInsets} from 'react-native-safe-area-context'
5import {msg, plural} from '@lingui/core/macro'
6import {useLingui} from '@lingui/react'
7import {Trans} from '@lingui/react/macro'
8
9import {logger} from '#/logger'
10import {isSignupQueued, useAgent, useSessionApi} from '#/state/session'
11import {useOnboardingDispatch} from '#/state/shell'
12import {Logo} from '#/view/icons/Logo'
13import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf'
14import {Button, ButtonIcon, ButtonText} from '#/components/Button'
15import {Loader} from '#/components/Loader'
16import {P, Text} from '#/components/Typography'
17import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
18
19const COL_WIDTH = 400
20
21export function SignupQueued() {
22 const {_} = useLingui()
23 const t = useTheme()
24 const insets = useSafeAreaInsets()
25 const {gtMobile} = useBreakpoints()
26 const onboardingDispatch = useOnboardingDispatch()
27 const {logoutCurrentAccount} = useSessionApi()
28 const agent = useAgent()
29
30 const [isProcessing, setProcessing] = useState(false)
31 const [estimatedTime, setEstimatedTime] = useState<string | undefined>(
32 undefined,
33 )
34 const [placeInQueue, setPlaceInQueue] = useState<number | undefined>(
35 undefined,
36 )
37
38 const checkStatus = useCallback(async () => {
39 setProcessing(true)
40 try {
41 const res = await agent.com.atproto.temp.checkSignupQueue()
42 if (res.data.activated) {
43 // ready to go, exchange the access token for a usable one and kick off onboarding
44 await agent.sessionManager.refreshSession()
45 if (!isSignupQueued(agent.session?.accessJwt)) {
46 onboardingDispatch({type: 'start'})
47 }
48 } else {
49 // not ready, update UI
50 setEstimatedTime(msToString(res.data.estimatedTimeMs))
51 if (typeof res.data.placeInQueue !== 'undefined') {
52 setPlaceInQueue(Math.max(res.data.placeInQueue, 1))
53 }
54 }
55 } catch (e: any) {
56 logger.error('Failed to check signup queue', {err: e.toString()})
57 } finally {
58 setProcessing(false)
59 }
60 }, [
61 setProcessing,
62 setEstimatedTime,
63 setPlaceInQueue,
64 onboardingDispatch,
65 agent,
66 ])
67
68 useEffect(() => {
69 checkStatus()
70 const interval = setInterval(checkStatus, 60e3)
71 return () => clearInterval(interval)
72 }, [checkStatus])
73
74 const checkBtn = (
75 <Button
76 variant="solid"
77 color="primary"
78 size="large"
79 label={_(msg`Check my status`)}
80 onPress={checkStatus}
81 disabled={isProcessing}>
82 <ButtonText>
83 <Trans>Check my status</Trans>
84 </ButtonText>
85 {isProcessing && <ButtonIcon icon={Loader} />}
86 </Button>
87 )
88
89 const logoutBtn = (
90 <Button
91 variant="ghost"
92 size="large"
93 color="primary"
94 label={_(msg`Sign out`)}
95 onPress={() => logoutCurrentAccount('SignupQueued')}>
96 <ButtonText>
97 <Trans>Sign out</Trans>
98 </ButtonText>
99 </Button>
100 )
101
102 const webLayout = IS_WEB && gtMobile
103
104 return (
105 <Modal
106 visible
107 animationType={native('slide')}
108 presentationStyle="formSheet"
109 style={[web(a.util_screen_outer)]}>
110 {IS_IOS && !IS_LIQUID_GLASS && (
111 <SystemBars style={{statusBar: 'light'}} />
112 )}
113 <ScrollView
114 style={[a.flex_1, t.atoms.bg]}
115 contentContainerStyle={{borderWidth: 0}}
116 bounces={false}>
117 <View
118 style={[
119 a.flex_row,
120 a.justify_center,
121 gtMobile ? a.pt_4xl : [a.px_xl, a.pt_xl],
122 ]}>
123 <View style={[a.flex_1, {maxWidth: COL_WIDTH}]}>
124 <View
125 style={[a.w_full, a.justify_center, a.align_center, a.my_4xl]}>
126 <Logo width={120} />
127 </View>
128
129 <Text style={[a.text_4xl, a.font_bold, a.pb_sm]}>
130 <Trans>You're in line</Trans>
131 </Text>
132 <P style={[t.atoms.text_contrast_medium]}>
133 <Trans>
134 There's been a rush of new users to Bluesky! We'll activate your
135 account as soon as we can.
136 </Trans>
137 </P>
138
139 <View
140 style={[
141 a.rounded_sm,
142 a.px_2xl,
143 a.py_4xl,
144 a.mt_2xl,
145 a.mb_md,
146 a.border,
147 t.atoms.bg_contrast_25,
148 t.atoms.border_contrast_medium,
149 ]}>
150 {typeof placeInQueue === 'number' && (
151 <Text
152 style={[a.text_5xl, a.text_center, a.font_bold, a.mb_2xl]}>
153 {placeInQueue}
154 </Text>
155 )}
156 <P style={[a.text_center]}>
157 {typeof placeInQueue === 'number' ? (
158 <Trans>left to go.</Trans>
159 ) : (
160 <Trans>You are in line.</Trans>
161 )}{' '}
162 {estimatedTime ? (
163 <Trans>
164 We estimate {estimatedTime} until your account is ready.
165 </Trans>
166 ) : (
167 <Trans>
168 We will let you know when your account is ready.
169 </Trans>
170 )}
171 </P>
172 </View>
173
174 {webLayout && (
175 <View
176 style={[
177 a.w_full,
178 a.flex_row,
179 a.justify_between,
180 a.pt_5xl,
181 {paddingBottom: 200},
182 ]}>
183 {logoutBtn}
184 {checkBtn}
185 </View>
186 )}
187 </View>
188 </View>
189 </ScrollView>
190
191 {!webLayout && (
192 <View
193 style={[
194 a.align_center,
195 t.atoms.bg,
196 gtMobile ? a.px_5xl : a.px_xl,
197 {paddingBottom: Math.max(insets.bottom, a.pb_5xl.paddingBottom)},
198 ]}>
199 <View style={[a.w_full, a.gap_sm, {maxWidth: COL_WIDTH}]}>
200 {checkBtn}
201 {logoutBtn}
202 </View>
203 </View>
204 )}
205 </Modal>
206 )
207}
208
209function msToString(ms: number | undefined): string | undefined {
210 if (ms && ms > 0) {
211 const estimatedTimeMins = Math.ceil(ms / 60e3)
212 if (estimatedTimeMins > 59) {
213 const estimatedTimeHrs = Math.round(estimatedTimeMins / 60)
214 if (estimatedTimeHrs > 6) {
215 // dont even bother
216 return undefined
217 }
218 // hours
219 return `${estimatedTimeHrs} ${plural(estimatedTimeHrs, {
220 one: 'hour',
221 other: 'hours',
222 })}`
223 }
224 // minutes
225 return `${estimatedTimeMins} ${plural(estimatedTimeMins, {
226 one: 'minute',
227 other: 'minutes',
228 })}`
229 }
230 return undefined
231}