handle age assurance data load failures
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user