diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx
index 688db28bd8..12d1728730 100644
--- a/src/ageAssurance/components/NoAccessScreen.tsx
+++ b/src/ageAssurance/components/NoAccessScreen.tsx
@@ -6,10 +6,6 @@ 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 {useSession, useSessionApi} from '#/state/session'
@@ -43,6 +39,7 @@ import {logger} from '#/ageAssurance/logger'
import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import {
+ canBirthdateUpdateIncreaseAccess,
createGeolocationString,
getAgeAssuranceDataFromDeviceSignals,
isLegacyBirthdateBug,
@@ -67,7 +64,6 @@ export function NoAccessScreen() {
const region = useAgeAssuranceRegionConfig()
const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
const {logoutCurrentAccount} = useSessionApi()
- const createSupportLink = useCreateSupportLink()
const geolocation = useGeolocation()
const {setDeviceGeolocation} = useDeviceGeolocationApi()
const locationControl = Dialog.useDialogControl()
@@ -77,8 +73,13 @@ export function NoAccessScreen() {
const isBlocked = aa.state.status === aa.Status.Blocked
const isAARegion = !!region
const hasDeclaredAge = aa.flags.hasDeclaredAge
+ const birthdateMightIncreaseAccess = Boolean(
+ region && canBirthdateUpdateIncreaseAccess({region, metadata}),
+ )
const canUpdateBirthday =
- isBirthdateUpdateAllowed || isLegacyBirthdateBug(metadata?.birthdate || '')
+ (isBirthdateUpdateAllowed ||
+ isLegacyBirthdateBug(metadata?.birthdate || '')) &&
+ birthdateMightIncreaseAccess
const geolocationString = createGeolocationString(geolocation, i18n.locale)
const isUsingGPS = !!geolocation.deviceGeolocation?.countryCode && IS_NATIVE
@@ -136,20 +137,7 @@ export function NoAccessScreen() {
{orgAdmonition}
>
- ) : (
-
-
- If you believe your birthdate is incorrect, please{' '}
-
- contact our support team
-
- .
-
-
- )
+ ) : null
return (
<>
@@ -180,149 +168,152 @@ export function NoAccessScreen() {
- {hasDeclaredAge ? (
- <>
- {isAARegion ? (
- <>
-
-
- Hey there!
-
-
-
- You are accessing Bluesky from a region that legally
- requires us to verify your age before allowing you to
- access the app.
-
-
-
- {region && (
+
+ {hasDeclaredAge ? (
+ <>
+ {isAARegion ? (
+ <>
+
- {isUsingGPS ? (
-
- Based on your device's location, we think you're
- in{' '}
-
- {geolocationString}
-
- .
-
- ) : (
- <>
+ Hey there!
+
+
+
+ You are accessing Bluesky from a region that legally
+ requires us to verify your age before allowing you
+ to access the app.
+
+
+
+ {region && (
+
+ {isUsingGPS ? (
- Based on your network, we think you're in{' '}
+ Based on your device's location, we think you're
+ in{' '}
{geolocationString}
- . This estimate may be inaccurate if you're
- using a VPN.
+ .
- >
- )}
- {IS_NATIVE && (
- <>
- {' '}
- {
- locationControl.open()
- })}
- style={[textStyles]}>
+ ) : (
+ <>
- Tap here to update your location via GPS.
+ Based on your network, we think you're in{' '}
+
+ {geolocationString}
+
+ . This estimate may be inaccurate if you're
+ using a VPN.
-
- {
- 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',
- },
+ >
+ )}
+ {IS_NATIVE && (
+ <>
+ {' '}
+ {
+ locationControl.open()
+ })}
+ style={[textStyles]}>
+
+ Tap here to update your location with GPS.
+
+
+ {
+ 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',
+ },
+ )
+ })
+ }
+ }}
+ />
+ >
+ )}
+
+ )}
- {!aa.flags.isOverRegionMinAccessAge && (
-
-
- Unfortunately, your declared age indicates that you
- are not old enough to access Bluesky in your region.
-
-
- )}
+ {!aa.flags.isOverRegionMinAccessAge && (
+
+
+ Unfortunately, your declared age indicates that
+ you are not old enough to access Bluesky in your
+ region.
+
+
+ )}
- {!isBlocked && birthdateUpdateText}
+ {!isBlocked && birthdateUpdateText}
+
+
+ {aa.flags.isOverRegionMinAccessAge && }
+ >
+ ) : (
+
+
+
+ Unfortunately, the birthdate you have saved to your
+ profile makes you too young to access Bluesky.
+
+
+
+ {birthdateUpdateText}
+ )}
+ >
+ ) : (
+
+
+ Hi there!
+
+
+
+ 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.
+
+
+
+
+ Set your birthdate below and we'll get you back to posting
+ and exploring in no time!
+
+
+
- {aa.flags.isOverRegionMinAccessAge && }
- >
- ) : (
-
-
-
- Unfortunately, the birthdate you have saved to your
- profile makes you too young to access Bluesky.
-
-
-
- {birthdateUpdateText}
-
- )}
- >
- ) : (
-
-
- Hi there!
-
-
-
- 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.
-
-
-
-
- Set your birthdate below and we'll get you back to posting
- and exploring in no time!
-
-
-
-
- {orgAdmonition}
-
- )}
+ {orgAdmonition}
+
+ )}
+
@@ -540,7 +531,7 @@ function AccessSection() {
{verifyCta}
- {allowsDeviceVerification && (
+ {allowsDeviceVerification ? (
@@ -558,9 +549,7 @@ function AccessSection() {
platforms.
- )}
-
- {lastInitiatedAt && timeAgo && diff ? (
+ ) : lastInitiatedAt && timeAgo && diff ? (
= {
+ [AgeAssuranceAccess.None]: 0,
+ [AgeAssuranceAccess.Safe]: 1,
+ [AgeAssuranceAccess.Full]: 2,
+ // `unknown` isn't a real granted level; treat it as the floor.
+ [AgeAssuranceAccess.Unknown]: -1,
+}
+
+/**
+ * Whether correcting the user's declared age (i.e. updating their birthdate)
+ * could meaningfully improve their standing in the current region.
+ *
+ * There are two ways a birthdate update can help:
+ *
+ * 1. Raising the rule-engine access level. Some regions grant `safe`/`full` off
+ * a sufficient *declared* age, so a user whose birthdate is wrong (too young)
+ * can unlock more by correcting it. Other regions gate higher access purely
+ * on an *assured* age (or account date), where a declared age changes
+ * nothing.
+ * 2. Crossing the region's `minAccessAge`. Below it the user is hard-blocked
+ * with no verify path (see `isOverRegionMinAccessAge` gating in the
+ * NoAccessScreen); crossing it unlocks the verify flow, which is itself a
+ * path to more access even when the rule-engine level would still be `none`.
+ *
+ * We answer by simulating the real rule engine: hold `accountCreatedAt` and
+ * `assuredAge` fixed and re-run access for a set of candidate declared ages
+ * drawn from the region's declared-age rule thresholds and its `minAccessAge`.
+ * If any candidate yields strictly more access, or crosses `minAccessAge` when
+ * the current declared age doesn't, a birthdate update could help. Simulating
+ * rather than statically inspecting rules means first-match precedence (e.g. an
+ * assured/account rule pre-empting a declared rule) is handled correctly for
+ * free.
+ */
+export function canBirthdateUpdateIncreaseAccess({
+ region,
+ metadata,
+}: {
+ region: AppBskyAgeassuranceDefs.ConfigRegion
+ metadata?: AgeAssuranceMetadata
+}): boolean {
+ const baseline = computeAgeAssuranceRegionAccess(region, {
+ accountCreatedAt: metadata?.accountCreatedAt,
+ declaredAge: metadata?.declaredAge,
+ assuredAge: metadata?.assuredAge,
+ })
+ const baselineRank =
+ ACCESS_RANK[baseline?.access ?? AgeAssuranceAccess.Unknown]
+ const baselineOverMin =
+ metadata?.declaredAge !== undefined &&
+ metadata.declaredAge >= region.minAccessAge
+
+ /*
+ * Candidate declared ages to probe: each declared-age rule's threshold and
+ * the region's `minAccessAge`, plus one below each (to cover
+ * `IfDeclaredUnderAge` and the min-age boundary). Anything a birthdate edit
+ * could achieve is captured by crossing one of these thresholds, so we don't
+ * need to sweep every integer.
+ */
+ const thresholds = new Set([region.minAccessAge])
+ for (const rule of region.rules) {
+ if (
+ AppBskyAgeassuranceDefs.isConfigRegionRuleIfDeclaredOverAge(rule) ||
+ AppBskyAgeassuranceDefs.isConfigRegionRuleIfDeclaredUnderAge(rule)
+ ) {
+ thresholds.add(rule.age)
+ }
+ }
+ const candidates = new Set()
+ for (const threshold of thresholds) {
+ candidates.add(threshold)
+ candidates.add(Math.max(0, threshold - 1))
+ }
+
+ for (const declaredAge of candidates) {
+ const result = computeAgeAssuranceRegionAccess(region, {
+ accountCreatedAt: metadata?.accountCreatedAt,
+ declaredAge,
+ assuredAge: metadata?.assuredAge,
+ })
+ const rank = ACCESS_RANK[result?.access ?? AgeAssuranceAccess.Unknown]
+ const overMin = declaredAge >= region.minAccessAge
+ if (rank > baselineRank || (overMin && !baselineOverMin)) return true
+ }
+
+ return false
+}
+
/**
* Hook to get the age assurance region config based on current geolocation.
* Does not fall-back to our app defaults. If no config is found, returns
diff --git a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx
index 8703390d55..e5a4b5eaf2 100644
--- a/src/components/ageAssurance/AgeAssuranceAccountCard.tsx
+++ b/src/components/ageAssurance/AgeAssuranceAccountCard.tsx
@@ -235,20 +235,20 @@ function RegionNotice() {
estimate may be inaccurate if you're using a VPN.
)}
-
- )}
-
- {IS_NATIVE && (
-
-
- {
- locationControl.open()
- })}>
- Tap here to update your location with GPS.
-
-
+ {IS_NATIVE && (
+
+ {' '}
+
+ {
+ locationControl.open()
+ })}>
+ Tap here to update your location with GPS.
+
+
+
+ )}
)}
>