From 755ce365f6bd8ba0d09334c629b0915063a5612e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 4 Aug 2026 01:58:31 +0300 Subject: [PATCH] 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. --- .../components/RedirectOverlay.tsx | 12 +- src/ageAssurance/data.tsx | 105 +++++++++++------- .../AgeAssuranceRedirectDialog.tsx | 12 +- .../queries/messages/actor-declaration.ts | 28 ++--- src/state/session/create-account.ts | 13 ++- src/state/session/session-core.ts | 11 +- 6 files changed, 102 insertions(+), 79 deletions(-) diff --git a/src/ageAssurance/components/RedirectOverlay.tsx b/src/ageAssurance/components/RedirectOverlay.tsx index 5a6f5b2934..5b31e94365 100644 --- a/src/ageAssurance/components/RedirectOverlay.tsx +++ b/src/ageAssurance/components/RedirectOverlay.tsx @@ -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 ( diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index 9cf6703dfc..2beac738c2 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -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(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( 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 { 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(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}), ]) } diff --git a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx index ad584ca203..acf20f38c5 100644 --- a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx @@ -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 ( diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index 17f36cbdd8..0824e52aae 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -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 } diff --git a/src/state/session/create-account.ts b/src/state/session/create-account.ts index 29dfabacfc..959df45b13 100644 --- a/src/state/session/create-account.ts +++ b/src/state/session/create-account.ts @@ -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[] = [ savePersonalDetails(pdsClient, birthDate), initializeProfile(pdsClient, {handle, createdAt, isProd}), diff --git a/src/state/session/session-core.ts b/src/state/session/session-core.ts index 5e3a9e8a8f..759e6afcfa 100644 --- a/src/state/session/session-core.ts +++ b/src/state/session/session-core.ts @@ -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())