migrate the age assurance data reads to the session clients

getOtherRequiredData's getPreferences read moves onto the sdk action, which
also unblocks fetchActorDeclarationRecord - its only caller - so the record
read moves to the pds client too.

The prefetch and refetch entry points now take the transport they need
rather than an agent: the appview client for getState and device signals,
the account client for preferences and the declaration record. The session
bundle still carries only an agent, so the three session call sites derive
both clients from it; the full bundle rework is a later slice. The standalone
config read stays on its own unauthenticated agent.
This commit is contained in:
Samuel Newman
2026-08-04 01:58:31 +03:00
parent 17c17b4543
commit 755ce365f6
6 changed files with 102 additions and 79 deletions
@@ -16,7 +16,7 @@ import {Trans} from '@lingui/react/macro'
import {retry} from '#/lib/async/retry'
import {wait} from '#/lib/async/wait'
import {parseLinkingUrl} from '#/lib/parseLinkingUrl'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {Button, ButtonText} from '#/components/Button'
@@ -176,7 +176,7 @@ function Inner() {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const agent = useAgent()
const appviewClient = useAppviewClient()
const polling = useRef(false)
const unmounted = useRef(false)
const [error, setError] = useState(false)
@@ -196,10 +196,10 @@ function Inner() {
5,
() => true,
async () => {
if (!agent.session) return
if (!appviewClient.did) return
if (unmounted.current) return
const data = await refetchAgeAssuranceServerState({agent})
const data = await refetchAgeAssuranceServerState({appviewClient})
if (data?.state.status !== 'assured') {
throw new Error(
@@ -214,7 +214,7 @@ function Inner() {
)
.then(async data => {
if (!data) return
if (!agent.session) return
if (!appviewClient.did) return
if (unmounted.current) return
setSuccess(true)
@@ -230,7 +230,7 @@ function Inner() {
return () => {
unmounted.current = true
}
}, [ax, agent])
}, [ax, appviewClient])
if (success) {
return (
+65 -40
View File
@@ -7,6 +7,8 @@ import {
AtpAgent,
type ChatBskyActorDeclaration,
} from '@atproto/api'
import {type Client} from '@atproto/lex'
import {getPreferences} from '@bsky.app/sdk'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import {focusManager, QueryClient, useQuery} from '@tanstack/react-query'
import {persistQueryClient} from '@tanstack/react-query-persist-client'
@@ -21,7 +23,7 @@ import {
snoozeBirthdateUpdateAllowedForDid,
} from '#/state/birthdate'
import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration'
import {useAgent, useSession} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import {DEVICE_SIGNALS_SUPPORTED} from '#/ageAssurance/const'
import * as debug from '#/ageAssurance/debug'
import {logger} from '#/ageAssurance/logger'
@@ -37,6 +39,7 @@ import {
} from '#/ageAssurance/util'
import {IS_DEV} from '#/env'
import {useGeolocation} from '#/geolocation'
import {app} from '#/lexicons'
import {device} from '#/storage'
/**
@@ -63,12 +66,6 @@ const [, cacheHydrationPromise] = persistQueryClient({
persister,
})
export function getDidFromAgentSession(agent: AtpAgent) {
const sessionManager = agent.sessionManager
if (!sessionManager || !sessionManager.did) return
return sessionManager.did
}
/*
* Optimistic data
*/
@@ -187,7 +184,7 @@ export function useConfigQuery() {
export function createServerStateQueryKey({did}: {did: string}) {
return ['serverState', did]
}
export async function getServerState({agent}: {agent: AtpAgent}) {
export async function getServerState({appviewClient}: {appviewClient: Client}) {
if (debug.enabled && debug.serverState)
return debug.resolve(debug.serverState)
const geolocation = device.get(['mergedGeolocation'])
@@ -195,17 +192,21 @@ export async function getServerState({agent}: {agent: AtpAgent}) {
logger.error(`getServerState: missing geolocation countryCode`)
return null
}
const {data} = await agent.app.bsky.ageassurance.getState({
const data = await appviewClient.call(app.bsky.ageassurance.getState, {
countryCode: geolocation.countryCode,
regionCode: geolocation.regionCode,
})
const did = getDidFromAgentSession(agent)
const did = appviewClient.did
if (data && did && createdAtCache.has(did)) {
/*
* If account was just created, just use the local cache if available. On
* subsequent reloads, the server should have the correct value.
* subsequent reloads, the server should have the correct value. The cache
* holds ISO datetime strings (written from `new Date().toISOString()`), so
* assert the branded DatetimeString at this boundary.
*/
data.metadata.accountCreatedAt = createdAtCache.get(did)
data.metadata.accountCreatedAt = createdAtCache.get(
did,
) as typeof data.metadata.accountCreatedAt
}
return data ?? null
}
@@ -218,8 +219,12 @@ export function getServerStateFromCache({
createServerStateQueryKey({did}),
)
}
export async function prefetchServerState({agent}: {agent: AtpAgent}) {
const did = getDidFromAgentSession(agent)
export async function prefetchServerState({
appviewClient,
}: {
appviewClient: Client
}) {
const did = appviewClient.did
if (!did) return
@@ -234,7 +239,7 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
try {
logger.debug(`prefetchServerState: resolving...`)
const res = await networkRetry(3, () => getServerState({agent}))
const res = await networkRetry(3, () => getServerState({appviewClient}))
if (res) {
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(qk, res)
}
@@ -245,11 +250,15 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
})
}
}
export async function refetchServerState({agent}: {agent: AtpAgent}) {
const did = getDidFromAgentSession(agent)
export async function refetchServerState({
appviewClient,
}: {
appviewClient: Client
}) {
const did = appviewClient.did
if (!did) return
logger.debug(`refetchServerState: fetching...`)
const res = await networkRetry(3, () => getServerState({agent}))
const res = await networkRetry(3, () => getServerState({appviewClient}))
if (res) {
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
createServerStateQueryKey({did}),
@@ -279,8 +288,8 @@ export function usePatchServerState() {
)
}
export function useServerStateQuery() {
const agent = useAgent()
const did = getDidFromAgentSession(agent)
const appviewClient = useAppviewClient()
const did = appviewClient.did
const query = useQuery(
{
enabled: !!did,
@@ -290,7 +299,7 @@ export function useServerStateQuery() {
},
queryKey: createServerStateQueryKey({did: did!}),
async queryFn() {
return getServerState({agent})
return getServerState({appviewClient})
},
},
qc,
@@ -342,15 +351,15 @@ export function createOtherRequiredDataQueryKey({did}: {did: string}) {
return ['otherRequiredData', did]
}
async function getOtherRequiredData({
agent,
accountClient,
}: {
agent: AtpAgent
accountClient: Client
}): Promise<OtherRequiredData> {
if (debug.enabled) return debug.resolve(debug.otherRequiredData)
const did = getDidFromAgentSession(agent)
const did = accountClient.did
const [prefs, actorDeclaration] = await Promise.all([
agent.getPreferences(),
fetchActorDeclarationRecord({did, agent}),
accountClient.call(getPreferences),
fetchActorDeclarationRecord({did, client: accountClient}),
])
const data: OtherRequiredData = {
birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined,
@@ -426,8 +435,12 @@ export function setOtherRequiredDataActorDeclarationCache({
next,
)
}
export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
const did = getDidFromAgentSession(agent)
export async function prefetchOtherRequiredData({
accountClient,
}: {
accountClient: Client
}) {
const did = accountClient.did
if (!did) return
@@ -442,7 +455,9 @@ export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
try {
logger.debug(`prefetchOtherRequiredData: resolving...`)
const res = await networkRetry(3, () => getOtherRequiredData({agent}))
const res = await networkRetry(3, () =>
getOtherRequiredData({accountClient}),
)
qc.setQueryData<OtherRequiredData>(qk, res)
} catch (err) {
const e = err as Error
@@ -471,8 +486,8 @@ export function usePatchOtherRequiredData() {
)
}
export function useOtherRequiredDataQuery() {
const agent = useAgent()
const did = getDidFromAgentSession(agent)
const accountClient = usePdsClient()
const did = accountClient.did
return useQuery(
{
enabled: !!did,
@@ -482,7 +497,7 @@ export function useOtherRequiredDataQuery() {
},
queryKey: createOtherRequiredDataQueryKey({did: did!}),
async queryFn() {
return getOtherRequiredData({agent})
return getOtherRequiredData({accountClient})
},
},
qc,
@@ -577,8 +592,12 @@ export function setDeviceSignalsForRegion({
prev => ({...prev, [regionKey]: signals}),
)
}
export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) {
const did = getDidFromAgentSession(agent)
export async function prefetchDeviceSignals({
appviewClient,
}: {
appviewClient: Client
}) {
const did = appviewClient.did
if (!did) return
/**
@@ -613,8 +632,8 @@ export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) {
*/
}
export function useDeviceSignalsQuery() {
const agent = useAgent()
const did = getDidFromAgentSession(agent)
const appviewClient = useAppviewClient()
const did = appviewClient.did
const {data: config} = useConfigQuery()
const geolocation = useGeolocation()
/*
@@ -659,13 +678,19 @@ export function useDeviceSignalsQuery() {
/**
* Helper to prefetch all age assurance data from the server.
*/
export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) {
export function prefetchAgeAssuranceServerData({
appviewClient,
accountClient,
}: {
appviewClient: Client
accountClient: Client
}) {
return Promise.allSettled([
// config fetch initiated at the top of the App.platform.tsx files, awaited here
configPrefetchPromise,
prefetchServerState({agent}),
prefetchOtherRequiredData({agent}),
prefetchDeviceSignals({agent}),
prefetchServerState({appviewClient}),
prefetchOtherRequiredData({accountClient}),
prefetchDeviceSignals({appviewClient}),
])
}
@@ -6,7 +6,7 @@ import {Trans} from '@lingui/react/macro'
import {retry} from '#/lib/async/retry'
import {wait} from '#/lib/async/wait'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {atoms as a, useTheme, web} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {Button, ButtonText} from '#/components/Button'
@@ -84,7 +84,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
const t = useTheme()
const ax = useAnalytics()
const {_} = useLingui()
const agent = useAgent()
const appviewClient = useAppviewClient()
const polling = useRef(false)
const unmounted = useRef(false)
const control = useAgeAssuranceRedirectDialogControl()
@@ -104,10 +104,10 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
5,
() => true,
async () => {
if (!agent.session) return
if (!appviewClient.did) return
if (unmounted.current) return
const data = await refetchAgeAssuranceServerState({agent})
const data = await refetchAgeAssuranceServerState({appviewClient})
if (data?.state.status !== 'assured') {
throw new Error(
@@ -122,7 +122,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
)
.then(async data => {
if (!data) return
if (!agent.session) return
if (!appviewClient.did) return
if (unmounted.current) return
setSuccess(true)
@@ -138,7 +138,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
return () => {
unmounted.current = true
}
}, [ax, agent, control])
}, [ax, appviewClient, control])
if (success) {
return (
@@ -1,15 +1,12 @@
import type AtpAgent from '@atproto/api'
import {
type AppBskyActorDefs,
type ChatBskyActorDeclaration,
} from '@atproto/api'
import {type AppBskyActorDefs} from '@atproto/api'
import {type Client} from '@atproto/lex'
import {type DidString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger'
import {usePdsClient, useSession} from '#/state/session'
import {resolveAllowGroupInvites} from '#/components/dms/util'
import {com} from '#/lexicons'
import {chat, com} from '#/lexicons'
import {RQKEY as PROFILE_RKEY} from '../profile'
export function useUpdateActorDeclaration({
@@ -119,25 +116,16 @@ export function useDeleteActorDeclaration() {
})
}
/*
* Still takes the legacy agent: its only caller is `getOtherRequiredData` in
* `#/ageAssurance/data`, which is blocked on `getPreferences` and so cannot
* hand over a lex client yet.
*/
export async function fetchActorDeclarationRecord({
agent,
client,
did,
}: {
agent: AtpAgent
client: Client
did?: string
}) {
if (!did) return
const res = await agent.com.atproto.repo
.getRecord({
repo: did,
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
})
const res = await client
.get(chat.bsky.actor.declaration, {repo: did as DidString, rkey: 'self'})
.catch(_e => undefined)
return res?.data.value as ChatBskyActorDeclaration.Main
return res?.value
}
+8 -5
View File
@@ -27,7 +27,7 @@ import {
import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state'
import {features} from '#/analytics'
import {type app} from '#/lexicons'
import {agentToPdsClient} from './clients'
import {agentToAppviewClient, agentToPdsClient} from './clients'
import {configureModerationForAccount} from './moderation'
import {
buildBundle,
@@ -105,12 +105,15 @@ export async function createSessionBundleAndCreateAccount(
setCreatedAtForDid({did: earlyAccount.did, createdAt})
setBirthdateForDid({did: earlyAccount.did, birthdate})
snoozeBirthdateUpdateAllowedForDid(earlyAccount.did)
// Start the prefetch after seeding its synchronous birthdate inputs.
const aa = prefetchAgeAssuranceServerData({agent: bundle.agent})
const isProd = Boolean(IS_PROD_SERVICE(service))
// Post-signup writes all target the account's own repo and actor store.
const pdsClient = agentToPdsClient(bundle.agent)
// Start the prefetch after seeding its synchronous birthdate inputs.
const aa = prefetchAgeAssuranceServerData({
appviewClient: agentToAppviewClient(bundle.agent),
accountClient: pdsClient,
})
const isProd = Boolean(IS_PROD_SERVICE(service))
const postSignupTasks: Promise<unknown>[] = [
savePersonalDetails(pdsClient, birthDate),
initializeProfile(pdsClient, {handle, createdAt, isProd}),
+9 -2
View File
@@ -13,6 +13,7 @@ import {
createPublicAgent,
PasswordSessionManager,
} from './bridge-agent'
import {agentToAppviewClient, agentToPdsClient} from './clients'
import {addSessionErrorLog} from './logging'
import {configureModerationForAccount} from './moderation'
import {networkAwareFetch} from './network'
@@ -290,7 +291,10 @@ export async function createSessionBundleAndResume(
) ?? storedAccount
configureModerationForAccount(bundle.agent, earlyAccount)
const aa = prefetchAgeAssuranceServerData({agent: bundle.agent})
const aa = prefetchAgeAssuranceServerData({
appviewClient: agentToAppviewClient(bundle.agent),
accountClient: agentToPdsClient(bundle.agent),
})
/*
* The proxy header is applied after the PDS-targeting setup above, so those
@@ -355,7 +359,10 @@ export async function createSessionBundleAndLogin(
const gates = features.refresh({strategy: 'prefer-fresh-gates'})
configureModerationForAccount(bundle.agent, earlyAccount)
const aa = prefetchAgeAssuranceServerData({agent: bundle.agent})
const aa = prefetchAgeAssuranceServerData({
appviewClient: agentToAppviewClient(bundle.agent),
accountClient: agentToPdsClient(bundle.agent),
})
bundle.agent.configureProxy(BLUESKY_PROXY_HEADER.get())