Return an object from device-signals age data helper

Rename getAssuredAgeFromDeviceSignals to getAgeAssuranceDataFromDeviceSignals
and return an object instead of a bare number, leaving room to derive more
rule-engine inputs from the native response (declared vs. guardian-declared,
parental controls, platform verification status) as the rules grow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Eric Bailey
2026-06-23 16:06:52 -05:00
parent 425505b5ec
commit 18f8d360cb
4 changed files with 26 additions and 15 deletions
@@ -42,7 +42,7 @@ import {
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import { import {
getAssuredAgeFromDeviceSignals, getAgeAssuranceDataFromDeviceSignals,
isLegacyBirthdateBug, isLegacyBirthdateBug,
regionAllowsDeviceVerification, regionAllowsDeviceVerification,
useAgeAssuranceRegionConfig, useAgeAssuranceRegionConfig,
@@ -360,7 +360,10 @@ function AccessSection() {
setIsVerifyingDevice(false) setIsVerifyingDevice(false)
} }
if (signals && did) { if (signals && did) {
const assuredAge = getAssuredAgeFromDeviceSignals(region, signals) const {assuredAge} = getAgeAssuranceDataFromDeviceSignals(
region,
signals,
)
if (assuredAge !== undefined) { if (assuredAge !== undefined) {
// Sufficient device signals: persist (keyed by this region) and let // Sufficient device signals: persist (keyed by this region) and let
// the AA state recompute from the cache write unlock access. Nothing // the AA state recompute from the cache write unlock access. Nothing
+5 -2
View File
@@ -25,8 +25,8 @@ import {
} from '#/ageAssurance/types' } from '#/ageAssurance/types'
import { import {
computeAgeAssuranceFlags, computeAgeAssuranceFlags,
getAgeAssuranceDataFromDeviceSignals,
getAgeAssuranceRegionConfigWithFallback, getAgeAssuranceRegionConfigWithFallback,
getAssuredAgeFromDeviceSignals,
} from '#/ageAssurance/util' } from '#/ageAssurance/util'
import {type Geolocation, useGeolocation} from '#/geolocation' import {type Geolocation, useGeolocation} from '#/geolocation'
import {device} from '#/storage' import {device} from '#/storage'
@@ -103,7 +103,10 @@ function computeAgeAssuranceState({
* is treated as an assured age and fed into the rule engine, where it * is treated as an assured age and fed into the rule engine, where it
* matches `IfAssuredOverAge`/`IfAssuredUnderAge` rules. * matches `IfAssuredOverAge`/`IfAssuredUnderAge` rules.
*/ */
const assuredAge = getAssuredAgeFromDeviceSignals(region, deviceSignals) const {assuredAge} = getAgeAssuranceDataFromDeviceSignals(
region,
deviceSignals,
)
const result = computeAgeAssuranceRegionAccess(region, { const result = computeAgeAssuranceRegionAccess(region, {
accountCreatedAt: metadata?.accountCreatedAt, accountCreatedAt: metadata?.accountCreatedAt,
declaredAge: metadata?.declaredAge, declaredAge: metadata?.declaredAge,
+1 -1
View File
@@ -38,7 +38,7 @@ export type AgeAssuranceConfigRegion = AppBskyAgeassuranceDefs.ConfigRegion & {
* attestation, only age bounds), so we persist it client-side only and bind it * attestation, only age bounds), so we persist it client-side only and bind it
* to its capture region via the key. A grant captured in TX is only ever read * to its capture region via the key. A grant captured in TX is only ever read
* back for TX — it can't silently unlock another region. See * back for TX — it can't silently unlock another region. See
* `getAssuredAgeFromDeviceSignals`. * `getAgeAssuranceDataFromDeviceSignals`.
*/ */
export type AgeAssuranceDeviceSignals = { export type AgeAssuranceDeviceSignals = {
[regionKey: string]: AgeRange.AgeRangeResponse [regionKey: string]: AgeRange.AgeRangeResponse
+15 -10
View File
@@ -79,26 +79,31 @@ export function createRegionKey(region: {
} }
/** /**
* Derives an assured age from native device signals for the given region, but * Derives age assurance data from native device signals for the given region,
* only when the region permits device verification. The signals are expected to * but only when the region permits device verification. The signals are
* already be resolved to the user's current region (see * expected to already be resolved to the user's current region (see
* `getDeviceSignalsFromCacheForCurrentRegion`), so a grant captured in another * `getDeviceSignalsFromCacheForCurrentRegion`), so a grant captured in another
* region won't reach here. * region won't reach here.
* *
* The OS-provided `lowerBound` is the minimum age the platform will attest to, * 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. * which maps onto the `assuredAge` input of the rule engine (i.e.
* `IfAssuredOverAge`/`IfAssuredUnderAge` rules). * `IfAssuredOverAge`/`IfAssuredUnderAge` rules).
* *
* Returns undefined when device verification doesn't apply or the OS didn't * Always returns an object (so callers can spread it unconditionally); fields
* provide a usable lower bound. * are populated only when device verification applies and the OS provided
* usable data.
*/ */
export function getAssuredAgeFromDeviceSignals( export function getAgeAssuranceDataFromDeviceSignals(
region: AppBskyAgeassuranceDefs.ConfigRegion, region: AppBskyAgeassuranceDefs.ConfigRegion,
deviceSignals: AgeRange.AgeRangeResponse | undefined, deviceSignals: AgeRange.AgeRangeResponse | undefined,
): number | undefined { ): {
if (!regionAllowsDeviceVerification(region)) return undefined assuredAge?: number
} {
if (!regionAllowsDeviceVerification(region)) return {}
const lowerBound = deviceSignals?.lowerBound const lowerBound = deviceSignals?.lowerBound
return typeof lowerBound === 'number' ? lowerBound : undefined return {
assuredAge: typeof lowerBound === 'number' ? lowerBound : undefined,
}
} }
/** /**