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