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 {isWeb} from '#/platform/detection'
14import {isNative} from '#/platform/detection'
15import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
16import {useSessionApi} from '#/state/session'
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 {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 (isWeb) {
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: isWeb
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 {!isBlocked && birthdateUpdateText}
181 </View>
182
183 <AccessSection />
184 </>
185 ) : (
186 <View style={[a.gap_lg]}>
187 <Text style={[textStyles]}>
188 <Trans>
189 Unfortunately, the birthdate you have saved to your
190 profile makes you too young to access Bluesky.
191 </Trans>
192 </Text>
193
194 {birthdateUpdateText}
195 </View>
196 )}
197 </>
198 ) : (
199 <View style={[a.gap_lg]}>
200 <Text style={[textStyles]}>
201 <Trans>Hi there!</Trans>
202 </Text>
203 <Text style={[textStyles]}>
204 <Trans>
205 In order to provide an age-appropriate experience, we need
206 to know your birthdate. This is a one-time thing, and your
207 data will be kept private.
208 </Trans>
209 </Text>
210 <Text style={[textStyles]}>
211 <Trans>
212 Set your birthdate below and we'll get you back to posting
213 and exploring in no time!
214 </Trans>
215 </Text>
216 <Button
217 color="primary"
218 size="large"
219 label={_(msg`Click here to update your birthdate`)}
220 onPress={() => birthdateControl.open()}>
221 <ButtonText>
222 <Trans>Add your birthdate</Trans>
223 </ButtonText>
224 </Button>
225
226 {orgAdmonition}
227 </View>
228 )}
229
230 <View style={[a.pt_lg, a.gap_xl]}>
231 <Logo width={120} textFill={t.atoms.text.color} />
232 <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
233 <Trans>
234 To log out,{' '}
235 <SimpleInlineLinkText
236 label={_(msg`Click here to log out`)}
237 {...createStaticClick(() => {
238 onPressLogout()
239 })}>
240 click here
241 </SimpleInlineLinkText>
242 .
243 </Trans>
244 </Text>
245 </View>
246 </View>
247 </ScrollView>
248 </View>
249
250 <BirthDateSettingsDialog control={birthdateControl} />
251
252 {/*
253 * While this blocking overlay is up, other dialogs in the shell
254 * are not mounted, so it _should_ be safe to use these here
255 * without fear of other modals showing up.
256 */}
257 <BottomSheetOutlet />
258 <PortalOutlet />
259 </>
260 )
261}
262
263function AccessSection() {
264 const t = useTheme()
265 const {_, i18n} = useLingui()
266 const control = useDialogControl()
267 const appealControl = Dialog.useDialogControl()
268 const locationControl = Dialog.useDialogControl()
269 const getTimeAgo = useGetTimeAgo()
270 const {setDeviceGeolocation} = useDeviceGeolocationApi()
271 const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
272
273 const aa = useAgeAssurance()
274 const {status, lastInitiatedAt} = aa.state
275 const isBlocked = status === aa.Status.Blocked
276 const hasInitiated = !!lastInitiatedAt
277 const timeAgo = lastInitiatedAt
278 ? getTimeAgo(lastInitiatedAt, new Date())
279 : null
280 const diff = lastInitiatedAt
281 ? dateDiff(lastInitiatedAt, new Date(), 'down')
282 : null
283
284 return (
285 <>
286 <AgeAssuranceInitDialog control={control} />
287 <AgeAssuranceAppealDialog control={appealControl} />
288
289 <View style={[a.gap_xl]}>
290 {isBlocked ? (
291 <Admonition type="warning">
292 <Trans>
293 You are currently unable to access Bluesky's Age Assurance flow.
294 Please{' '}
295 <SimpleInlineLinkText
296 label={_(msg`Contact our moderation team`)}
297 {...createStaticClick(() => {
298 appealControl.open()
299 logger.metric('ageAssurance:appealDialogOpen', {})
300 })}>
301 contact our moderation team
302 </SimpleInlineLinkText>{' '}
303 if you believe this is an error.
304 </Trans>
305 </Admonition>
306 ) : (
307 <>
308 <View style={[a.gap_md]}>
309 <Button
310 label={_(msg`Verify now`)}
311 size="large"
312 color={hasInitiated ? 'secondary' : 'primary'}
313 onPress={() => {
314 control.open()
315 logger.metric('ageAssurance:initDialogOpen', {
316 hasInitiatedPreviously: hasInitiated,
317 })
318 }}>
319 <ButtonIcon icon={ShieldIcon} />
320 <ButtonText>
321 {hasInitiated ? (
322 <Trans>Verify again</Trans>
323 ) : (
324 <Trans>Verify now</Trans>
325 )}
326 </ButtonText>
327 </Button>
328
329 {lastInitiatedAt && timeAgo && diff ? (
330 <Text
331 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}
332 title={i18n.date(lastInitiatedAt, {
333 dateStyle: 'medium',
334 timeStyle: 'medium',
335 })}>
336 {diff.value === 0 ? (
337 <Trans>Last initiated just now</Trans>
338 ) : (
339 <Trans>Last initiated {timeAgo} ago</Trans>
340 )}
341 </Text>
342 ) : (
343 <Text
344 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
345 <Trans>Age assurance only takes a few minutes</Trans>
346 </Text>
347 )}
348 </View>
349 </>
350 )}
351
352 <View style={[a.gap_xs]}>
353 {isNative && (
354 <>
355 <Admonition>
356 <Trans>
357 Is your location not accurate?{' '}
358 <SimpleInlineLinkText
359 label={_(msg`Confirm your location`)}
360 {...createStaticClick(() => {
361 locationControl.open()
362 })}>
363 Tap here to confirm your location.
364 </SimpleInlineLinkText>{' '}
365 </Trans>
366 </Admonition>
367
368 <DeviceLocationRequestDialog
369 control={locationControl}
370 onLocationAcquired={props => {
371 const access = computeAgeAssuranceRegionAccess(
372 props.geolocation,
373 )
374 if (access !== aa.Access.Full) {
375 props.disableDialogAction()
376 props.setDialogError(
377 _(
378 msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`,
379 ),
380 )
381 } else {
382 props.closeDialog(() => {
383 // set this after close!
384 setDeviceGeolocation(props.geolocation)
385 Toast.show(_(msg`Thanks! You're all set.`), {
386 type: 'success',
387 })
388 })
389 }
390 }}
391 />
392 </>
393 )}
394 </View>
395 </View>
396 </>
397 )
398}