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 }, [])
68
69 const onPressLogout = useCallback(() => {
70 if (isWeb) {
71 // We're switching accounts, which remounts the entire app.
72 // On mobile, this gets us Home, but on the web we also need reset the URL.
73 // We can't change the URL via a navigate() call because the navigator
74 // itself is about to unmount, and it calls pushState() too late.
75 // So we change the URL ourselves. The navigator will pick it up on remount.
76 history.pushState(null, '', '/')
77 }
78 logoutCurrentAccount('AgeAssuranceNoAccessScreen')
79 }, [logoutCurrentAccount])
80
81 const birthdateUpdateText = canUpdateBirthday ? (
82 <Text style={[textStyles]}>
83 <Trans>
84 If you believe your birthdate is incorrect, you can update it by{' '}
85 <SimpleInlineLinkText
86 label={_(msg`Click here to update your birthdate`)}
87 style={[textStyles]}
88 {...createStaticClick(() => {
89 birthdateControl.open()
90 })}>
91 clicking here
92 </SimpleInlineLinkText>
93 .
94 </Trans>
95 </Text>
96 ) : (
97 <Text style={[textStyles]}>
98 <Trans>
99 If you believe your birthdate is incorrect, please{' '}
100 <SimpleInlineLinkText
101 to={createSupportLink({code: SupportCode.AA_BIRTHDATE})}
102 label={_(msg`Click here to contact our support team`)}
103 style={[textStyles]}>
104 contact our support team
105 </SimpleInlineLinkText>
106 .
107 </Trans>
108 </Text>
109 )
110
111 return (
112 <>
113 <ScrollView
114 contentContainerStyle={[
115 a.px_2xl,
116 {
117 paddingTop: isWeb ? a.p_5xl.padding : insets.top + a.p_2xl.padding,
118 paddingBottom: 100,
119 },
120 ]}>
121 <View
122 style={[
123 a.mx_auto,
124 a.w_full,
125 web({
126 maxWidth: 380,
127 paddingTop: gtPhone ? '8vh' : undefined,
128 }),
129 {
130 gap: 32,
131 },
132 ]}>
133 <View style={[a.align_start]}>
134 <AgeAssuranceBadge />
135 </View>
136
137 {hasDeclaredAge ? (
138 <>
139 {isAARegion ? (
140 <>
141 <View style={[a.gap_lg]}>
142 <Text style={[textStyles]}>
143 <Trans>
144 You are accessing Bluesky from a region that legally
145 requires us to verify your age before allowing you to
146 access the app.
147 </Trans>
148 </Text>
149
150 {!isBlocked && birthdateUpdateText}
151 </View>
152
153 <AccessSection />
154 </>
155 ) : (
156 <View style={[a.gap_lg]}>
157 <Text style={[textStyles]}>
158 <Trans>
159 Unfortunately, the birthdate you have saved to your
160 profile makes you too young to access Bluesky.
161 </Trans>
162 </Text>
163
164 {birthdateUpdateText}
165 </View>
166 )}
167 </>
168 ) : (
169 <View style={[a.gap_lg]}>
170 <Text style={[textStyles]}>
171 <Trans>
172 It looks like you haven't added your birthdate. You must
173 provide an accurate date of birth to use Bluesky.
174 </Trans>
175 </Text>
176 <Button
177 color="primary"
178 size="large"
179 label={_(msg`Click here to update your birthdate`)}
180 onPress={() => birthdateControl.open()}>
181 <ButtonText>
182 <Trans>Add your birthdate</Trans>
183 </ButtonText>
184 </Button>
185 </View>
186 )}
187
188 <View style={[a.pt_lg, a.gap_xl]}>
189 <Logo width={120} textFill={t.atoms.text.color} />
190 <Text style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
191 <Trans>
192 To log out,{' '}
193 <SimpleInlineLinkText
194 label={_(msg`Click here to log out`)}
195 {...createStaticClick(() => {
196 onPressLogout()
197 })}>
198 click here
199 </SimpleInlineLinkText>
200 .
201 </Trans>
202 </Text>
203 </View>
204 </View>
205 </ScrollView>
206
207 <BirthDateSettingsDialog control={birthdateControl} />
208
209 {/*
210 * While this blocking overlay is up, other dialogs in the shell
211 * are not mounted, so it _should_ be safe to use these here
212 * without fear of other modals showing up.
213 */}
214 <BottomSheetOutlet />
215 <PortalOutlet />
216 </>
217 )
218}
219
220function AccessSection() {
221 const t = useTheme()
222 const {_, i18n} = useLingui()
223 const control = useDialogControl()
224 const appealControl = Dialog.useDialogControl()
225 const locationControl = Dialog.useDialogControl()
226 const getTimeAgo = useGetTimeAgo()
227 const {setDeviceGeolocation} = useDeviceGeolocationApi()
228 const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
229
230 const aa = useAgeAssurance()
231 const {status, lastInitiatedAt} = aa.state
232 const isBlocked = status === aa.Status.Blocked
233 const hasInitiated = !!lastInitiatedAt
234 const timeAgo = lastInitiatedAt
235 ? getTimeAgo(lastInitiatedAt, new Date())
236 : null
237 const diff = lastInitiatedAt
238 ? dateDiff(lastInitiatedAt, new Date(), 'down')
239 : null
240
241 return (
242 <>
243 <AgeAssuranceInitDialog control={control} />
244 <AgeAssuranceAppealDialog control={appealControl} />
245
246 <View style={[a.gap_xl]}>
247 {isBlocked ? (
248 <Admonition type="warning">
249 <Trans>
250 You are currently unable to access Bluesky's Age Assurance flow.
251 Please{' '}
252 <SimpleInlineLinkText
253 label={_(msg`Contact our moderation team`)}
254 {...createStaticClick(() => {
255 appealControl.open()
256 logger.metric('ageAssurance:appealDialogOpen', {})
257 })}>
258 contact our moderation team
259 </SimpleInlineLinkText>{' '}
260 if you believe this is an error.
261 </Trans>
262 </Admonition>
263 ) : (
264 <>
265 <View style={[a.gap_md]}>
266 <Button
267 label={_(msg`Verify now`)}
268 size="large"
269 color={hasInitiated ? 'secondary' : 'primary'}
270 onPress={() => {
271 control.open()
272 logger.metric('ageAssurance:initDialogOpen', {
273 hasInitiatedPreviously: hasInitiated,
274 })
275 }}>
276 <ButtonIcon icon={ShieldIcon} />
277 <ButtonText>
278 {hasInitiated ? (
279 <Trans>Verify again</Trans>
280 ) : (
281 <Trans>Verify now</Trans>
282 )}
283 </ButtonText>
284 </Button>
285
286 {lastInitiatedAt && timeAgo && diff ? (
287 <Text
288 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}
289 title={i18n.date(lastInitiatedAt, {
290 dateStyle: 'medium',
291 timeStyle: 'medium',
292 })}>
293 {diff.value === 0 ? (
294 <Trans>Last initiated just now</Trans>
295 ) : (
296 <Trans>Last initiated {timeAgo} ago</Trans>
297 )}
298 </Text>
299 ) : (
300 <Text
301 style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
302 <Trans>Age assurance only takes a few minutes</Trans>
303 </Text>
304 )}
305 </View>
306 </>
307 )}
308
309 <View style={[a.gap_xs]}>
310 {isNative && (
311 <>
312 <Admonition>
313 <Trans>
314 Is your location not accurate?{' '}
315 <SimpleInlineLinkText
316 label={_(msg`Confirm your location`)}
317 {...createStaticClick(() => {
318 locationControl.open()
319 })}>
320 Tap here to confirm your location.
321 </SimpleInlineLinkText>{' '}
322 </Trans>
323 </Admonition>
324
325 <DeviceLocationRequestDialog
326 control={locationControl}
327 onLocationAcquired={props => {
328 const access = computeAgeAssuranceRegionAccess(
329 props.geolocation,
330 )
331 if (access !== aa.Access.Full) {
332 props.disableDialogAction()
333 props.setDialogError(
334 _(
335 msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`,
336 ),
337 )
338 } else {
339 props.closeDialog(() => {
340 // set this after close!
341 setDeviceGeolocation(props.geolocation)
342 Toast.show(_(msg`Thanks! You're all set.`), {
343 type: 'success',
344 })
345 })
346 }
347 }}
348 />
349 </>
350 )}
351 </View>
352 </View>
353 </>
354 )
355}