age assurance card

This commit is contained in:
vineyardbovines
2026-05-06 12:41:13 -04:00
parent 0b6ae1cc21
commit 61115c6204
@@ -1,9 +1,8 @@
import {View} from 'react-native' import {View} from 'react-native'
import {msg} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo' import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {regionName} from '#/locale/helpers'
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf' import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
import {Admonition} from '#/components/Admonition' import {Admonition} from '#/components/Admonition'
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog' import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
@@ -25,7 +24,38 @@ import {useAgeAssurance} from '#/ageAssurance'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import {useAnalytics} from '#/analytics' import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env' import {IS_NATIVE} from '#/env'
import {useDeviceGeolocationApi} from '#/geolocation' import {
type Geolocation,
useDeviceGeolocationApi,
useGeolocation,
} from '#/geolocation'
import {USRegionNameToRegionCode} from '#/geolocation/util'
import {device, useStorage} from '#/storage'
const USRegionCodeToRegionName: {[regionCode: string]: string} =
Object.fromEntries(
Object.entries(USRegionNameToRegionCode).map(([name, code]) => [
code,
name,
]),
)
function formatRegion(
geolocation: Geolocation,
appLang: string,
): string | undefined {
const {countryCode, regionCode} = geolocation
if (!countryCode) return undefined
const country = regionName(countryCode, appLang)
// If `regionName` couldn't resolve a real name and fell through to the raw
// code, we'd rather show nothing than a bare ISO code in the prose.
if (country === countryCode) return undefined
if (regionCode && countryCode === 'US') {
const state = USRegionCodeToRegionName[regionCode]
if (state) return `${state}, ${country}`
}
return country
}
export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) { export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
const aa = useAgeAssurance() const aa = useAgeAssurance()
@@ -42,15 +72,12 @@ export function AgeAssuranceAccountCard({style}: ViewStyleProp & {}) {
function Inner({style}: ViewStyleProp & {}) { function Inner({style}: ViewStyleProp & {}) {
const t = useTheme() const t = useTheme()
const {_, i18n} = useLingui() const {t: l, i18n} = useLingui()
const ax = useAnalytics() const ax = useAnalytics()
const control = useDialogControl() const control = useDialogControl()
const appealControl = Dialog.useDialogControl() const appealControl = Dialog.useDialogControl()
const locationControl = Dialog.useDialogControl()
const getTimeAgo = useGetTimeAgo() const getTimeAgo = useGetTimeAgo()
const {gtPhone} = useBreakpoints() const {gtPhone} = useBreakpoints()
const {setDeviceGeolocation} = useDeviceGeolocationApi()
const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
const copy = useAgeAssuranceCopy() const copy = useAgeAssuranceCopy()
const aa = useAgeAssurance() const aa = useAgeAssurance()
@@ -72,7 +99,12 @@ function Inner({style}: ViewStyleProp & {}) {
<View style={style}> <View style={style}>
<View <View
style={[a.p_lg, a.rounded_md, a.border, t.atoms.border_contrast_low]}> style={[
a.p_lg,
a.rounded_md,
a.border,
t.atoms.border_contrast_low,
]}>
<View <View
style={[ style={[
a.flex_row, a.flex_row,
@@ -88,16 +120,19 @@ function Inner({style}: ViewStyleProp & {}) {
</View> </View>
<View style={[a.pb_md, a.gap_sm]}> <View style={[a.pb_md, a.gap_sm]}>
<Text style={[a.text_sm, a.leading_snug]}>{copy.notice}</Text> <Text style={[a.text_sm, a.leading_snug]}>
{copy.notice}
</Text>
{hasCompletedFlow && ( {hasCompletedFlow && (
<Text style={[a.text_sm, a.leading_snug]}> <Text style={[a.text_sm, a.leading_snug]}>
<Trans> <Trans>
If you are 18 years of age or older and want to try again, If you are 18 years of age or older and want
click the button below and use a different verification method to try again, click the button below and use
if one is available in your region. If you have questions or a different verification method if one is
concerns,{' '} available in your region. If you have
questions or concerns,{' '}
<InlineLinkText <InlineLinkText
label={_(msg`Contact our support team`)} label={l`Contact our support team`}
{...createStaticClick(() => { {...createStaticClick(() => {
appealControl.open() appealControl.open()
})}> })}>
@@ -107,59 +142,22 @@ function Inner({style}: ViewStyleProp & {}) {
</Text> </Text>
)} )}
{IS_NATIVE && ( <RegionNotice />
<>
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>
Is your location not accurate?{' '}
<InlineLinkText
label={_(msg`Confirm your location`)}
{...createStaticClick(() => {
locationControl.open()
})}>
Tap here to confirm your location.
</InlineLinkText>{' '}
</Trans>
</Text>
<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>
{isBlocked ? ( {isBlocked ? (
<Admonition type="warning"> <Admonition type="warning">
<Trans> <Trans>
You are currently unable to access Bluesky's Age Assurance flow. You are currently unable to access Bluesky's Age
Please{' '} Assurance flow. Please{' '}
<InlineLinkText <InlineLinkText
label={_(msg`Contact our moderation team`)} label={l`Contact our moderation team`}
{...createStaticClick(() => { {...createStaticClick(() => {
appealControl.open() appealControl.open()
ax.metric('ageAssurance:appealDialogOpen', {}) ax.metric(
'ageAssurance:appealDialogOpen',
{},
)
})}> })}>
contact our moderation team contact our moderation team
</InlineLinkText>{' '} </InlineLinkText>{' '}
@@ -182,15 +180,21 @@ function Inner({style}: ViewStyleProp & {}) {
: [a.gap_md], : [a.gap_md],
]}> ]}>
<Button <Button
label={_(msg`Verify now`)} label={l`Verify now`}
size="small" size="small"
variant="solid" variant="solid"
color={hasInitiated ? 'secondary' : 'primary'} color={
hasInitiated ? 'secondary' : 'primary'
}
onPress={() => { onPress={() => {
control.open() control.open()
ax.metric('ageAssurance:initDialogOpen', { ax.metric(
hasInitiatedPreviously: hasInitiated, 'ageAssurance:initDialogOpen',
}) {
hasInitiatedPreviously:
hasInitiated,
},
)
}}> }}>
<ButtonText> <ButtonText>
{hasInitiated ? ( {hasInitiated ? (
@@ -203,21 +207,36 @@ function Inner({style}: ViewStyleProp & {}) {
{lastInitiatedAt && timeAgo && diff ? ( {lastInitiatedAt && timeAgo && diff ? (
<Text <Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]} style={[
a.text_sm,
a.italic,
t.atoms.text_contrast_medium,
]}
title={i18n.date(lastInitiatedAt, { title={i18n.date(lastInitiatedAt, {
dateStyle: 'medium', dateStyle: 'medium',
timeStyle: 'medium', timeStyle: 'medium',
})}> })}>
{diff.value === 0 ? ( {diff.value === 0 ? (
<Trans>Last initiated just now</Trans> <Trans>
Last initiated just now
</Trans>
) : ( ) : (
<Trans>Last initiated {timeAgo} ago</Trans> <Trans>
Last initiated {timeAgo} ago
</Trans>
)} )}
</Text> </Text>
) : ( ) : (
<Text <Text
style={[a.text_sm, a.italic, t.atoms.text_contrast_medium]}> style={[
<Trans>Age assurance only takes a few minutes</Trans> a.text_sm,
a.italic,
t.atoms.text_contrast_medium,
]}>
<Trans>
Age assurance only takes a few
minutes
</Trans>
</Text> </Text>
)} )}
</View> </View>
@@ -228,3 +247,101 @@ function Inner({style}: ViewStyleProp & {}) {
</> </>
) )
} }
function RegionNotice() {
const {t: l, i18n} = useLingui()
const aa = useAgeAssurance()
const geolocation = useGeolocation()
const [deviceGeolocation] = useStorage(device, ['deviceGeolocation'])
const {setDeviceGeolocation} = useDeviceGeolocationApi()
const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
const locationControl = Dialog.useDialogControl()
const region = formatRegion(geolocation, i18n.locale)
if (!region) return null
const isGPS = !!deviceGeolocation?.countryCode
return (
<>
{IS_NATIVE && (
<DeviceLocationRequestDialog
control={locationControl}
onLocationAcquired={props => {
const access = computeAgeAssuranceRegionAccess(
props.geolocation,
)
if (access !== aa.Access.Full) {
props.disableDialogAction()
props.setDialogError(
l`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(l`Thanks! You're all set.`, {
type: 'success',
})
})
}
}}
/>
)}
<Text style={[a.text_sm, a.leading_snug]}>
{isGPS ? (
<Trans>
Based on your device's location, we think you're in{' '}
<Text style={[a.text_sm, a.font_bold]}>{region}</Text>.
Some features are limited because of regional age laws.
</Trans>
) : (
<Trans>
Based on your network, we think you're in{' '}
<Text style={[a.text_sm, a.font_bold]}>{region}</Text>.
Some features are limited because of regional age laws.
</Trans>
)}
</Text>
{!isGPS &&
(IS_NATIVE ? (
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>
This estimate may be inaccurate if you're using a
VPN.{' '}
<InlineLinkText
label={l`Confirm your location`}
{...createStaticClick(() => {
locationControl.open()
})}>
Tap here to confirm your location.
</InlineLinkText>
</Trans>
</Text>
) : (
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>
This estimate may be inaccurate if you're using a
VPN.
</Trans>
</Text>
))}
{isGPS && IS_NATIVE && (
<Text style={[a.text_sm, a.leading_snug]}>
<Trans>
<InlineLinkText
label={l`Confirm your location`}
{...createStaticClick(() => {
locationControl.open()
})}>
Tap here to confirm your location.
</InlineLinkText>
</Trans>
</Text>
)}
</>
)
}