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 {retry} from '#/lib/async/retry'
import {wait} from '#/lib/async/wait' import {wait} from '#/lib/async/wait'
import {parseLinkingUrl} from '#/lib/parseLinkingUrl' 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 {atoms as a, platform, useBreakpoints, useTheme} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -176,7 +176,7 @@ function Inner() {
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const agent = useAgent() const appviewClient = useAppviewClient()
const polling = useRef(false) const polling = useRef(false)
const unmounted = useRef(false) const unmounted = useRef(false)
const [error, setError] = useState(false) const [error, setError] = useState(false)
@@ -196,10 +196,10 @@ function Inner() {
5, 5,
() => true, () => true,
async () => { async () => {
if (!agent.session) return if (!appviewClient.did) return
if (unmounted.current) return if (unmounted.current) return
const data = await refetchAgeAssuranceServerState({agent}) const data = await refetchAgeAssuranceServerState({appviewClient})
if (data?.state.status !== 'assured') { if (data?.state.status !== 'assured') {
throw new Error( throw new Error(
@@ -214,7 +214,7 @@ function Inner() {
) )
.then(async data => { .then(async data => {
if (!data) return if (!data) return
if (!agent.session) return if (!appviewClient.did) return
if (unmounted.current) return if (unmounted.current) return
setSuccess(true) setSuccess(true)
@@ -230,7 +230,7 @@ function Inner() {
return () => { return () => {
unmounted.current = true unmounted.current = true
} }
}, [ax, agent]) }, [ax, appviewClient])
if (success) { if (success) {
return ( return (
+65 -40
View File
@@ -7,6 +7,8 @@ import {
AtpAgent, AtpAgent,
type ChatBskyActorDeclaration, type ChatBskyActorDeclaration,
} from '@atproto/api' } from '@atproto/api'
import {type Client} from '@atproto/lex'
import {getPreferences} from '@bsky.app/sdk'
import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister'
import {focusManager, QueryClient, useQuery} from '@tanstack/react-query' import {focusManager, QueryClient, useQuery} from '@tanstack/react-query'
import {persistQueryClient} from '@tanstack/react-query-persist-client' import {persistQueryClient} from '@tanstack/react-query-persist-client'
@@ -21,7 +23,7 @@ import {
snoozeBirthdateUpdateAllowedForDid, snoozeBirthdateUpdateAllowedForDid,
} from '#/state/birthdate' } from '#/state/birthdate'
import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration' 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 {DEVICE_SIGNALS_SUPPORTED} from '#/ageAssurance/const'
import * as debug from '#/ageAssurance/debug' import * as debug from '#/ageAssurance/debug'
import {logger} from '#/ageAssurance/logger' import {logger} from '#/ageAssurance/logger'
@@ -37,6 +39,7 @@ import {
} from '#/ageAssurance/util' } from '#/ageAssurance/util'
import {IS_DEV} from '#/env' import {IS_DEV} from '#/env'
import {useGeolocation} from '#/geolocation' import {useGeolocation} from '#/geolocation'
import {app} from '#/lexicons'
import {device} from '#/storage' import {device} from '#/storage'
/** /**
@@ -63,12 +66,6 @@ const [, cacheHydrationPromise] = persistQueryClient({
persister, persister,
}) })
export function getDidFromAgentSession(agent: AtpAgent) {
const sessionManager = agent.sessionManager
if (!sessionManager || !sessionManager.did) return
return sessionManager.did
}
/* /*
* Optimistic data * Optimistic data
*/ */
@@ -187,7 +184,7 @@ export function useConfigQuery() {
export function createServerStateQueryKey({did}: {did: string}) { export function createServerStateQueryKey({did}: {did: string}) {
return ['serverState', did] return ['serverState', did]
} }
export async function getServerState({agent}: {agent: AtpAgent}) { export async function getServerState({appviewClient}: {appviewClient: Client}) {
if (debug.enabled && debug.serverState) if (debug.enabled && debug.serverState)
return debug.resolve(debug.serverState) return debug.resolve(debug.serverState)
const geolocation = device.get(['mergedGeolocation']) const geolocation = device.get(['mergedGeolocation'])
@@ -195,17 +192,21 @@ export async function getServerState({agent}: {agent: AtpAgent}) {
logger.error(`getServerState: missing geolocation countryCode`) logger.error(`getServerState: missing geolocation countryCode`)
return null return null
} }
const {data} = await agent.app.bsky.ageassurance.getState({ const data = await appviewClient.call(app.bsky.ageassurance.getState, {
countryCode: geolocation.countryCode, countryCode: geolocation.countryCode,
regionCode: geolocation.regionCode, regionCode: geolocation.regionCode,
}) })
const did = getDidFromAgentSession(agent) const did = appviewClient.did
if (data && did && createdAtCache.has(did)) { if (data && did && createdAtCache.has(did)) {
/* /*
* If account was just created, just use the local cache if available. On * 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 return data ?? null
} }
@@ -218,8 +219,12 @@ export function getServerStateFromCache({
createServerStateQueryKey({did}), createServerStateQueryKey({did}),
) )
} }
export async function prefetchServerState({agent}: {agent: AtpAgent}) { export async function prefetchServerState({
const did = getDidFromAgentSession(agent) appviewClient,
}: {
appviewClient: Client
}) {
const did = appviewClient.did
if (!did) return if (!did) return
@@ -234,7 +239,7 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) {
try { try {
logger.debug(`prefetchServerState: resolving...`) logger.debug(`prefetchServerState: resolving...`)
const res = await networkRetry(3, () => getServerState({agent})) const res = await networkRetry(3, () => getServerState({appviewClient}))
if (res) { if (res) {
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(qk, 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}) { export async function refetchServerState({
const did = getDidFromAgentSession(agent) appviewClient,
}: {
appviewClient: Client
}) {
const did = appviewClient.did
if (!did) return if (!did) return
logger.debug(`refetchServerState: fetching...`) logger.debug(`refetchServerState: fetching...`)
const res = await networkRetry(3, () => getServerState({agent})) const res = await networkRetry(3, () => getServerState({appviewClient}))
if (res) { if (res) {
qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>( qc.setQueryData<AppBskyAgeassuranceGetState.OutputSchema>(
createServerStateQueryKey({did}), createServerStateQueryKey({did}),
@@ -279,8 +288,8 @@ export function usePatchServerState() {
) )
} }
export function useServerStateQuery() { export function useServerStateQuery() {
const agent = useAgent() const appviewClient = useAppviewClient()
const did = getDidFromAgentSession(agent) const did = appviewClient.did
const query = useQuery( const query = useQuery(
{ {
enabled: !!did, enabled: !!did,
@@ -290,7 +299,7 @@ export function useServerStateQuery() {
}, },
queryKey: createServerStateQueryKey({did: did!}), queryKey: createServerStateQueryKey({did: did!}),
async queryFn() { async queryFn() {
return getServerState({agent}) return getServerState({appviewClient})
}, },
}, },
qc, qc,
@@ -342,15 +351,15 @@ export function createOtherRequiredDataQueryKey({did}: {did: string}) {
return ['otherRequiredData', did] return ['otherRequiredData', did]
} }
async function getOtherRequiredData({ async function getOtherRequiredData({
agent, accountClient,
}: { }: {
agent: AtpAgent accountClient: Client
}): Promise<OtherRequiredData> { }): Promise<OtherRequiredData> {
if (debug.enabled) return debug.resolve(debug.otherRequiredData) if (debug.enabled) return debug.resolve(debug.otherRequiredData)
const did = getDidFromAgentSession(agent) const did = accountClient.did
const [prefs, actorDeclaration] = await Promise.all([ const [prefs, actorDeclaration] = await Promise.all([
agent.getPreferences(), accountClient.call(getPreferences),
fetchActorDeclarationRecord({did, agent}), fetchActorDeclarationRecord({did, client: accountClient}),
]) ])
const data: OtherRequiredData = { const data: OtherRequiredData = {
birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined, birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined,
@@ -426,8 +435,12 @@ export function setOtherRequiredDataActorDeclarationCache({
next, next,
) )
} }
export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) { export async function prefetchOtherRequiredData({
const did = getDidFromAgentSession(agent) accountClient,
}: {
accountClient: Client
}) {
const did = accountClient.did
if (!did) return if (!did) return
@@ -442,7 +455,9 @@ export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) {
try { try {
logger.debug(`prefetchOtherRequiredData: resolving...`) logger.debug(`prefetchOtherRequiredData: resolving...`)
const res = await networkRetry(3, () => getOtherRequiredData({agent})) const res = await networkRetry(3, () =>
getOtherRequiredData({accountClient}),
)
qc.setQueryData<OtherRequiredData>(qk, res) qc.setQueryData<OtherRequiredData>(qk, res)
} catch (err) { } catch (err) {
const e = err as Error const e = err as Error
@@ -471,8 +486,8 @@ export function usePatchOtherRequiredData() {
) )
} }
export function useOtherRequiredDataQuery() { export function useOtherRequiredDataQuery() {
const agent = useAgent() const accountClient = usePdsClient()
const did = getDidFromAgentSession(agent) const did = accountClient.did
return useQuery( return useQuery(
{ {
enabled: !!did, enabled: !!did,
@@ -482,7 +497,7 @@ export function useOtherRequiredDataQuery() {
}, },
queryKey: createOtherRequiredDataQueryKey({did: did!}), queryKey: createOtherRequiredDataQueryKey({did: did!}),
async queryFn() { async queryFn() {
return getOtherRequiredData({agent}) return getOtherRequiredData({accountClient})
}, },
}, },
qc, qc,
@@ -577,8 +592,12 @@ export function setDeviceSignalsForRegion({
prev => ({...prev, [regionKey]: signals}), prev => ({...prev, [regionKey]: signals}),
) )
} }
export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) { export async function prefetchDeviceSignals({
const did = getDidFromAgentSession(agent) appviewClient,
}: {
appviewClient: Client
}) {
const did = appviewClient.did
if (!did) return if (!did) return
/** /**
@@ -613,8 +632,8 @@ export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) {
*/ */
} }
export function useDeviceSignalsQuery() { export function useDeviceSignalsQuery() {
const agent = useAgent() const appviewClient = useAppviewClient()
const did = getDidFromAgentSession(agent) const did = appviewClient.did
const {data: config} = useConfigQuery() const {data: config} = useConfigQuery()
const geolocation = useGeolocation() const geolocation = useGeolocation()
/* /*
@@ -659,13 +678,19 @@ export function useDeviceSignalsQuery() {
/** /**
* Helper to prefetch all age assurance data from the server. * 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([ 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,
prefetchServerState({agent}), prefetchServerState({appviewClient}),
prefetchOtherRequiredData({agent}), prefetchOtherRequiredData({accountClient}),
prefetchDeviceSignals({agent}), prefetchDeviceSignals({appviewClient}),
]) ])
} }
@@ -6,7 +6,7 @@ import {Trans} from '@lingui/react/macro'
import {retry} from '#/lib/async/retry' import {retry} from '#/lib/async/retry'
import {wait} from '#/lib/async/wait' 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 {atoms as a, useTheme, web} from '#/alf'
import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonText} from '#/components/Button'
@@ -84,7 +84,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
const t = useTheme() const t = useTheme()
const ax = useAnalytics() const ax = useAnalytics()
const {_} = useLingui() const {_} = useLingui()
const agent = useAgent() const appviewClient = useAppviewClient()
const polling = useRef(false) const polling = useRef(false)
const unmounted = useRef(false) const unmounted = useRef(false)
const control = useAgeAssuranceRedirectDialogControl() const control = useAgeAssuranceRedirectDialogControl()
@@ -104,10 +104,10 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
5, 5,
() => true, () => true,
async () => { async () => {
if (!agent.session) return if (!appviewClient.did) return
if (unmounted.current) return if (unmounted.current) return
const data = await refetchAgeAssuranceServerState({agent}) const data = await refetchAgeAssuranceServerState({appviewClient})
if (data?.state.status !== 'assured') { if (data?.state.status !== 'assured') {
throw new Error( throw new Error(
@@ -122,7 +122,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
) )
.then(async data => { .then(async data => {
if (!data) return if (!data) return
if (!agent.session) return if (!appviewClient.did) return
if (unmounted.current) return if (unmounted.current) return
setSuccess(true) setSuccess(true)
@@ -138,7 +138,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) {
return () => { return () => {
unmounted.current = true unmounted.current = true
} }
}, [ax, agent, control]) }, [ax, appviewClient, control])
if (success) { if (success) {
return ( return (
@@ -1,15 +1,12 @@
import type AtpAgent from '@atproto/api' import {type AppBskyActorDefs} from '@atproto/api'
import { import {type Client} from '@atproto/lex'
type AppBskyActorDefs,
type ChatBskyActorDeclaration,
} from '@atproto/api'
import {type DidString} from '@atproto/syntax' import {type DidString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {logger} from '#/logger' import {logger} from '#/logger'
import {usePdsClient, useSession} from '#/state/session' import {usePdsClient, useSession} from '#/state/session'
import {resolveAllowGroupInvites} from '#/components/dms/util' import {resolveAllowGroupInvites} from '#/components/dms/util'
import {com} from '#/lexicons' import {chat, com} from '#/lexicons'
import {RQKEY as PROFILE_RKEY} from '../profile' import {RQKEY as PROFILE_RKEY} from '../profile'
export function useUpdateActorDeclaration({ 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({ export async function fetchActorDeclarationRecord({
agent, client,
did, did,
}: { }: {
agent: AtpAgent client: Client
did?: string did?: string
}) { }) {
if (!did) return if (!did) return
const res = await agent.com.atproto.repo const res = await client
.getRecord({ .get(chat.bsky.actor.declaration, {repo: did as DidString, rkey: 'self'})
repo: did,
collection: 'chat.bsky.actor.declaration',
rkey: 'self',
})
.catch(_e => undefined) .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 {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state'
import {features} from '#/analytics' import {features} from '#/analytics'
import {type app} from '#/lexicons' import {type app} from '#/lexicons'
import {agentToPdsClient} from './clients' import {agentToAppviewClient, agentToPdsClient} from './clients'
import {configureModerationForAccount} from './moderation' import {configureModerationForAccount} from './moderation'
import { import {
buildBundle, buildBundle,
@@ -105,12 +105,15 @@ export async function createSessionBundleAndCreateAccount(
setCreatedAtForDid({did: earlyAccount.did, createdAt}) setCreatedAtForDid({did: earlyAccount.did, createdAt})
setBirthdateForDid({did: earlyAccount.did, birthdate}) setBirthdateForDid({did: earlyAccount.did, birthdate})
snoozeBirthdateUpdateAllowedForDid(earlyAccount.did) 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. // Post-signup writes all target the account's own repo and actor store.
const pdsClient = agentToPdsClient(bundle.agent) 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>[] = [ const postSignupTasks: Promise<unknown>[] = [
savePersonalDetails(pdsClient, birthDate), savePersonalDetails(pdsClient, birthDate),
initializeProfile(pdsClient, {handle, createdAt, isProd}), initializeProfile(pdsClient, {handle, createdAt, isProd}),
+9 -2
View File
@@ -13,6 +13,7 @@ import {
createPublicAgent, createPublicAgent,
PasswordSessionManager, PasswordSessionManager,
} from './bridge-agent' } from './bridge-agent'
import {agentToAppviewClient, agentToPdsClient} from './clients'
import {addSessionErrorLog} from './logging' import {addSessionErrorLog} from './logging'
import {configureModerationForAccount} from './moderation' import {configureModerationForAccount} from './moderation'
import {networkAwareFetch} from './network' import {networkAwareFetch} from './network'
@@ -290,7 +291,10 @@ export async function createSessionBundleAndResume(
) ?? storedAccount ) ?? storedAccount
configureModerationForAccount(bundle.agent, earlyAccount) 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 * 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'}) const gates = features.refresh({strategy: 'prefer-fresh-gates'})
configureModerationForAccount(bundle.agent, earlyAccount) 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()) bundle.agent.configureProxy(BLUESKY_PROXY_HEADER.get())