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