Integrate on-device age verification into access decision

Wire the expo-age-range device signals (already plumbed) into the age
assurance access decision, gated per-region.

- Model verificationMethods on region config (AgeAssuranceConfigRegion)
  and add a debug US/TX region permitting the 'device' method
- Add region accessors + getAssuredAgeFromDeviceSignals in util
- Surface deviceSignals through the AA context and feed lowerBound in as
  assuredAge, matching the existing IfAssuredOverAge rules
- NoAccessScreen "Verify now" now prompts the OS in device regions and
  falls back to the KWS dialog when unavailable/insufficient
- Keep useDeviceSignalsQuery disabled so we never prompt on load; restore
  from the persisted cache only

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Eric Bailey
2026-06-22 18:21:47 -05:00
parent 4b7abf0ff2
commit e834167f81
6 changed files with 221 additions and 74 deletions
+45
View File
@@ -1,4 +1,5 @@
import {useMemo} from 'react'
import type * as AgeRange from 'expo-age-range'
import {
type AppBskyAgeassuranceDefs,
getAgeAssuranceRegionConfig,
@@ -11,9 +12,11 @@ import {FALLBACK_REGION_CONFIG, MIN_ACCESS_AGE} from '#/ageAssurance/const'
import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data'
import {
AgeAssuranceAccess,
type AgeAssuranceConfigRegion,
type AgeAssuranceFlags,
type AgeAssuranceMetadata,
type AgeAssuranceState,
type AgeAssuranceVerificationMethod,
} from '#/ageAssurance/types'
import {type Geolocation, useGeolocation} from '#/geolocation'
@@ -36,6 +39,48 @@ export function getAgeAssuranceRegionConfigWithFallback(
return region || FALLBACK_REGION_CONFIG
}
/**
* Returns the verification methods permitted for a region, defaulting to
* `['kws']` when the region doesn't specify any (the historical behavior).
*
* NOTE: `verificationMethods` is not yet part of the lexicon, so we read it via
* {@link AgeAssuranceConfigRegion}. See that type for the migration note.
*/
export function getRegionVerificationMethods(
region: AppBskyAgeassuranceDefs.ConfigRegion,
): AgeAssuranceVerificationMethod[] {
const methods = (region as AgeAssuranceConfigRegion).verificationMethods
return methods && methods.length > 0 ? methods : ['kws']
}
/**
* Whether a region permits satisfying age assurance via the native on-device
* age APIs (Apple Declared Age Range / Google Play Age Signals).
*/
export function regionAllowsDeviceVerification(
region: AppBskyAgeassuranceDefs.ConfigRegion,
): boolean {
return getRegionVerificationMethods(region).includes('device')
}
/**
* Derives an assured age from native device signals, but only for regions that
* permit device verification. The OS-provided `lowerBound` is the minimum age
* the platform will attest to, which maps directly onto the `assuredAge` input
* of the rule engine (i.e. `IfAssuredOverAge`/`IfAssuredUnderAge` rules).
*
* Returns undefined when the region doesn't allow device verification or when
* the OS didn't provide a usable lower bound.
*/
export function getAssuredAgeFromDeviceSignals(
region: AppBskyAgeassuranceDefs.ConfigRegion,
deviceSignals: AgeRange.AgeRangeResponse | undefined,
): number | undefined {
if (!regionAllowsDeviceVerification(region)) return undefined
const lowerBound = deviceSignals?.lowerBound
return typeof lowerBound === 'number' ? lowerBound : undefined
}
/**
* 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