handle age assurance data load failures

This commit is contained in:
Samuel Newman
2026-08-31 17:35:57 +03:00
parent 076cdd650d
commit 3246356fa6
4 changed files with 122 additions and 6 deletions
+11 -2
View File
@@ -347,6 +347,7 @@ export type OtherRequiredData = {
birthdate: string | undefined
actorDeclaration?: chat.bsky.actor.declaration.Main
}
export type OtherRequiredDataStatus = 'pending' | 'error' | 'success'
export function createOtherRequiredDataQueryKey({did}: {did: string}) {
return ['otherRequiredData', did]
}
@@ -722,6 +723,11 @@ export type AgeAssuranceServerData = {
*/
state: app.bsky.ageassurance.defs.State | undefined
metadata: AgeAssuranceMetadata | undefined
/**
* Whether the account data needed to compute age assurance is available.
* A successful response without a birthdate is still `success`.
*/
otherRequiredDataStatus: OtherRequiredDataStatus
/**
* The native on-device age signals for the region the user is currently in,
* if they've granted access there. Already resolved from the region-keyed
@@ -739,6 +745,7 @@ const AgeAssuranceServerDataContext = createContext<AgeAssuranceServerData>({
declaredAge: undefined,
birthdate: undefined,
},
otherRequiredDataStatus: 'pending',
deviceSignals: undefined,
})
export function useAgeAssuranceServerDataContext() {
@@ -752,7 +759,8 @@ export function AgeAssuranceServerDataProvider({
const {data: config} = useConfigQuery()
const serverState = useServerStateQuery()
const {state, metadata} = serverState.data || {}
const {data} = useOtherRequiredDataQuery()
const {data, status} = useOtherRequiredDataQuery()
const otherRequiredDataStatus = data === undefined ? status : 'success'
// `select` resolves the cached region-keyed map to the current region.
const {data: deviceSignals} = useDeviceSignalsQuery()
const ctx = useMemo(
@@ -767,9 +775,10 @@ export function AgeAssuranceServerDataProvider({
: undefined,
birthdate: data?.birthdate,
},
otherRequiredDataStatus,
deviceSignals,
}),
[config, state, data, metadata, deviceSignals],
[config, state, data, metadata, otherRequiredDataStatus, deviceSignals],
)
return (
<AgeAssuranceServerDataContext.Provider value={ctx}>
+77
View File
@@ -0,0 +1,77 @@
import {computeAgeAssuranceState} from '#/ageAssurance/state'
import {AgeAssuranceAccess, AgeAssuranceStatus} from '#/ageAssurance/types'
const geolocation = {
countryCode: undefined,
regionCode: undefined,
}
describe('computeAgeAssuranceState', () => {
it('waits for required account data before computing access', () => {
expect(
computeAgeAssuranceState({
hasSession: true,
geolocation,
config: {regions: []},
otherRequiredDataStatus: 'pending',
}),
).toEqual({
status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Safe,
isLoading: true,
})
})
it('surfaces required account data failures without computing access', () => {
expect(
computeAgeAssuranceState({
hasSession: true,
geolocation,
config: {regions: []},
otherRequiredDataStatus: 'error',
}),
).toEqual({
status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Safe,
error: 'account-data',
})
})
it('computes access after a successful response without a birthdate', () => {
expect(
computeAgeAssuranceState({
hasSession: true,
geolocation,
config: {regions: []},
metadata: {birthdate: undefined},
otherRequiredDataStatus: 'success',
}),
).toMatchObject({
status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.None,
})
})
it('preserves authoritative terminal server state without account data', () => {
expect(
computeAgeAssuranceState({
hasSession: true,
geolocation: {countryCode: 'AA', regionCode: undefined},
config: {
regions: [
{
countryCode: 'AA',
minAccessAge: 13,
rules: [],
},
],
},
state: {status: 'blocked', access: 'none'},
otherRequiredDataStatus: 'error',
}),
).toMatchObject({
status: AgeAssuranceStatus.Blocked,
access: AgeAssuranceAccess.None,
})
})
})
+32 -3
View File
@@ -9,6 +9,7 @@ import {
getDeviceSignalsFromCacheForRegion,
getOtherRequiredDataFromCache,
getServerStateFromCache,
type OtherRequiredDataStatus,
useAgeAssuranceServerDataContext,
} from '#/ageAssurance/data'
import {logger} from '#/ageAssurance/logger'
@@ -35,12 +36,13 @@ import {device} from '#/storage'
* server state before computing access based on AA config from the server +
* geolocation and other data.
*/
function computeAgeAssuranceState({
export function computeAgeAssuranceState({
hasSession,
geolocation,
config,
state,
metadata,
otherRequiredDataStatus,
deviceSignals,
}: {
hasSession: boolean
@@ -48,6 +50,7 @@ function computeAgeAssuranceState({
config?: app.bsky.ageassurance.defs.Config
state?: app.bsky.ageassurance.defs.State
metadata?: AgeAssuranceMetadata
otherRequiredDataStatus: OtherRequiredDataStatus
deviceSignals?: AgeRange.AgeRangeResponse
}) {
/**
@@ -93,6 +96,22 @@ function computeAgeAssuranceState({
}
}
if (otherRequiredDataStatus === 'pending') {
return {
status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Safe,
isLoading: true,
}
}
if (otherRequiredDataStatus === 'error') {
return {
status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Safe,
error: 'account-data' as const,
}
}
/*
* Otherwise, we need to compute the access based on the latest data. For
* accounts with an accurate birthdate, our default fallback rules should
@@ -177,6 +196,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) {
geolocation,
state: state.state,
metadata,
otherRequiredDataStatus: 'success',
deviceSignals,
})
@@ -194,7 +214,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) {
export function useAgeAssuranceState(): AgeAssuranceState {
const {hasSession} = useSession()
const geolocation = useGeolocation()
const {config, state, metadata, deviceSignals} =
const {config, state, metadata, otherRequiredDataStatus, deviceSignals} =
useAgeAssuranceServerDataContext()
return useMemo(
@@ -205,9 +225,18 @@ export function useAgeAssuranceState(): AgeAssuranceState {
geolocation,
state,
metadata,
otherRequiredDataStatus,
deviceSignals,
}),
[hasSession, geolocation, config, state, metadata, deviceSignals],
[
hasSession,
geolocation,
config,
state,
metadata,
otherRequiredDataStatus,
deviceSignals,
],
)
}
+2 -1
View File
@@ -43,7 +43,8 @@ export type AgeAssuranceState = {
lastInitiatedAt?: string
status: AgeAssuranceStatus
access: AgeAssuranceAccess
error?: 'config' // maybe other specific cases in the future
isLoading?: boolean
error?: 'config' | 'account-data'
}
export type AgeAssuranceFlags = {