diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx
index 09dc1e976e..b82594ab61 100644
--- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx
+++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx
@@ -1,17 +1,16 @@
import {View} from 'react-native'
-import {msg} from '@lingui/core/macro'
-import {useLingui} from '@lingui/react'
-import {Trans} from '@lingui/react/macro'
+import {Trans, useLingui} from '@lingui/react/macro'
import {dateDiff, useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
+import {regionName} from '#/locale/helpers'
import {atoms as a, useBreakpoints, useTheme, type ViewStyleProp} from '#/alf'
import {Admonition} from '#/components/Admonition'
import {AgeAssuranceAppealDialog} from '#/components/ageAssurance/AgeAssuranceAppealDialog'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {AgeAssuranceConfigUnavailableError} from '#/components/ageAssurance/AgeAssuranceErrors'
import {
- AgeAssuranceInitDialog,
- useDialogControl,
+ AgeAssuranceInitDialog,
+ useDialogControl,
} from '#/components/ageAssurance/AgeAssuranceInitDialog'
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
import {Button, ButtonText} from '#/components/Button'
@@ -25,206 +24,324 @@ import {useAgeAssurance} from '#/ageAssurance'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import {useAnalytics} from '#/analytics'
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 & {}) {
- const aa = useAgeAssurance()
- if (aa.state.access === aa.Access.Full) return null
- if (aa.state.error === 'config') {
- return (
-
-
-
- )
- }
- return
+ const aa = useAgeAssurance()
+ if (aa.state.access === aa.Access.Full) return null
+ if (aa.state.error === 'config') {
+ return (
+
+
+
+ )
+ }
+ return
}
function Inner({style}: ViewStyleProp & {}) {
- const t = useTheme()
- const {_, i18n} = useLingui()
- const ax = useAnalytics()
- const control = useDialogControl()
- const appealControl = Dialog.useDialogControl()
- const locationControl = Dialog.useDialogControl()
- const getTimeAgo = useGetTimeAgo()
- const {gtPhone} = useBreakpoints()
- const {setDeviceGeolocation} = useDeviceGeolocationApi()
- const computeAgeAssuranceRegionAccess = useComputeAgeAssuranceRegionAccess()
+ const t = useTheme()
+ const {t: l, i18n} = useLingui()
+ const ax = useAnalytics()
+ const control = useDialogControl()
+ const appealControl = Dialog.useDialogControl()
+ const getTimeAgo = useGetTimeAgo()
+ const {gtPhone} = useBreakpoints()
- const copy = useAgeAssuranceCopy()
- const aa = useAgeAssurance()
- const {status, lastInitiatedAt} = aa.state
- const isBlocked = status === aa.Status.Blocked
- const hasInitiated = !!lastInitiatedAt
- const hasCompletedFlow = status === aa.Status.Assured
- const timeAgo = lastInitiatedAt
- ? getTimeAgo(lastInitiatedAt, new Date())
- : null
- const diff = lastInitiatedAt
- ? dateDiff(lastInitiatedAt, new Date(), 'down')
- : null
+ const copy = useAgeAssuranceCopy()
+ const aa = useAgeAssurance()
+ const {status, lastInitiatedAt} = aa.state
+ const isBlocked = status === aa.Status.Blocked
+ const hasInitiated = !!lastInitiatedAt
+ const hasCompletedFlow = status === aa.Status.Assured
+ const timeAgo = lastInitiatedAt
+ ? getTimeAgo(lastInitiatedAt, new Date())
+ : null
+ const diff = lastInitiatedAt
+ ? dateDiff(lastInitiatedAt, new Date(), 'down')
+ : null
- return (
- <>
-
-
+ return (
+ <>
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ {copy.notice}
+
+ {hasCompletedFlow && (
+
+
+ If you are 18 years of age or older and want
+ to try again, click the button below and use
+ a different verification method if one is
+ available in your region. If you have
+ questions or concerns,{' '}
+ {
+ appealControl.open()
+ })}>
+ our support team can help.
+
+
+
+ )}
+
+
+
+
+ {isBlocked ? (
+
+
+ You are currently unable to access Bluesky's Age
+ Assurance flow. Please{' '}
+ {
+ appealControl.open()
+ ax.metric(
+ 'ageAssurance:appealDialogOpen',
+ {},
+ )
+ })}>
+ contact our moderation team
+ {' '}
+ if you believe this is an error.
+
+
+ ) : (
+ <>
+
+
+
+
+ {lastInitiatedAt && timeAgo && diff ? (
+
+ {diff.value === 0 ? (
+
+ Last initiated just now
+
+ ) : (
+
+ Last initiated {timeAgo} ago
+
+ )}
+
+ ) : (
+
+
+ Age assurance only takes a few
+ minutes
+
+
+ )}
+
+ >
+ )}
+
-
-
-
- {copy.notice}
- {hasCompletedFlow && (
-
-
- If you are 18 years of age or older and want to try again,
- click the button below and use a different verification method
- if one is available in your region. If you have questions or
- concerns,{' '}
- {
- appealControl.open()
- })}>
- our support team can help.
-
-
-
- )}
-
- {IS_NATIVE && (
- <>
-
-
- Is your location not accurate?{' '}
- {
- locationControl.open()
- })}>
- Tap here to confirm your location.
- {' '}
-
-
-
- {
- 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',
- })
- })
- }
- }}
- />
- >
- )}
-
-
- {isBlocked ? (
-
-
- You are currently unable to access Bluesky's Age Assurance flow.
- Please{' '}
- {
- appealControl.open()
- ax.metric('ageAssurance:appealDialogOpen', {})
- })}>
- contact our moderation team
- {' '}
- if you believe this is an error.
-
-
- ) : (
- <>
-
-
-
-
- {lastInitiatedAt && timeAgo && diff ? (
-
- {diff.value === 0 ? (
- Last initiated just now
- ) : (
- Last initiated {timeAgo} ago
- )}
-
- ) : (
-
- Age assurance only takes a few minutes
-
- )}
-
- >
- )}
-
-
- >
- )
+ >
+ )
+}
+
+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 && (
+ {
+ 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',
+ })
+ })
+ }
+ }}
+ />
+ )}
+
+
+ {isGPS ? (
+
+ Based on your device's location, we think you're in{' '}
+ {region}.
+ Some features are limited because of regional age laws.
+
+ ) : (
+
+ Based on your network, we think you're in{' '}
+ {region}.
+ Some features are limited because of regional age laws.
+
+ )}
+
+
+ {!isGPS &&
+ (IS_NATIVE ? (
+
+
+ This estimate may be inaccurate if you're using a
+ VPN.{' '}
+ {
+ locationControl.open()
+ })}>
+ Tap here to confirm your location.
+
+
+
+ ) : (
+
+
+ This estimate may be inaccurate if you're using a
+ VPN.
+
+
+ ))}
+
+ {isGPS && IS_NATIVE && (
+
+
+ {
+ locationControl.open()
+ })}>
+ Tap here to confirm your location.
+
+
+
+ )}
+ >
+ )
}