From d8c84057ea46de2a6044dbb6ffd73759baf7059d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 1 Jul 2026 10:28:18 -0500 Subject: [PATCH] Gate age signals by platform and version --- .../components/NoAccessScreen.tsx | 4 +--- src/ageAssurance/const.ts | 17 ++++++++++++++ src/ageAssurance/data.tsx | 5 +++-- src/ageAssurance/util.ts | 9 +++++--- src/env/index.ts | 22 ++++++++++++++----- src/env/index.web.ts | 11 ++++++++++ 6 files changed, 54 insertions(+), 14 deletions(-) diff --git a/src/ageAssurance/components/NoAccessScreen.tsx b/src/ageAssurance/components/NoAccessScreen.tsx index ce14b5238b..d179c08226 100644 --- a/src/ageAssurance/components/NoAccessScreen.tsx +++ b/src/ageAssurance/components/NoAccessScreen.tsx @@ -45,7 +45,6 @@ import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAs import { getAgeAssuranceDataFromDeviceSignals, isLegacyBirthdateBug, - regionAllowsDeviceVerification, useAgeAssuranceRegionConfig, } from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' @@ -330,8 +329,7 @@ function AccessSection() { const diff = lastInitiatedAt ? dateDiff(lastInitiatedAt, new Date(), 'down') : null - const allowsDeviceVerification = - region && regionAllowsDeviceVerification(region) + const allowsDeviceVerification = region && aa.flags.allowsDeviceVerification const verifyCta = hasInitiated ? _(msg`Verify again`) : allowsDeviceVerification diff --git a/src/ageAssurance/const.ts b/src/ageAssurance/const.ts index bd79353b51..f410d63489 100644 --- a/src/ageAssurance/const.ts +++ b/src/ageAssurance/const.ts @@ -4,12 +4,29 @@ import { } from '@atproto/api' import {AgeAssuranceAccess} from '#/ageAssurance/types' +import {ANDROID_API_LEVEL, IOS_MAJOR_VERSION, IS_ANDROID, IS_IOS} from '#/env' /** * Minimum age required to access the app at all. */ export const MIN_ACCESS_AGE = 13 +/** + * Whether the current device can provide the native on-device age signals we + * use for age assurance (via `expo-age-range`). We gate on OS version because + * the underlying platform APIs are only available on: + * + * - iOS 26.0+ (Declared Age Range API) + * https://developer.apple.com/documentation/declaredagerange/ + * - Android 6.0 / API level 23+ (Play Age Signals API) + * https://developer.android.com/google/play/age-signals/use-age-signals-api + * + * On unsupported platforms/versions (including web) this is `false`, so we skip + * the device flow and fall back to KWS. + */ +export const DEVICE_SIGNALS_SUPPORTED: boolean = + (IS_IOS && IOS_MAJOR_VERSION >= 26) || (IS_ANDROID && ANDROID_API_LEVEL >= 23) + export const FALLBACK_REGION_CONFIG: AppBskyAgeassuranceDefs.ConfigRegion = { countryCode: '*', regionCode: undefined, diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index 03c724e322..e1d793f387 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -23,6 +23,7 @@ import { } from '#/state/birthdate' import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration' import {useAgent, useSession} from '#/state/session' +import {DEVICE_SIGNALS_SUPPORTED} from '#/ageAssurance/const' import * as debug from '#/ageAssurance/debug' import {logger} from '#/ageAssurance/logger' import { @@ -34,7 +35,7 @@ import { getBirthdateStringFromAge, isLegacyBirthdateBug, } from '#/ageAssurance/util' -import {IS_DEV, IS_NATIVE} from '#/env' +import {IS_DEV} from '#/env' import {useGeolocation} from '#/geolocation' import {device} from '#/storage' @@ -506,7 +507,7 @@ export async function getDeviceSignals(): Promise< > { if (debug.enabled && debug.useMockDeviceSignalsAPIResponse) return debug.resolve(debug.deviceSignals) - if (!IS_NATIVE) return undefined + if (!DEVICE_SIGNALS_SUPPORTED) return undefined try { return await AgeRange.requestAgeRangeAsync({ threshold1: 13, diff --git a/src/ageAssurance/util.ts b/src/ageAssurance/util.ts index 3809acf67f..0ad5b31ea2 100644 --- a/src/ageAssurance/util.ts +++ b/src/ageAssurance/util.ts @@ -8,7 +8,11 @@ import { import {getAge} from '#/lib/strings/time' import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/const' -import {FALLBACK_REGION_CONFIG, MIN_ACCESS_AGE} from '#/ageAssurance/const' +import { + DEVICE_SIGNALS_SUPPORTED, + FALLBACK_REGION_CONFIG, + MIN_ACCESS_AGE, +} from '#/ageAssurance/const' import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data' import { AgeAssuranceAccess, @@ -16,7 +20,6 @@ import { type AgeAssuranceMetadata, type AgeAssuranceState, } from '#/ageAssurance/types' -import {IS_WEB} from '#/env' import {type Geolocation, useGeolocation} from '#/geolocation' /** @@ -189,7 +192,7 @@ export function computeAgeAssuranceFlags({ const adultContentDisabled = state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge const allowsDeviceVerification = - regionAllowsDeviceVerification(regionConfig) && !IS_WEB + DEVICE_SIGNALS_SUPPORTED && regionAllowsDeviceVerification(regionConfig) return { isAgeRestricted, diff --git a/src/env/index.ts b/src/env/index.ts index 13ae3dcc7d..994f3002ab 100644 --- a/src/env/index.ts +++ b/src/env/index.ts @@ -5,12 +5,23 @@ import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common' export * from '#/env/common' -// for some reason Platform.OS === 'ios' AND Platform.Version is undefined in our CI unit tests -sfn -const iOSMajorVersion = +/** + * The major version number of the current iOS device (e.g. `26` for iOS 26.x), + * or `0` on non-iOS platforms. + * + * Note: for some reason Platform.OS === 'ios' AND Platform.Version is undefined + * in our CI unit tests -sfn + */ +export const IOS_MAJOR_VERSION: number = Platform.OS === 'ios' && typeof Platform.Version === 'string' ? parseInt(Platform.Version.split('.')[0], 10) : 0 -const androidPlatformVersion = +/** + * The Android API level of the current device (e.g. `23` for Android 6.0), or + * `0` on non-Android platforms. Note this is the API level, not the marketing + * version. + */ +export const ANDROID_API_LEVEL: number = Platform.OS === 'android' && typeof Platform.Version === 'number' ? Platform.Version : 0 @@ -52,8 +63,7 @@ export const IS_WEB_FIREFOX: boolean = false */ export const IS_HIGH_DPI: boolean = true // ideally we'd use isLiquidGlassAvailable() from expo-glass-effect but checking iOS version is good enough for now -export const IS_LIQUID_GLASS: boolean = iOSMajorVersion >= 26 +export const IS_LIQUID_GLASS: boolean = IOS_MAJOR_VERSION >= 26 // So we can avoid attempting on-device translation when we know it's unsupported. export const IS_TRANSLATION_SUPPORTED: boolean = - (IS_IOS && iOSMajorVersion >= 18) || - (IS_ANDROID && androidPlatformVersion > 22) + (IS_IOS && IOS_MAJOR_VERSION >= 18) || (IS_ANDROID && ANDROID_API_LEVEL > 22) diff --git a/src/env/index.web.ts b/src/env/index.web.ts index 68604a2e5b..c130ab8146 100644 --- a/src/env/index.web.ts +++ b/src/env/index.web.ts @@ -14,6 +14,17 @@ export const APP_VERSION = RELEASE_VERSION */ export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})` +/** + * The major version number of the current iOS device, or `0` on non-iOS + * platforms (always `0` on web). + */ +export const IOS_MAJOR_VERSION: number = 0 +/** + * The Android API level of the current device, or `0` on non-Android platforms + * (always `0` on web). + */ +export const ANDROID_API_LEVEL: number = 0 + /** * Platform detection */