···11import {useEffect, useMemo, useState} from 'react'
22import {computeAgeAssuranceRegionAccess} from '@atproto/api'
3344+import {getAge} from '#/lib/strings/time'
45import {useSession} from '#/state/session'
55-import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
66+import {
77+ type AgeAssuranceData,
88+ getConfigFromCache,
99+ getOtherRequiredDataFromCache,
1010+ getServerStateFromCache,
1111+ useAgeAssuranceDataContext,
1212+} from '#/ageAssurance/data'
613import {logger} from '#/ageAssurance/logger'
714import {
815 AgeAssuranceAccess,
···1219 parseStatusFromString,
1320} from '#/ageAssurance/types'
1421import {getAgeAssuranceRegionConfigWithFallback} from '#/ageAssurance/util'
1515-import {useGeolocation} from '#/geolocation'
2222+import {type Geolocation, useGeolocation} from '#/geolocation'
2323+import {device} from '#/storage'
16241717-export function useAgeAssuranceState(): AgeAssuranceState {
1818- const {hasSession} = useSession()
1919- const geolocation = useGeolocation()
2020- const {config, state, data} = useAgeAssuranceDataContext()
2525+/**
2626+ * Get final evaluated age assurance state. Handles fallbacks and defers to
2727+ * server state before computing access based on AA config from the server +
2828+ * geolocation and other data.
2929+ */
3030+export function computeAgeAssuranceState({
3131+ hasSession,
3232+ config,
3333+ geolocation,
3434+ state,
3535+ data,
3636+}: {
3737+ hasSession: boolean
3838+ config: AgeAssuranceData['config']
3939+ geolocation: Geolocation
4040+ state: AgeAssuranceData['state']
4141+ data: AgeAssuranceData['data']
4242+}) {
4343+ /**
4444+ * This is where we control logged-out moderation prefs. It's all
4545+ * downstream of AA now.
4646+ */
4747+ if (!hasSession)
4848+ return {
4949+ status: AgeAssuranceStatus.Unknown,
5050+ access: AgeAssuranceAccess.Safe,
5151+ }
21522222- return useMemo(() => {
2323- /**
2424- * This is where we control logged-out moderation prefs. It's all
2525- * downstream of AA now.
2626- */
2727- if (!hasSession)
2828- return {
2929- status: AgeAssuranceStatus.Unknown,
3030- access: AgeAssuranceAccess.Safe,
3131- }
5353+ /**
5454+ * This can happen if the prefetch fails (such as due to network issues).
5555+ * The query handler will try it again, but if it continues to fail, of
5656+ * course we won't have config.
5757+ *
5858+ * In this case, fail open to avoid blocking users.
5959+ */
6060+ if (!config) {
6161+ logger.warn('useAgeAssuranceState: missing config')
6262+ return {
6363+ status: AgeAssuranceStatus.Unknown,
6464+ access: AgeAssuranceAccess.Safe,
6565+ error: 'config' as const,
6666+ }
6767+ }
32683333- /**
3434- * This can happen if the prefetch fails (such as due to network issues).
3535- * The query handler will try it again, but if it continues to fail, of
3636- * course we won't have config.
3737- *
3838- * In this case, fail open to avoid blocking users.
3939- */
4040- if (!config) {
4141- logger.warn('useAgeAssuranceState: missing config')
4242- return {
4343- status: AgeAssuranceStatus.Unknown,
4444- access: AgeAssuranceAccess.Safe,
4545- error: 'config',
4646- }
6969+ const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation)
7070+ const isAARequired = region.countryCode !== '*'
7171+ const isTerminalState =
7272+ state?.status === 'assured' || state?.status === 'blocked'
7373+7474+ /*
7575+ * If we are in a terminal state and AA is required for this region,
7676+ * we can trust the server state completely and avoid recomputing.
7777+ */
7878+ if (isTerminalState && isAARequired) {
7979+ return {
8080+ lastInitiatedAt: state.lastInitiatedAt,
8181+ status: parseStatusFromString(state.status),
8282+ access: parseAccessFromString(state.access),
4783 }
8484+ }
48854949- const region = getAgeAssuranceRegionConfigWithFallback(config, geolocation)
5050- const isAARequired = region.countryCode !== '*'
5151- const isTerminalState =
5252- state?.status === 'assured' || state?.status === 'blocked'
8686+ /*
8787+ * Otherwise, we need to compute the access based on the latest data. For
8888+ * accounts with an accurate birthdate, our default fallback rules should
8989+ * ensure correct access.
9090+ */
9191+ const result = computeAgeAssuranceRegionAccess(region, data)
9292+ const computed = {
9393+ lastInitiatedAt: state?.lastInitiatedAt,
9494+ // prefer server state
9595+ status: state?.status
9696+ ? parseStatusFromString(state?.status)
9797+ : AgeAssuranceStatus.Unknown,
9898+ // prefer server state
9999+ access: result
100100+ ? parseAccessFromString(result.access)
101101+ : AgeAssuranceAccess.Full,
102102+ }
103103+ logger.debug('debug useAgeAssuranceState', {
104104+ region,
105105+ state,
106106+ data,
107107+ computed,
108108+ })
109109+ return computed
110110+}
531115454- /*
5555- * If we are in a terminal state and AA is required for this region,
5656- * we can trust the server state completely and avoid recomputing.
5757- */
5858- if (isTerminalState && isAARequired) {
5959- return {
6060- lastInitiatedAt: state.lastInitiatedAt,
6161- status: parseStatusFromString(state.status),
6262- access: parseAccessFromString(state.access),
6363- }
112112+/**
113113+ * This is a last-ditch helper for out-of-band reads of the AA state, such as
114114+ * during account creation. Don't use it for anything else.
115115+ */
116116+export function getAndComputeAgeAssuranceState({did}: {did: string}) {
117117+ const config = getConfigFromCache()
118118+ const state = getServerStateFromCache({did})
119119+ const data = getOtherRequiredDataFromCache({did})
120120+ const geolocation = device.get(['mergedGeolocation'])
121121+122122+ if (!geolocation || !config || !state || !data) {
123123+ return {
124124+ status: AgeAssuranceStatus.Unknown,
125125+ access: AgeAssuranceAccess.Safe,
64126 }
127127+ }
651286666- /*
6767- * Otherwise, we need to compute the access based on the latest data. For
6868- * accounts with an accurate birthdate, our default fallback rules should
6969- * ensure correct access.
7070- */
7171- const result = computeAgeAssuranceRegionAccess(region, data)
7272- const computed = {
7373- lastInitiatedAt: state?.lastInitiatedAt,
7474- // prefer server state
7575- status: state?.status
7676- ? parseStatusFromString(state?.status)
7777- : AgeAssuranceStatus.Unknown,
7878- // prefer server state
7979- access: result
8080- ? parseAccessFromString(result.access)
8181- : AgeAssuranceAccess.Full,
8282- }
8383- logger.debug('debug useAgeAssuranceState', {
8484- region,
8585- state,
8686- data,
8787- computed,
8888- })
8989- return computed
9090- }, [hasSession, geolocation, config, state, data])
129129+ return computeAgeAssuranceState({
130130+ hasSession: true,
131131+ config,
132132+ geolocation,
133133+ state: state.state,
134134+ data: {
135135+ accountCreatedAt: state.metadata?.accountCreatedAt,
136136+ declaredAge: data?.birthdate
137137+ ? getAge(new Date(data.birthdate))
138138+ : undefined,
139139+ birthdate: data?.birthdate,
140140+ },
141141+ })
142142+}
143143+144144+export function useAgeAssuranceState(): AgeAssuranceState {
145145+ const {hasSession} = useSession()
146146+ const geolocation = useGeolocation()
147147+ const {config, state, data} = useAgeAssuranceDataContext()
148148+149149+ return useMemo(
150150+ () =>
151151+ computeAgeAssuranceState({
152152+ hasSession,
153153+ config,
154154+ geolocation,
155155+ state,
156156+ data,
157157+ }),
158158+ [hasSession, geolocation, config, state, data],
159159+ )
91160}
9216193162export function useOnAgeAssuranceAccessUpdate(
+20-1
src/ageAssurance/util.ts
···22import {
33 ageAssuranceRuleIDs as ids,
44 type AppBskyAgeassuranceDefs,
55+ type AtpAgent,
56 getAgeAssuranceRegionConfig,
67 type ModerationPrefs,
78} from '@atproto/api'
89910import {getAge} from '#/lib/strings/time'
1111+import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings'
1012import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
1111-import {useAgeAssuranceDataContext} from '#/ageAssurance/data'
1313+import {
1414+ getDidFromAgentSession,
1515+ getOtherRequiredDataFromCache,
1616+ useAgeAssuranceDataContext,
1717+} from '#/ageAssurance/data'
1218import {AgeAssuranceAccess} from '#/ageAssurance/types'
1319import {type Geolocation, useGeolocation} from '#/geolocation'
1420···109115 adultContentEnabled: false,
110116 labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
111117})
118118+119119+/**
120120+ * Checks our cache of the actor's chat declaration record, and if it's not
121121+ * already restricted, restricts it.
122122+ */
123123+export function maybeRestrictChatSettings({agent}: {agent: AtpAgent}) {
124124+ const did = getDidFromAgentSession(agent)
125125+ if (!did) return
126126+ const data = getOtherRequiredDataFromCache({did})
127127+ // ...update the chat setting record if allowIncoming is not already 'none'.
128128+ if (data?.actorDeclaration?.allowIncoming === 'none') return
129129+ restrictChatSettings({agent, did})
130130+}
···44import {preferencesQueryKey} from '#/state/queries/preferences'
55import {useAgent, useSession} from '#/state/session'
66import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance'
77+import {isUnderAge, maybeRestrictChatSettings} from '#/ageAssurance/util'
78import {IS_DEV} from '#/env'
89import {account} from '#/storage'
910···6364 await queryClient.invalidateQueries({
6465 queryKey: preferencesQueryKey,
6566 })
6767+6868+ if (isUnderAge(birthDate.toISOString(), 18)) {
6969+ maybeRestrictChatSettings({agent})
7070+ }
7171+6672 /**
6773 * Also patch the age assurance other required data with the new
6874 * birthdate, which may change the user's age assurance access level.
+23-1
src/state/queries/messages/actor-declaration.ts
···11-import {type AppBskyActorDefs} from '@atproto/api'
11+import type AtpAgent from '@atproto/api'
22+import {
33+ type AppBskyActorDefs,
44+ type ChatBskyActorDeclaration,
55+} from '@atproto/api'
26import {useMutation, useQueryClient} from '@tanstack/react-query'
3748import {logger} from '#/logger'
···7882 },
7983 })
8084}
8585+8686+export async function fetchActorDeclarationRecord({
8787+ agent,
8888+ did,
8989+}: {
9090+ agent: AtpAgent
9191+ did?: string
9292+}) {
9393+ if (!did) return
9494+ const res = await agent.com.atproto.repo
9595+ .getRecord({
9696+ repo: did,
9797+ collection: 'chat.bsky.actor.declaration',
9898+ rkey: 'self',
9999+ })
100100+ .catch(_e => undefined)
101101+ return res?.data.value as ChatBskyActorDeclaration.Main
102102+}