From f099a9947006b0d72cc3f14fa4a98ebacdcc8845 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 2 Jul 2026 10:20:59 -0500 Subject: [PATCH] Handle birthday confusion by adding a util that determines if manually updating bday could even help --- .../components/NoAccessScreen.tsx | 297 +++++++++--------- src/ageAssurance/util.ts | 94 +++++- .../ageAssurance/AgeAssuranceAccountCard.tsx | 28 +- 3 files changed, 250 insertions(+), 169 deletions(-) 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. + + + + )} )}