forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect} from 'react'
2import {ScrollView, View} from 'react-native'
3import {useSafeAreaInsets} from 'react-native-safe-area-context'
4import {msg, Trans} from '@lingui/macro'
5import {useLingui} from '@lingui/react'
6
7import {
8 SupportCode,
9 useCreateSupportLink,
10} from '#/lib/hooks/useCreateSupportLink'
11import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
12import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
13import {useSessionApi} from '#/state/session'
14import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
15import {Admonition} from '#/components/Admonition'
16import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
17import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
18import {AgeAssuranceInitDialog} from '#/components/ageAssurance/AgeAssuranceInitDialog'
19import {Button, ButtonIcon, ButtonText} from '#/components/Button'
20import {useDialogControl} from '#/components/Dialog'
21import * as Dialog from '#/components/Dialog'
22import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
23import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
24import {Full as Logo} from '#/components/icons/Logo'
25import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
26import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
27import {Outlet as PortalOutlet} from '#/components/Portal'
28import * as Toast from '#/components/Toast'
29import {Text} from '#/components/Typography'
30import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
31import {useAgeAssurance} from '#/ageAssurance'
32import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
33import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
34import {
35 isLegacyBirthdateBug,
36 useAgeAssuranceRegionConfig,
37} from '#/ageAssurance/util'
38import {useAnalytics} from '#/analytics'
39import {IS_NATIVE, IS_WEB} from '#/env'
40import {useDeviceGeolocationApi} from '#/geolocation'
41
42const textStyles = [a.text_md, a.leading_snug]
43
44export function NoAccessScreen() {
45 const t = useTheme()
46 const {_} = useLingui()
47 const ax = useAnalytics()
48 const {gtPhone} = useBreakpoints()
49 const insets = useSafeAreaInsets()
50 const birthdateControl = useDialogControl()
51 const {data} = useAgeAssuranceDataContext()
52 const region = useAgeAssuranceRegionConfig()
53 const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
54 const {logoutCurrentAccount} = useSessionApi()
55 const createSupportLink = useCreateSupportLink()
56
57 const aa = useAgeAssurance()
58 const isBlocked = aa.state.status === aa.Status.Blocked
59 const isAARegion = !!region
60 const hasDeclaredAge = data?.declaredAge !== undefined
61 const canUpdateBirthday =
62 isBirthdateUpdateAllowed || isLegacyBirthdateBug(data?.birthdate || '')
63
64 useEffect(() => {
65 // just counting overall hits here
66 ax.metric(`blockedGeoOverlay:shown`, {})
67 ax.metric(`ageAssurance:noAccessScreen:shown`, {
68 accountCreatedAt: data?.accountCreatedAt || 'unknown',
69 isAARegion,
70 hasDeclaredAge,
71 canUpdateBirthday,
72 })
73 // eslint-disable-next-line react-hooks/exhaustive-deps
74 }, [])
75
76 const onPressLogout = useCallback(() => {
77 if (IS_WEB) {
78 // We're switching accounts, which remounts the entire app.
79 // On mobile, this gets us Home, but on the web we also need reset the URL.
80 // We can't change the URL via a navigate() call because the navigator
81 // itself is about to unmount, and it calls pushState() too late.
82 // So we change the URL ourselves. The navigator will pick it up on remount.
83 history.pushState(null, '', '/')
84 }
85 logoutCurrentAccount('AgeAssuranceNoAccessScreen')
86 }, [logoutCurrentAccount])
87
88 const orgAdmonition = (
89 <Admonition type="tip">
90 <Trans>
91 For organizational accounts, use the birthdate of the person who is
92 responsible for the account.
93 </Trans>
94 </Admonition>
95 )
96
97 const birthdateUpdateText = canUpdateBirthday ? (
98 <>
99 <Text style={[textStyles]}>
100 <Trans>
101 If you believe your birthdate is incorrect, you can update it by{' '}
102 <SimpleInlineLinkText
103 label={_(msg`Click here to update your birthdate`)}
104 style={[textStyles]}
105 {...createStaticClick(() => {
106 ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {})
107 birthdateControl.open()
108 })}>
109 clicking here
110 </SimpleInlineLinkText>
111 .
112 </Trans>
113 </Text>
114
115 {orgAdmonition}
116 </>
117 ) : (
118 <Text style={[textStyles]}>
119 <Trans>
120 If you believe your birthdate is incorrect, please{' '}
121 <SimpleInlineLinkText
122 to={createSupportLink({code: SupportCode.AA_BIRTHDATE})}
123 label={_(msg`Click here to contact our support team`)}
124 style={[textStyles]}>
125 contact our support team
126 </SimpleInlineLinkText>
127 .
128 </Trans>
129 </Text>
130 )
131
132 return (
133 <>
134 <View style={[a.util_screen_outer, a.flex_1]}>
135 <ScrollView
136 contentContainerStyle={[
137 a.px_2xl,
138 {
139 paddingTop: IS_WEB
140 ? a.p_5xl.padding
141 : insets.top + a.p_2xl.padding,
142 paddingBottom: 100,
143 },
144 ]}>
145 <View
146 style={[
147 a.mx_auto,
148 a.w_full,
149 web({
150 maxWidth: 380,
151 paddingTop: gtPhone ? '8vh' : undefined,
152 }),
153 {
154 gap: 32,
155 },
156 ]}>
157 <View style={[a.align_start]}>
158 <AgeAssuranceBadge />
159 </View>
160
161 {hasDeclaredAge ? (
162 <>
163 {isAARegion ? (
164 <>
165 <View style={[a.gap_lg]}>
166 <Text style={[textStyles]}>
167 <Trans>Hey there!</Trans>
168 </Text>
169 <Text style={[textStyles]}>
170 <Trans>
171 You are accessing Bluesky from a region that legally
172 requires us to verify your age before allowing you to
173 access the app.
174 </Trans>
175 </Text>
176
177 {!aa.flags.isOverRegionMinAccessAge && (
178 <Text style={[textStyles]}>
179 <Trans>
180 Unfortunately, your declared age indicates that you
181 are not old enough to access Bluesky in your region.
182 </Trans>
183 </Text>
184 )}
185
186 {!isBlocked && birthdateUpdateText}
187 </View>
188
189 {aa.flags.isOverRegionMinAccessAge && <AccessSection />}
190 </>
191 ) : (
192 <View style={[a.gap_lg]}>
193 <Text style={[textStyles]}>
194 <Trans>
195 Unfortunately, the birthdate you have saved to your
196 profile makes you too young to access Bluesky.
197 </Trans>
198 </Text>
199
200 {birthdateUpdateText}
201 </View>
202 )}
203 </>
204 ) : (
205 <View style={[a.gap_lg]}>
206 <Text style={[textStyles]}>
207 <Trans>Hi there!</Trans>
208 </Text>
209 <Text style={[textStyles]}>
210 <Trans>
211 In order to provide an age-appropriate experience, we need
212 to know your birthdate. This is a one-time thing, and your
213 data will be kept private.
214 </Trans>
215 </Text>
216 <Text style={[textStyles]}>
217 <Trans>
218 Set your birthdate below and we'll get you back to posting
219 and exploring in no time!
220 </Trans>
221 </Text>
222 <Button
223 color="primary"
224 size="large"
225 label={_(msg`Click here to update your birthdate`)}
226 onPress={() => birthdateControl.open()}>
227 <ButtonText>
228 <Trans>Add your birthdate</Trans>
229 </ButtonText>
230 </Button>
231
232 {orgAdmonition}
233 </View>
234 )}
235
236 <View style={[a.pt_lg, a.gap_xl]}>
237 <Logo width={120} textFill={t.atoms.text.color} />
238 <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
239 <Trans>
240 To log out,{' '}
241 <SimpleInlineLinkText
242 label={_(msg`Click here to log out`)}
243 {...createStaticClick(() => {
244 onPressLogout()
245 })}>
246 click here
247 </SimpleInlineLinkText>
248 .
249 </Trans>
250 </Text>
251 </View>
252 </View>
253 </ScrollView>
254 </View>
255
256 <BirthDateSettingsDialog control={birthdateControl} />
257
258 {/*
259 * While this blocking overlay is up, other dialogs in the shell
260 * are not mounted, so it _should_ be safe to use these here
261 * without fear of other modals showing up.
262 */}
263 <BottomSheetOutlet />
264 <PortalOutlet />
265 </>
266 )
267}
268
269function AccessSection() {
270 const t = useTheme()
271 const {_, i18n} = useLingui()
272 const ax = useAnalytics()
273 const control = useDialogControl()
274 const appealControl = Dialog.useDialogControl()
275 const locationControl = Dialog.useDialogControl()
276 const getTimeAgo = useGetTimeAgo()
277 const {setDeviceGeolocation} = useDeviceGeolocationApi()
278 const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
279
280 const aa = useAgeAssurance()
281 const {status, lastInitiatedAt} = aa.state
282 const isBlocked = status === aa.Status.Blocked
283 const hasInitiated = !!lastInitiatedAt
284 const timeAgo = lastInitiatedAt
285 ? getTimeAgo(lastInitiatedAt, new Date())
286 : null
287 const diff = lastInitiatedAt
288 ? dateDiff(lastInitiatedAt, new Date(), 'down')
289 : null
290
291 return (
292 <>
293 <AgeAssuranceInitDialog control={control} />
294 <AgeAssuranceAppealDialog control={appealControl} />
295
296 <View style={[a.gap_xl]}>
297 {isBlocked ? (
298 <Admonition type="warning">
299 <Trans>
300 You are currently unable to access Bluesky's Age Assurance flow.
301 Please{' '}
302 <SimpleInlineLinkText
303 label={_(msg`Contact our moderation team`)}
304 {...createStaticClick(() => {
305 appealControl.open()
306 ax.metric('ageAssurance:appealDialogOpen', {})
307 })}>
308 contact our moderation team
309 </SimpleInlineLinkText>{' '}
310 if you believe this is an error.
311 </Trans>
312 </Admonition>
313 ) : (
314 <>
315 <View style={[a.gap_md]}>
316 <Button
317 label={_(msg`Verify now`)}
318 size="large"
319 color={hasInitiated ? 'secondary' : 'primary'}
320 onPress={() => {
321 control.open()
322 ax.metric('ageAssurance:initDialogOpen', {
323 hasInitiatedPreviously: hasInitiated,
324 })
325 }}>
326 <ButtonIcon icon={ShieldIcon} />
327 <ButtonText>
328 {hasInitiated ? (
329 <Trans>Verify again</Trans>
330 ) : (
331 <Trans>Verify now</Trans>
332 )}
333 </ButtonText>
334 </Button>
335
336 {lastInitiatedAt && timeAgo && diff ? (
337 <Text
338 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}
339 title={i18n.date(lastInitiatedAt, {
340 dateStyle: 'medium',
341 timeStyle: 'medium',
342 })}>
343 {diff.value === 0 ? (
344 <Trans>Last initiated just now</Trans>
345 ) : (
346 <Trans>Last initiated {timeAgo} ago</Trans>
347 )}
348 </Text>
349 ) : (
350 <Text
351 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
352 <Trans>Age assurance only takes a few minutes</Trans>
353 </Text>
354 )}
355 </View>
356 </>
357 )}
358
359 <View style={[a.gap_xs]}>
360 {IS_NATIVE && (
361 <>
362 <Admonition>
363 <Trans>
364 Is your location not accurate?{' '}
365 <SimpleInlineLinkText
366 label={_(msg`Confirm your location`)}
367 {...createStaticClick(() => {
368 locationControl.open()
369 })}>
370 Tap here to confirm your location.
371 </SimpleInlineLinkText>{' '}
372 </Trans>
373 </Admonition>
374
375 <DeviceLocationRequestDialog
376 control={locationControl}
377 onLocationAcquired={props => {
378 const access = computeAgeAssuranceRegionAccess(
379 props.geolocation,
380 )
381 if (access !== aa.Access.Full) {
382 props.disableDialogAction()
383 props.setDialogError(
384 _(
385 msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`,
386 ),
387 )
388 } else {
389 props.closeDialog(() => {
390 // set this after close!
391 setDeviceGeolocation(props.geolocation)
392 Toast.show(_(msg`Thanks! You're all set.`), {
393 type: 'success',
394 })
395 })
396 }
397 }}
398 />
399 </>
400 )}
401 </View>
402 </View>
403 </>
404 )
405}