handle age assurance data load failures
This commit is contained in:
@@ -347,6 +347,7 @@ export type OtherRequiredData = {
|
|||||||
birthdate: string | undefined
|
birthdate: string | undefined
|
||||||
actorDeclaration?: chat.bsky.actor.declaration.Main
|
actorDeclaration?: chat.bsky.actor.declaration.Main
|
||||||
}
|
}
|
||||||
|
export type OtherRequiredDataStatus = 'pending' | 'error' | 'success'
|
||||||
export function createOtherRequiredDataQueryKey({did}: {did: string}) {
|
export function createOtherRequiredDataQueryKey({did}: {did: string}) {
|
||||||
return ['otherRequiredData', did]
|
return ['otherRequiredData', did]
|
||||||
}
|
}
|
||||||
@@ -722,6 +723,11 @@ export type AgeAssuranceServerData = {
|
|||||||
*/
|
*/
|
||||||
state: app.bsky.ageassurance.defs.State | undefined
|
state: app.bsky.ageassurance.defs.State | undefined
|
||||||
metadata: AgeAssuranceMetadata | 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,
|
* 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
|
* if they've granted access there. Already resolved from the region-keyed
|
||||||
@@ -739,6 +745,7 @@ const AgeAssuranceServerDataContext = createContext<AgeAssuranceServerData>({
|
|||||||
declaredAge: undefined,
|
declaredAge: undefined,
|
||||||
birthdate: undefined,
|
birthdate: undefined,
|
||||||
},
|
},
|
||||||
|
otherRequiredDataStatus: 'pending',
|
||||||
deviceSignals: undefined,
|
deviceSignals: undefined,
|
||||||
})
|
})
|
||||||
export function useAgeAssuranceServerDataContext() {
|
export function useAgeAssuranceServerDataContext() {
|
||||||
@@ -752,7 +759,8 @@ export function AgeAssuranceServerDataProvider({
|
|||||||
const {data: config} = useConfigQuery()
|
const {data: config} = useConfigQuery()
|
||||||
const serverState = useServerStateQuery()
|
const serverState = useServerStateQuery()
|
||||||
const {state, metadata} = serverState.data || {}
|
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.
|
// `select` resolves the cached region-keyed map to the current region.
|
||||||
const {data: deviceSignals} = useDeviceSignalsQuery()
|
const {data: deviceSignals} = useDeviceSignalsQuery()
|
||||||
const ctx = useMemo(
|
const ctx = useMemo(
|
||||||
@@ -767,9 +775,10 @@ export function AgeAssuranceServerDataProvider({
|
|||||||
: undefined,
|
: undefined,
|
||||||
birthdate: data?.birthdate,
|
birthdate: data?.birthdate,
|
||||||
},
|
},
|
||||||
|
otherRequiredDataStatus,
|
||||||
deviceSignals,
|
deviceSignals,
|
||||||
}),
|
}),
|
||||||
[config, state, data, metadata, deviceSignals],
|
[config, state, data, metadata, otherRequiredDataStatus, deviceSignals],
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
<AgeAssuranceServerDataContext.Provider value={ctx}>
|
<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,
|
getDeviceSignalsFromCacheForRegion,
|
||||||
getOtherRequiredDataFromCache,
|
getOtherRequiredDataFromCache,
|
||||||
getServerStateFromCache,
|
getServerStateFromCache,
|
||||||
|
type OtherRequiredDataStatus,
|
||||||
useAgeAssuranceServerDataContext,
|
useAgeAssuranceServerDataContext,
|
||||||
} from '#/ageAssurance/data'
|
} from '#/ageAssurance/data'
|
||||||
import {logger} from '#/ageAssurance/logger'
|
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 +
|
* server state before computing access based on AA config from the server +
|
||||||
* geolocation and other data.
|
* geolocation and other data.
|
||||||
*/
|
*/
|
||||||
function computeAgeAssuranceState({
|
export function computeAgeAssuranceState({
|
||||||
hasSession,
|
hasSession,
|
||||||
geolocation,
|
geolocation,
|
||||||
config,
|
config,
|
||||||
state,
|
state,
|
||||||
metadata,
|
metadata,
|
||||||
|
otherRequiredDataStatus,
|
||||||
deviceSignals,
|
deviceSignals,
|
||||||
}: {
|
}: {
|
||||||
hasSession: boolean
|
hasSession: boolean
|
||||||
@@ -48,6 +50,7 @@ function computeAgeAssuranceState({
|
|||||||
config?: app.bsky.ageassurance.defs.Config
|
config?: app.bsky.ageassurance.defs.Config
|
||||||
state?: app.bsky.ageassurance.defs.State
|
state?: app.bsky.ageassurance.defs.State
|
||||||
metadata?: AgeAssuranceMetadata
|
metadata?: AgeAssuranceMetadata
|
||||||
|
otherRequiredDataStatus: OtherRequiredDataStatus
|
||||||
deviceSignals?: AgeRange.AgeRangeResponse
|
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
|
* Otherwise, we need to compute the access based on the latest data. For
|
||||||
* accounts with an accurate birthdate, our default fallback rules should
|
* accounts with an accurate birthdate, our default fallback rules should
|
||||||
@@ -177,6 +196,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) {
|
|||||||
geolocation,
|
geolocation,
|
||||||
state: state.state,
|
state: state.state,
|
||||||
metadata,
|
metadata,
|
||||||
|
otherRequiredDataStatus: 'success',
|
||||||
deviceSignals,
|
deviceSignals,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -194,7 +214,7 @@ export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) {
|
|||||||
export function useAgeAssuranceState(): AgeAssuranceState {
|
export function useAgeAssuranceState(): AgeAssuranceState {
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
const geolocation = useGeolocation()
|
const geolocation = useGeolocation()
|
||||||
const {config, state, metadata, deviceSignals} =
|
const {config, state, metadata, otherRequiredDataStatus, deviceSignals} =
|
||||||
useAgeAssuranceServerDataContext()
|
useAgeAssuranceServerDataContext()
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
@@ -205,9 +225,18 @@ export function useAgeAssuranceState(): AgeAssuranceState {
|
|||||||
geolocation,
|
geolocation,
|
||||||
state,
|
state,
|
||||||
metadata,
|
metadata,
|
||||||
|
otherRequiredDataStatus,
|
||||||
deviceSignals,
|
deviceSignals,
|
||||||
}),
|
}),
|
||||||
[hasSession, geolocation, config, state, metadata, deviceSignals],
|
[
|
||||||
|
hasSession,
|
||||||
|
geolocation,
|
||||||
|
config,
|
||||||
|
state,
|
||||||
|
metadata,
|
||||||
|
otherRequiredDataStatus,
|
||||||
|
deviceSignals,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ export type AgeAssuranceState = {
|
|||||||
lastInitiatedAt?: string
|
lastInitiatedAt?: string
|
||||||
status: AgeAssuranceStatus
|
status: AgeAssuranceStatus
|
||||||
access: AgeAssuranceAccess
|
access: AgeAssuranceAccess
|
||||||
error?: 'config' // maybe other specific cases in the future
|
isLoading?: boolean
|
||||||
|
error?: 'config' | 'account-data'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AgeAssuranceFlags = {
|
export type AgeAssuranceFlags = {
|
||||||
|
|||||||
Reference in New Issue
Block a user