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