Refactor age assurance flags and contexts (#10794)

This commit is contained in:
Eric Bailey
2026-06-08 15:34:51 -05:00
committed by GitHub
parent 46e54d2375
commit 690b8184a1
12 changed files with 390 additions and 146 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
export const prefetchAgeAssuranceData = () => {} export const prefetchAgeAssuranceServerData = () => {}
export const setBirthdateForDid = () => {} export const setBirthdateForDid = () => {}
export const setCreatedAtForDid = () => {} export const setCreatedAtForDid = () => {}
@@ -32,7 +32,7 @@ import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {BottomSheetOutlet} from '#/../modules/bottom-sheet' import {BottomSheetOutlet} from '#/../modules/bottom-sheet'
import {useAgeAssurance} from '#/ageAssurance' import {useAgeAssurance} from '#/ageAssurance'
import {useAgeAssuranceDataContext} from '#/ageAssurance/data' import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data'
import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess' import {useComputeAgeAssuranceRegionAccess} from '#/ageAssurance/useComputeAgeAssuranceRegionAccess'
import { import {
isLegacyBirthdateBug, isLegacyBirthdateBug,
@@ -53,7 +53,7 @@ export function NoAccessScreen() {
const birthdateControl = useDialogControl() const birthdateControl = useDialogControl()
const deactivateAccountControl = useDialogControl() const deactivateAccountControl = useDialogControl()
const deleteAccountControl = useDialogControl() const deleteAccountControl = useDialogControl()
const {data} = useAgeAssuranceDataContext() const {metadata} = useAgeAssuranceServerDataContext()
const region = useAgeAssuranceRegionConfig() const region = useAgeAssuranceRegionConfig()
const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed() const isBirthdateUpdateAllowed = useIsBirthdateUpdateAllowed()
const {logoutCurrentAccount} = useSessionApi() const {logoutCurrentAccount} = useSessionApi()
@@ -62,15 +62,15 @@ export function NoAccessScreen() {
const aa = useAgeAssurance() const aa = useAgeAssurance()
const isBlocked = aa.state.status === aa.Status.Blocked const isBlocked = aa.state.status === aa.Status.Blocked
const isAARegion = !!region const isAARegion = !!region
const hasDeclaredAge = data?.declaredAge !== undefined const hasDeclaredAge = metadata?.declaredAge !== undefined
const canUpdateBirthday = const canUpdateBirthday =
isBirthdateUpdateAllowed || isLegacyBirthdateBug(data?.birthdate || '') isBirthdateUpdateAllowed || isLegacyBirthdateBug(metadata?.birthdate || '')
useEffect(() => { useEffect(() => {
// just counting overall hits here // just counting overall hits here
ax.metric(`blockedGeoOverlay:shown`, {}) ax.metric(`blockedGeoOverlay:shown`, {})
ax.metric(`ageAssurance:noAccessScreen:shown`, { ax.metric(`ageAssurance:noAccessScreen:shown`, {
accountCreatedAt: data?.accountCreatedAt || 'unknown', accountCreatedAt: metadata?.accountCreatedAt || 'unknown',
isAARegion, isAARegion,
hasDeclaredAge, hasDeclaredAge,
canUpdateBirthday, canUpdateBirthday,
+24 -22
View File
@@ -24,6 +24,7 @@ import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declar
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import * as debug from '#/ageAssurance/debug' import * as debug from '#/ageAssurance/debug'
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
import {type AgeAssuranceMetadata} from '#/ageAssurance/types'
import { import {
getBirthdateStringFromAge, getBirthdateStringFromAge,
isLegacyBirthdateBug, isLegacyBirthdateBug,
@@ -485,9 +486,9 @@ export function useOtherRequiredDataQuery() {
} }
/** /**
* Helper to prefetch all age assurance data. * Helper to prefetch all age assurance data from the server.
*/ */
export function prefetchAgeAssuranceData({agent}: {agent: AtpAgent}) { export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) {
return Promise.allSettled([ return Promise.allSettled([
// config fetch initiated at the top of the App.platform.tsx files, awaited here // config fetch initiated at the top of the App.platform.tsx files, awaited here
configPrefetchPromise, configPrefetchPromise,
@@ -496,8 +497,8 @@ export function prefetchAgeAssuranceData({agent}: {agent: AtpAgent}) {
]) ])
} }
export function clearAgeAssuranceDataForDid({did}: {did: string}) { export function clearAgeAssuranceServerDataForDid({did}: {did: string}) {
logger.debug(`clearAgeAssuranceDataForDid: ${did}`) logger.debug(`clearAgeAssuranceServerDataForDid: ${did}`)
qc.removeQueries({queryKey: createServerStateQueryKey({did}), exact: true}) qc.removeQueries({queryKey: createServerStateQueryKey({did}), exact: true})
qc.removeQueries({ qc.removeQueries({
queryKey: createOtherRequiredDataQueryKey({did}), queryKey: createOtherRequiredDataQueryKey({did}),
@@ -505,8 +506,8 @@ export function clearAgeAssuranceDataForDid({did}: {did: string}) {
}) })
} }
export function clearAgeAssuranceData() { export function clearAgeAssuranceServerDataForAll() {
logger.debug(`clearAgeAssuranceData`) logger.debug(`clearAgeAssuranceServerDataForAll`)
qc.clear() qc.clear()
} }
@@ -514,30 +515,30 @@ export function clearAgeAssuranceData() {
* Context * Context
*/ */
export type AgeAssuranceData = { export type AgeAssuranceServerData = {
/**
* The raw config from the appview.
*/
config: AppBskyAgeassuranceDefs.Config | undefined config: AppBskyAgeassuranceDefs.Config | undefined
/**
* The raw state from the appview. Must be further processed before being useful.
*/
state: AppBskyAgeassuranceDefs.State | undefined state: AppBskyAgeassuranceDefs.State | undefined
data: metadata: AgeAssuranceMetadata | undefined
| {
accountCreatedAt: AppBskyAgeassuranceDefs.StateMetadata['accountCreatedAt']
declaredAge: number | undefined
birthdate: string | undefined
}
| undefined
} }
export const AgeAssuranceDataContext = createContext<AgeAssuranceData>({ const AgeAssuranceServerDataContext = createContext<AgeAssuranceServerData>({
config: undefined, config: undefined,
state: undefined, state: undefined,
data: { metadata: {
accountCreatedAt: undefined, accountCreatedAt: undefined,
declaredAge: undefined, declaredAge: undefined,
birthdate: undefined, birthdate: undefined,
}, },
}) })
export function useAgeAssuranceDataContext() { export function useAgeAssuranceServerDataContext() {
return useContext(AgeAssuranceDataContext) return useContext(AgeAssuranceServerDataContext)
} }
export function AgeAssuranceDataProvider({ export function AgeAssuranceServerDataProvider({
children, children,
}: { }: {
children: React.ReactNode children: React.ReactNode
@@ -550,7 +551,8 @@ export function AgeAssuranceDataProvider({
() => ({ () => ({
config, config,
state, state,
data: { metadata: {
// yes, it's weird, but accountCreatedAt comes back on the `getState` endpoint
accountCreatedAt: metadata?.accountCreatedAt, accountCreatedAt: metadata?.accountCreatedAt,
declaredAge: data?.birthdate declaredAge: data?.birthdate
? getAge(new Date(data.birthdate)) ? getAge(new Date(data.birthdate))
@@ -561,8 +563,8 @@ export function AgeAssuranceDataProvider({
[config, state, data, metadata], [config, state, data, metadata],
) )
return ( return (
<AgeAssuranceDataContext.Provider value={ctx}> <AgeAssuranceServerDataContext.Provider value={ctx}>
{children} {children}
</AgeAssuranceDataContext.Provider> </AgeAssuranceServerDataContext.Provider>
) )
} }
+213 -28
View File
@@ -26,35 +26,8 @@ export const deviceGeolocation: Geolocation | undefined =
} }
: undefined : undefined
export const config: AppBskyAgeassuranceDefs.Config = {
regions: [
{
countryCode: 'AA',
regionCode: undefined,
minAccessAge: 13,
rules: [
{
$type: ids.Default,
access: 'full',
},
],
},
{
countryCode: 'BB',
regionCode: undefined,
minAccessAge: 16,
rules: [
{
$type: ids.Default,
access: 'full',
},
],
},
],
}
export const otherRequiredData: OtherRequiredData = { export const otherRequiredData: OtherRequiredData = {
birthdate: new Date(2000, 1, 1).toISOString(), birthdate: new Date(2010, 12, 1).toISOString(),
} }
const serverStateEnabled = false || IS_E2E const serverStateEnabled = false || IS_E2E
@@ -72,6 +45,218 @@ export const serverState: AppBskyAgeassuranceGetState.OutputSchema | undefined =
} }
: undefined : undefined
export const config: AppBskyAgeassuranceDefs.Config = {
regions: [
{
countryCode: 'AA',
regionCode: undefined,
minAccessAge: 13,
rules: [
{
$type: ids.Default,
access: 'full',
},
],
},
{
countryCode: 'GB',
minAccessAge: 13,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 13,
access: 'safe',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'AU',
minAccessAge: 16,
rules: [
{
date: '2025-12-10T00:00:00Z',
access: 'none',
$type: ids.IfAccountNewerThan,
},
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 16,
access: 'safe',
$type: ids.IfAssuredOverAge,
},
{
age: 16,
access: 'safe',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'US',
regionCode: 'SD',
minAccessAge: 13,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 13,
access: 'safe',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'US',
regionCode: 'WY',
minAccessAge: 13,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 13,
access: 'safe',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'US',
regionCode: 'OH',
minAccessAge: 13,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 13,
access: 'safe',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'US',
regionCode: 'MS',
minAccessAge: 18,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'US',
regionCode: 'VA',
minAccessAge: 16,
rules: [
{
age: 16,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 16,
access: 'full',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'US',
regionCode: 'TN',
minAccessAge: 18,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 18,
access: 'full',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
{
countryCode: 'BR',
minAccessAge: 13,
rules: [
{
age: 18,
access: 'full',
$type: ids.IfAssuredOverAge,
},
{
age: 18,
access: 'full',
$type: ids.IfDeclaredOverAge,
},
{
age: 13,
access: 'safe',
$type: ids.IfDeclaredOverAge,
},
{
access: 'none',
$type: ids.Default,
},
],
},
],
}
export async function resolve<T>(data: T) { export async function resolve<T>(data: T) {
await new Promise(y => setTimeout(y, 500)) // simulate network await new Promise(y => setTimeout(y, 500)) // simulate network
return data return data
+20 -42
View File
@@ -1,11 +1,11 @@
import {createContext, useCallback, useContext, useEffect, useMemo} from 'react' import {createContext, useCallback, useContext, useMemo} from 'react'
import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications' import {useGetAndRegisterPushToken} from '#/lib/notifications/notifications'
import {useAgent} from '#/state/session' import {useAgent} from '#/state/session'
import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay' import {Provider as RedirectOverlayProvider} from '#/ageAssurance/components/RedirectOverlay'
import { import {
AgeAssuranceDataProvider, AgeAssuranceServerDataProvider,
useAgeAssuranceDataContext, useAgeAssuranceServerDataContext,
} from '#/ageAssurance/data' } from '#/ageAssurance/data'
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
import { import {
@@ -14,19 +14,19 @@ import {
} from '#/ageAssurance/state' } from '#/ageAssurance/state'
import { import {
AgeAssuranceAccess, AgeAssuranceAccess,
type AgeAssuranceFlags,
type AgeAssuranceState, type AgeAssuranceState,
AgeAssuranceStatus, AgeAssuranceStatus,
} from '#/ageAssurance/types' } from '#/ageAssurance/types'
import { import {
isUnderAge, computeAgeAssuranceFlags,
maybeRestrictChatSettings, maybeRestrictChatSettings,
MIN_ACCESS_AGE,
useAgeAssuranceRegionConfigWithFallback, useAgeAssuranceRegionConfigWithFallback,
} from '#/ageAssurance/util' } from '#/ageAssurance/util'
export { export {
prefetchConfig as prefetchAgeAssuranceConfig, prefetchConfig as prefetchAgeAssuranceConfig,
prefetchAgeAssuranceData, prefetchAgeAssuranceServerData,
refetchServerState as refetchAgeAssuranceServerState, refetchServerState as refetchAgeAssuranceServerState,
usePatchOtherRequiredData as usePatchAgeAssuranceOtherRequiredData, usePatchOtherRequiredData as usePatchAgeAssuranceOtherRequiredData,
usePatchServerState as usePatchAgeAssuranceServerState, usePatchServerState as usePatchAgeAssuranceServerState,
@@ -38,13 +38,7 @@ const AgeAssuranceStateContext = createContext<{
Access: typeof AgeAssuranceAccess Access: typeof AgeAssuranceAccess
Status: typeof AgeAssuranceStatus Status: typeof AgeAssuranceStatus
state: AgeAssuranceState state: AgeAssuranceState
flags: { flags: AgeAssuranceFlags
adultContentDisabled: boolean
chatDisabled: boolean
isDeclaredUnderAdultAge: boolean
isOverRegionMinAccessAge: boolean
isOverAppMinAccessAge: boolean
}
}>({ }>({
Access: AgeAssuranceAccess, Access: AgeAssuranceAccess,
Status: AgeAssuranceStatus, Status: AgeAssuranceStatus,
@@ -73,19 +67,19 @@ export function useAgeAssurance() {
export function Provider({children}: {children: React.ReactNode}) { export function Provider({children}: {children: React.ReactNode}) {
return ( return (
<AgeAssuranceDataProvider> <AgeAssuranceServerDataProvider>
<InnerProvider> <InnerProvider>
<RedirectOverlayProvider>{children}</RedirectOverlayProvider> <RedirectOverlayProvider>{children}</RedirectOverlayProvider>
</InnerProvider> </InnerProvider>
</AgeAssuranceDataProvider> </AgeAssuranceServerDataProvider>
) )
} }
function InnerProvider({children}: {children: React.ReactNode}) { function InnerProvider({children}: {children: React.ReactNode}) {
const agent = useAgent() const agent = useAgent()
const state = useAgeAssuranceState() const state = useAgeAssuranceState()
const {data} = useAgeAssuranceDataContext() const {metadata} = useAgeAssuranceServerDataContext()
const config = useAgeAssuranceRegionConfigWithFallback() const regionConfig = useAgeAssuranceRegionConfigWithFallback()
const getAndRegisterPushToken = useGetAndRegisterPushToken() const getAndRegisterPushToken = useGetAndRegisterPushToken()
const handleAccessUpdate = useCallback( const handleAccessUpdate = useCallback(
@@ -100,38 +94,22 @@ function InnerProvider({children}: {children: React.ReactNode}) {
) )
useOnAgeAssuranceAccessUpdate(handleAccessUpdate) useOnAgeAssuranceAccessUpdate(handleAccessUpdate)
useEffect(() => {
logger.debug(`useAgeAssuranceState`, {state})
}, [state])
return ( return (
<AgeAssuranceStateContext.Provider <AgeAssuranceStateContext.Provider
value={useMemo(() => { value={useMemo(() => {
const chatDisabled = state.access !== AgeAssuranceAccess.Full const res = {
const isDeclaredUnderAdultAge = data?.birthdate
? isUnderAge(data.birthdate, 18)
: true
const isOverRegionMinAccessAge = data?.birthdate
? !isUnderAge(data.birthdate, config.minAccessAge)
: false
const isOverAppMinAccessAge = data?.birthdate
? !isUnderAge(data.birthdate, MIN_ACCESS_AGE)
: false
const adultContentDisabled =
state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge
return {
Access: AgeAssuranceAccess, Access: AgeAssuranceAccess,
Status: AgeAssuranceStatus, Status: AgeAssuranceStatus,
state, state,
flags: { flags: computeAgeAssuranceFlags({
adultContentDisabled, state,
chatDisabled, regionConfig,
isDeclaredUnderAdultAge, metadata,
isOverRegionMinAccessAge, }),
isOverAppMinAccessAge,
},
} }
}, [state, data, config])}> logger.debug(`useAgeAssurance`, res)
return res
}, [state, metadata, regionConfig])}>
{children} {children}
</AgeAssuranceStateContext.Provider> </AgeAssuranceStateContext.Provider>
) )
+49 -27
View File
@@ -1,24 +1,30 @@
import {useEffect, useMemo, useState} from 'react' import {useEffect, useMemo, useState} from 'react'
import {computeAgeAssuranceRegionAccess} from '@atproto/api' import {
type AppBskyAgeassuranceDefs,
computeAgeAssuranceRegionAccess,
} from '@atproto/api'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import { import {
type AgeAssuranceData,
getConfigFromCache, getConfigFromCache,
getOtherRequiredDataFromCache, getOtherRequiredDataFromCache,
getServerStateFromCache, getServerStateFromCache,
useAgeAssuranceDataContext, useAgeAssuranceServerDataContext,
} from '#/ageAssurance/data' } from '#/ageAssurance/data'
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
import { import {
AgeAssuranceAccess, AgeAssuranceAccess,
type AgeAssuranceMetadata,
type AgeAssuranceState, type AgeAssuranceState,
AgeAssuranceStatus, AgeAssuranceStatus,
parseAccessFromString, parseAccessFromString,
parseStatusFromString, parseStatusFromString,
} from '#/ageAssurance/types' } from '#/ageAssurance/types'
import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util' import {
computeAgeAssuranceFlags,
getAgeAssuranceRegionConfigWithFallback,
} from '#/ageAssurance/util'
import {type Geolocation, useGeolocation} from '#/geolocation' import {type Geolocation, useGeolocation} from '#/geolocation'
import {device} from '#/storage' import {device} from '#/storage'
@@ -27,18 +33,18 @@ 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.
*/ */
export function computeAgeAssuranceState({ function computeAgeAssuranceState({
hasSession, hasSession,
config,
geolocation, geolocation,
config,
state, state,
data, metadata,
}: { }: {
hasSession: boolean hasSession: boolean
config: AgeAssuranceData['config']
geolocation: Geolocation geolocation: Geolocation
state: AgeAssuranceData['state'] config?: AppBskyAgeassuranceDefs.Config
data: AgeAssuranceData['data'] state?: AppBskyAgeassuranceDefs.State
metadata?: AgeAssuranceMetadata
}) { }) {
/** /**
* This is where we control logged-out moderation prefs. It's all * This is where we control logged-out moderation prefs. It's all
@@ -88,7 +94,10 @@ export function computeAgeAssuranceState({
* accounts with an accurate birthdate, our default fallback rules should * accounts with an accurate birthdate, our default fallback rules should
* ensure correct access. * ensure correct access.
*/ */
const result = computeAgeAssuranceRegionAccess(region, data) const result = computeAgeAssuranceRegionAccess(region, {
accountCreatedAt: metadata?.accountCreatedAt,
declaredAge: metadata?.declaredAge,
})
const computed = { const computed = {
lastInitiatedAt: state?.lastInitiatedAt, lastInitiatedAt: state?.lastInitiatedAt,
// prefer server state // prefer server state
@@ -100,10 +109,10 @@ export function computeAgeAssuranceState({
? parseAccessFromString(result.access) ? parseAccessFromString(result.access)
: AgeAssuranceAccess.Full, : AgeAssuranceAccess.Full,
} }
logger.debug('debug useAgeAssuranceState', { logger.debug('computeAgeAssuranceState', {
region, region,
state, state,
data, metadata,
computed, computed,
}) })
return computed return computed
@@ -113,38 +122,51 @@ export function computeAgeAssuranceState({
* This is a last-ditch helper for out-of-band reads of the AA state, such as * This is a last-ditch helper for out-of-band reads of the AA state, such as
* during account creation. Don't use it for anything else. * during account creation. Don't use it for anything else.
*/ */
export function getAndComputeAgeAssuranceState({did}: {did: string}) { export function unsafeGetAndComputeAgeAssurance({did}: {did: string}) {
const config = getConfigFromCache() const config = getConfigFromCache()
const state = getServerStateFromCache({did}) const state = getServerStateFromCache({did})
const data = getOtherRequiredDataFromCache({did}) const requiredData = getOtherRequiredDataFromCache({did})
const geolocation = device.get(['mergedGeolocation']) const geolocation = device.get(['mergedGeolocation'])
if (!geolocation || !config || !state || !data) { if (!geolocation || !config || !state || !requiredData) {
return { return {
state: {
status: AgeAssuranceStatus.Unknown, status: AgeAssuranceStatus.Unknown,
access: AgeAssuranceAccess.Safe, access: AgeAssuranceAccess.Safe,
},
} }
} }
return computeAgeAssuranceState({ const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation)
const metadata: AgeAssuranceMetadata = {
accountCreatedAt: state.metadata?.accountCreatedAt,
declaredAge: requiredData?.birthdate
? getAge(new Date(requiredData.birthdate))
: undefined,
birthdate: requiredData?.birthdate,
}
const computed = computeAgeAssuranceState({
hasSession: true, hasSession: true,
config, config,
geolocation, geolocation,
state: state.state, state: state.state,
data: { metadata,
accountCreatedAt: state.metadata?.accountCreatedAt,
declaredAge: data?.birthdate
? getAge(new Date(data.birthdate))
: undefined,
birthdate: data?.birthdate,
},
}) })
return {
state: computed,
flags: computeAgeAssuranceFlags({
state: computed,
regionConfig: region,
metadata,
}),
}
} }
export function useAgeAssuranceState(): AgeAssuranceState { export function useAgeAssuranceState(): AgeAssuranceState {
const {hasSession} = useSession() const {hasSession} = useSession()
const geolocation = useGeolocation() const geolocation = useGeolocation()
const {config, state, data} = useAgeAssuranceDataContext() const {config, state, metadata} = useAgeAssuranceServerDataContext()
return useMemo( return useMemo(
() => () =>
@@ -153,9 +175,9 @@ export function useAgeAssuranceState(): AgeAssuranceState {
config, config,
geolocation, geolocation,
state, state,
data, metadata,
}), }),
[hasSession, geolocation, config, state, data], [hasSession, geolocation, config, state, metadata],
) )
} }
+16
View File
@@ -1,3 +1,5 @@
import {type computeAgeAssuranceRegionAccess} from '@atproto/api'
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
export enum AgeAssuranceAccess { export enum AgeAssuranceAccess {
@@ -14,6 +16,12 @@ export enum AgeAssuranceStatus {
Blocked = 'blocked', Blocked = 'blocked',
} }
export type AgeAssuranceMetadata = Parameters<
typeof computeAgeAssuranceRegionAccess
>[1] & {
birthdate: string | undefined
}
export type AgeAssuranceState = { export type AgeAssuranceState = {
lastInitiatedAt?: string lastInitiatedAt?: string
status: AgeAssuranceStatus status: AgeAssuranceStatus
@@ -21,6 +29,14 @@ export type AgeAssuranceState = {
error?: 'config' // maybe other specific cases in the future error?: 'config' // maybe other specific cases in the future
} }
export type AgeAssuranceFlags = {
adultContentDisabled: boolean
chatDisabled: boolean
isDeclaredUnderAdultAge: boolean
isOverRegionMinAccessAge: boolean
isOverAppMinAccessAge: boolean
}
export function parseStatusFromString(raw: string) { export function parseStatusFromString(raw: string) {
switch (raw) { switch (raw) {
case 'unknown': case 'unknown':
@@ -1,14 +1,14 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {computeAgeAssuranceRegionAccess} from '@atproto/api' import {computeAgeAssuranceRegionAccess} from '@atproto/api'
import {useAgeAssuranceDataContext} from '#/ageAssurance/data' import {useAgeAssuranceServerDataContext} from '#/ageAssurance/data'
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
import {AgeAssuranceAccess, parseAccessFromString} from '#/ageAssurance/types' import {AgeAssuranceAccess, parseAccessFromString} from '#/ageAssurance/types'
import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util' import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util'
import {type Geolocation} from '#/geolocation' import {type Geolocation} from '#/geolocation'
export function useComputeAgeAssuranceRegionAccess() { export function useComputeAgeAssuranceRegionAccess() {
const {config, data} = useAgeAssuranceDataContext() const {config, metadata} = useAgeAssuranceServerDataContext()
return useCallback( return useCallback(
(geolocation: Geolocation) => { (geolocation: Geolocation) => {
if (!config) { if (!config) {
@@ -19,11 +19,14 @@ export function useComputeAgeAssuranceRegionAccess() {
config, config,
geolocation, geolocation,
) )
const result = computeAgeAssuranceRegionAccess(region, data) const result = computeAgeAssuranceRegionAccess(region, {
accountCreatedAt: metadata?.accountCreatedAt,
declaredAge: metadata?.declaredAge,
})
return result return result
? parseAccessFromString(result.access) ? parseAccessFromString(result.access)
: AgeAssuranceAccess.Full : AgeAssuranceAccess.Full
}, },
[config, data], [config, metadata],
) )
} }
+39 -3
View File
@@ -13,9 +13,14 @@ import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/
import { import {
getDidFromAgentSession, getDidFromAgentSession,
getOtherRequiredDataFromCache, getOtherRequiredDataFromCache,
useAgeAssuranceDataContext, useAgeAssuranceServerDataContext,
} from '#/ageAssurance/data' } from '#/ageAssurance/data'
import {AgeAssuranceAccess} from '#/ageAssurance/types' import {
AgeAssuranceAccess,
type AgeAssuranceFlags,
type AgeAssuranceMetadata,
type AgeAssuranceState,
} from '#/ageAssurance/types'
import {type Geolocation, useGeolocation} from '#/geolocation' import {type Geolocation, useGeolocation} from '#/geolocation'
export const MIN_ACCESS_AGE = 13 export const MIN_ACCESS_AGE = 13
@@ -62,7 +67,7 @@ export function getAgeAssuranceRegionConfigWithFallback(
*/ */
export function useAgeAssuranceRegionConfig() { export function useAgeAssuranceRegionConfig() {
const geolocation = useGeolocation() const geolocation = useGeolocation()
const {config} = useAgeAssuranceDataContext() const {config} = useAgeAssuranceServerDataContext()
return useMemo(() => { return useMemo(() => {
if (!config) return if (!config) return
// use generic helper, we want to potentially return undefined // use generic helper, we want to potentially return undefined
@@ -128,3 +133,34 @@ export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) {
if (data?.actorDeclaration?.allowIncoming === 'none') return if (data?.actorDeclaration?.allowIncoming === 'none') return
restrictChatSettings({agent, did}) restrictChatSettings({agent, did})
} }
export function computeAgeAssuranceFlags({
state,
regionConfig,
metadata,
}: {
state: AgeAssuranceState
regionConfig: AppBskyAgeassuranceDefs.ConfigRegion
metadata?: AgeAssuranceMetadata
}): AgeAssuranceFlags {
const chatDisabled = state.access !== AgeAssuranceAccess.Full
const isDeclaredUnderAdultAge = metadata?.declaredAge
? metadata.declaredAge < 18
: true
const isOverRegionMinAccessAge = metadata?.declaredAge
? metadata.declaredAge >= regionConfig.minAccessAge
: false
const isOverAppMinAccessAge = metadata?.declaredAge
? metadata.declaredAge >= MIN_ACCESS_AGE
: false
const adultContentDisabled =
state.access !== AgeAssuranceAccess.Full || isDeclaredUnderAdultAge
return {
adultContentDisabled,
chatDisabled,
isDeclaredUnderAdultAge,
isOverRegionMinAccessAge,
isOverAppMinAccessAge,
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ jest.mock('jwt-decode', () => ({
jest.mock('../../birthdate') jest.mock('../../birthdate')
jest.mock('../../../ageAssurance/data') jest.mock('../../../ageAssurance/data')
jest.mock('../../../ageAssurance/state', () => ({ jest.mock('../../../ageAssurance/state', () => ({
getAndComputeAgeAssuranceState: () => ({}), unsafeGetAndComputeAgeAssurance: () => ({state: {}}),
})) }))
jest.mock('#/lib/notifications/notifications', () => ({ jest.mock('#/lib/notifications/notifications', () => ({
unregisterPushToken(_agents: BskyAgent[]) { unregisterPushToken(_agents: BskyAgent[]) {
+6 -6
View File
@@ -24,11 +24,11 @@ import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate'
import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders'
import { import {
prefetchAgeAssuranceData, prefetchAgeAssuranceServerData,
setBirthdateForDid, setBirthdateForDid,
setCreatedAtForDid, setCreatedAtForDid,
} from '#/ageAssurance/data' } from '#/ageAssurance/data'
import {getAndComputeAgeAssuranceState} from '#/ageAssurance/state' import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state'
import {AgeAssuranceAccess} from '#/ageAssurance/types' import {AgeAssuranceAccess} from '#/ageAssurance/types'
import {features} from '#/analytics' import {features} from '#/analytics'
import {emitNetworkConfirmed, emitNetworkLost} from '../events' import {emitNetworkConfirmed, emitNetworkLost} from '../events'
@@ -74,7 +74,7 @@ export async function createAgentAndResume(
} }
// after session is attached // after session is attached
const aa = prefetchAgeAssuranceData({agent}) const aa = prefetchAgeAssuranceServerData({agent})
agent.configureProxy(BLUESKY_PROXY_HEADER.get()) agent.configureProxy(BLUESKY_PROXY_HEADER.get())
@@ -113,7 +113,7 @@ export async function createAgentAndLogin(
const account = agentToSessionAccountOrThrow(agent) const account = agentToSessionAccountOrThrow(agent)
const gates = features.refresh({strategy: 'prefer-fresh-gates'}) const gates = features.refresh({strategy: 'prefer-fresh-gates'})
const moderation = configureModerationForAccount(agent, account) const moderation = configureModerationForAccount(agent, account)
const aa = prefetchAgeAssuranceData({agent}) const aa = prefetchAgeAssuranceServerData({agent})
agent.configureProxy(BLUESKY_PROXY_HEADER.get()) agent.configureProxy(BLUESKY_PROXY_HEADER.get())
@@ -175,7 +175,7 @@ export async function createAgentAndCreateAccount(
setBirthdateForDid({did: account.did, birthdate}) setBirthdateForDid({did: account.did, birthdate})
snoozeBirthdateUpdateAllowedForDid(account.did) snoozeBirthdateUpdateAllowedForDid(account.did)
// do this last // do this last
const aa = prefetchAgeAssuranceData({agent}) const aa = prefetchAgeAssuranceServerData({agent})
// Not awaited so that we can still get into onboarding. // Not awaited so that we can still get into onboarding.
// This is OK because we won't let you toggle adult stuff until you set the date. // This is OK because we won't let you toggle adult stuff until you set the date.
@@ -219,7 +219,7 @@ export async function createAgentAndCreateAccount(
}), }),
// wait for AA data to load first, then check state // wait for AA data to load first, then check state
aa.then(async () => { aa.then(async () => {
const state = getAndComputeAgeAssuranceState({did: account.did}) const {state} = unsafeGetAndComputeAgeAssurance({did: account.did})
if (state.access !== AgeAssuranceAccess.Full) { if (state.access !== AgeAssuranceAccess.Full) {
restrictChatSettings({agent, did: account.did}) restrictChatSettings({agent, did: account.did})
} }
+7 -5
View File
@@ -36,8 +36,8 @@ import {
} from '#/state/session/types' } from '#/state/session/types'
import {useOnboardingDispatch} from '#/state/shell/onboarding' import {useOnboardingDispatch} from '#/state/shell/onboarding'
import { import {
clearAgeAssuranceData, clearAgeAssuranceServerDataForAll,
clearAgeAssuranceDataForDid, clearAgeAssuranceServerDataForDid,
} from '#/ageAssurance/data' } from '#/ageAssurance/data'
const StateContext = createContext<SessionStateContext>({ const StateContext = createContext<SessionStateContext>({
@@ -203,7 +203,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
) )
addSessionDebugLog({type: 'method:end', method: 'logout'}) addSessionDebugLog({type: 'method:end', method: 'logout'})
if (prevState.currentAgentState.did) { if (prevState.currentAgentState.did) {
clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did}) clearAgeAssuranceServerDataForDid({
did: prevState.currentAgentState.did,
})
void clearPersistedQueryStorage(prevState.currentAgentState.did) void clearPersistedQueryStorage(prevState.currentAgentState.did)
} }
// reset onboarding flow on logout // reset onboarding flow on logout
@@ -234,7 +236,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}, },
) )
addSessionDebugLog({type: 'method:end', method: 'logout'}) addSessionDebugLog({type: 'method:end', method: 'logout'})
clearAgeAssuranceData() clearAgeAssuranceServerDataForAll()
for (const account of prevState.accounts) { for (const account of prevState.accounts) {
void clearPersistedQueryStorage(account.did) void clearPersistedQueryStorage(account.did)
} }
@@ -304,7 +306,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
accountDid: account.did, accountDid: account.did,
}) })
addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) addSessionDebugLog({type: 'method:end', method: 'removeAccount', account})
clearAgeAssuranceDataForDid({did: account.did}) clearAgeAssuranceServerDataForDid({did: account.did})
}, },
[store, cancelPendingTask], [store, cancelPendingTask],
) )