Files
bsky-social-app/src/ageAssurance/components/NoAccessScreen.tsx
T
2026-05-26 10:51:58 -05:00

437 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {useCallback, useEffect} from 'react'
import {ScrollView, View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {
SupportCode,
useCreateSupportLink,
} from '#/lib/hooks/useCreateSupportLink'
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {useIsBirthdateUpdateAllowed} from '#/state/birthdate'
import {useSessionApi} from '#/state/session'
import {DeactivateAccountDialog} from '#/screens/Settings/components/DeactivateAccountDialog'
import {DeleteAccountDialog} from '#/screens/Settings/components/DeleteAccountDialog'
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {AgeAssuranceInitDialog} from '#/components/ageAssurance/AgeAssuranceInitDialog'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {BirthDateSettingsDialog} from '#/components/dialogs/BirthDateSettings'
import {DeviceLocationRequestDialog} from '#/components/dialogs/DeviceLocationRequestDialog'
import {Full as Logo} from '#/components/icons/Logo'
import {ShieldCheck_Stroke2_Corner0_Rounded as ShieldIcon} from '#/components/icons/Shield'
import {createStaticClick, SimpleInlineLinkText} from '#/components/Link'
import {Outlet as PortalOutlet} from '#/components/Portal'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
import {useAgeAssurance} from '#/ageAssurance'
import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import {
isLegacyBirthdateBug,
useAgeAssuranceRegionConfig,
} from '#/ageAssurance/util'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE, IS_WEB} from '#/env'
import {useDeviceGeolocationApi} from '#/geolocation'
const textStyles = [a.text_md, a.leading_snug]
export function NoAccessScreen() {
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const {gtPhone} = useBreakpoints()
const insets = useSafeAreaInsets()
const birthdateControl = useDialogControl()
const deactivateAccountControl = useDialogControl()
const deleteAccountControl = useDialogControl()
const {data} = useAgeAssuranceDataContext()
const region = useAgeAssuranceRegionConfig()
const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
const {logoutCurrentAccount} = useSessionApi()
const createSupportLink = useCreateSupportLink()
const aa = useAgeAssurance()
const isBlocked = aa.state.status === aa.Status.Blocked
const isAARegion = !!region
const hasDeclaredAge = data?.declaredAge !== undefined
const canUpdateBirthday =
isBirthdateUpdateAllowed || isLegacyBirthdateBug(data?.birthdate || '')
useEffect(() => {
// just counting overall hits here
ax.metric(`blockedGeoOverlay:shown`, {})
ax.metric(`ageAssurance:noAccessScreen:shown`, {
accountCreatedAt: data?.accountCreatedAt || 'unknown',
isAARegion,
hasDeclaredAge,
canUpdateBirthday,
})
// TODO This can be cleaned up with useEffectEvent once we're on 19.2
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const onPressLogout = useCallback(() => {
if (IS_WEB) {
// We're switching accounts, which remounts the entire app.
// On mobile, this gets us Home, but on the web we also need reset the URL.
// We can't change the URL via a navigate() call because the navigator
// itself is about to unmount, and it calls pushState() too late.
// So we change the URL ourselves. The navigator will pick it up on remount.
history.pushState(null, '', '/')
}
logoutCurrentAccount('AgeAssuranceNoAccessScreen')
}, [logoutCurrentAccount])
const orgAdmonition = (
<Admonition type="tip">
<Trans>
For organizational accounts, use the birthdate of the person who is
responsible for the account.
</Trans>
</Admonition>
)
const birthdateUpdateText = canUpdateBirthday ? (
<>
<Text style={[textStyles]}>
<Trans>
If you believe your birthdate is incorrect, you can update it by{' '}
<SimpleInlineLinkText
label={_(msg`Click here to update your birthdate`)}
style={[textStyles]}
{...createStaticClick(() => {
ax.metric('ageAssurance:noAccessScreen:openBirthdateDialog', {})
birthdateControl.open()
})}>
clicking here
</SimpleInlineLinkText>
.
</Trans>
</Text>
{orgAdmonition}
</>
) : (
<Text style={[textStyles]}>
<Trans>
If you believe your birthdate is incorrect, please{' '}
<SimpleInlineLinkText
to={createSupportLink({code: SupportCode.AA_BIRTHDATE})}
label={_(msg`Click here to contact our support team`)}
style={[textStyles]}>
contact our support team
</SimpleInlineLinkText>
.
</Trans>
</Text>
)
return (
<>
<View style={[a.util_screen_outer, a.flex_1]}>
<ScrollView
contentContainerStyle={[
a.px_2xl,
{
paddingTop: IS_WEB
? a.p_5xl.padding
: insets.top + a.p_2xl.padding,
paddingBottom: 100,
},
]}>
<View
style={[
a.mx_auto,
a.w_full,
web({
maxWidth: 380,
paddingTop: gtPhone ? '8vh' : undefined,
}),
{
gap: 32,
},
]}>
<View style={[a.align_start]}>
<AgeAssuranceBadge />
</View>
{hasDeclaredAge ? (
<>
{isAARegion ? (
<>
<View style={[a.gap_lg]}>
<Text style={[textStyles]}>
<Trans>Hey there!</Trans>
</Text>
<Text style={[textStyles]}>
<Trans>
You are accessing Bluesky from a region that legally
requires us to verify your age before allowing you to
access the app.
</Trans>
</Text>
{!aa.flags.isOverRegionMinAccessAge && (
<Text style={[textStyles]}>
<Trans>
Unfortunately, your declared age indicates that you
are not old enough to access Bluesky in your region.
</Trans>
</Text>
)}
{!isBlocked && birthdateUpdateText}
</View>
{aa.flags.isOverRegionMinAccessAge && <AccessSection />}
</>
) : (
<View style={[a.gap_lg]}>
<Text style={[textStyles]}>
<Trans>
Unfortunately, the birthdate you have saved to your
profile makes you too young to access Bluesky.
</Trans>
</Text>
{birthdateUpdateText}
</View>
)}
</>
) : (
<View style={[a.gap_lg]}>
<Text style={[textStyles]}>
<Trans>Hi there!</Trans>
</Text>
<Text style={[textStyles]}>
<Trans>
In order to provide an age-appropriate experience, we need
to know your birthdate. This is a one-time thing, and your
data will be kept private.
</Trans>
</Text>
<Text style={[textStyles]}>
<Trans>
Set your birthdate below and we'll get you back to posting
and exploring in no time!
</Trans>
</Text>
<Button
color="primary"
size="large"
label={_(msg`Click here to update your birthdate`)}
onPress={() => birthdateControl.open()}>
<ButtonText>
<Trans>Add your birthdate</Trans>
</ButtonText>
</Button>
{orgAdmonition}
</View>
)}
<View style={[a.pt_lg, a.gap_xl, {maxWidth: 280}]}>
<Logo width={120} textFill={t.atoms.text.color} />
<Text
style={[
a.text_sm,
a.italic,
a.leading_snug,
t.atoms.text_contrast_medium,
]}>
<Trans>
To log out,{' '}
<SimpleInlineLinkText
label={_(msg`Click here to log out`)}
{...createStaticClick(() => {
onPressLogout()
})}
style={[a.italic]}>
click here
</SimpleInlineLinkText>
. Or if youd prefer, you can{' '}
<SimpleInlineLinkText
label={_(msg`Click here to delete your account`)}
{...createStaticClick(() => {
ax.metric(
'ageAssurance:noAccessScreen:openDeleteAccountDialog',
{},
)
deleteAccountControl.open()
})}
style={[a.italic]}>
delete your account
</SimpleInlineLinkText>
.
</Trans>
</Text>
</View>
</View>
</ScrollView>
</View>
<BirthDateSettingsDialog control={birthdateControl} />
<DeactivateAccountDialog control={deactivateAccountControl} />
<DeleteAccountDialog
control={deleteAccountControl}
deactivateDialogControl={deactivateAccountControl}
/>
{/*
* While this blocking overlay is up, other dialogs in the shell
* are not mounted, so it _should_ be safe to use these here
* without fear of other modals showing up.
*/}
<BottomSheetOutlet />
<PortalOutlet />
</>
)
}
function AccessSection() {
const t = useTheme()
const {_, i18n} = useLingui()
const ax = useAnalytics()
const control = useDialogControl()
const appealControl = Dialog.useDialogControl()
const locationControl = Dialog.useDialogControl()
const getTimeAgo = useGetTimeAgo()
const {setDeviceGeolocation} = useDeviceGeolocationApi()
const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
const aa = useAgeAssurance()
const {status, lastInitiatedAt} = aa.state
const isBlocked = status === aa.Status.Blocked
const hasInitiated = !!lastInitiatedAt
const timeAgo = lastInitiatedAt
? getTimeAgo(lastInitiatedAt, new Date())
: null
const diff = lastInitiatedAt
? dateDiff(lastInitiatedAt, new Date(), 'down')
: null
return (
<>
<AgeAssuranceInitDialog control={control} />
<AgeAssuranceAppealDialog control={appealControl} />
<View style={[a.gap_xl]}>
{isBlocked ? (
<Admonition type="warning">
<Trans>
You are currently unable to access Bluesky's Age Assurance flow.
Please{' '}
<SimpleInlineLinkText
label={_(msg`Contact our moderation team`)}
{...createStaticClick(() => {
appealControl.open()
ax.metric('ageAssurance:appealDialogOpen', {})
})}>
contact our moderation team
</SimpleInlineLinkText>{' '}
if you believe this is an error.
</Trans>
</Admonition>
) : (
<>
<View style={[a.gap_md]}>
<Button
label={_(msg`Verify now`)}
size="large"
color={hasInitiated ? 'secondary' : 'primary'}
onPress={() => {
control.open()
ax.metric('ageAssurance:initDialogOpen', {
hasInitiatedPreviously: hasInitiated,
})
}}>
<ButtonIcon icon={ShieldIcon} />
<ButtonText>
{hasInitiated ? (
<Trans>Verify again</Trans>
) : (
<Trans>Verify now</Trans>
)}
</ButtonText>
</Button>
{lastInitiatedAt && timeAgo && diff ? (
<Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}
title={i18n.date(lastInitiatedAt, {
dateStyle: 'medium',
timeStyle: 'medium',
})}>
{diff.value === 0 ? (
<Trans>Last initiated just now</Trans>
) : (
<Trans>Last initiated {timeAgo} ago</Trans>
)}
</Text>
) : (
<Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}>
<Trans>Age assurance only takes a few minutes</Trans>
</Text>
)}
</View>
</>
)}
<View style={[a.gap_xs]}>
{IS_NATIVE && (
<>
<Admonition>
<Trans>
Is your location not accurate?{' '}
<SimpleInlineLinkText
label={_(msg`Update your location`)}
{...createStaticClick(() => {
locationControl.open()
})}>
Tap here to update your location with GPS.
</SimpleInlineLinkText>{' '}
</Trans>
</Admonition>
<DeviceLocationRequestDialog
control={locationControl}
onLocationAcquired={props => {
const access = computeAgeAssuranceRegionAccess(
props.geolocation,
)
if (access !== aa.Access.Full) {
props.disableDialogAction()
props.setDialogError(
_(
msg`We're sorry, but based on your device's location, you are currently located in a region that requires age assurance.`,
),
)
} else {
props.closeDialog(() => {
// set this after close!
setDeviceGeolocation(props.geolocation)
Toast.show(_(msg`Thanks! You're all set.`), {
type: 'success',
})
})
}
}}
/>
</>
)}
</View>
</View>
</>
)
}