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