From 73597004294d87dd65885f479cc8e2929c13d0e5 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 4 Aug 2026 00:57:28 +0300 Subject: [PATCH] migrate the data export dialog and signup queue off raw transport Co-Authored-By: Claude Fable 5 --- .../Settings/components/ExportCarDialog.tsx | 51 +++++++++++-------- src/screens/SignupQueued.tsx | 31 ++++++----- 2 files changed, 48 insertions(+), 34 deletions(-) diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index 13b60dfdaa..6f28aa340f 100644 --- a/src/screens/Settings/components/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -1,11 +1,11 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' +import {type DidString} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {saveBytesToDisk} from '#/lib/media/manip' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -14,6 +14,7 @@ import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {chat, com} from '#/lexicons' export function ExportCarDialog({ control, @@ -22,21 +23,29 @@ export function ExportCarDialog({ }) { const {t: l} = useLingui() const t = useTheme() - const agent = useAgent() + const {currentAccount} = useSession() + const pdsClient = usePdsClient() + const chatClient = useChatClient() const [loading, setLoading] = useState<'repo' | 'chat' | false>(false) const download = useCallback(async () => { - if (!agent.session) { + if (!currentAccount) { return // shouldn't ever happen } try { setLoading('repo') - const did = agent.session.did - const downloadRes = await agent.com.atproto.sync.getRepo({did}) + const did = currentAccount.did as DidString + const data = await pdsClient.call(com.atproto.sync.getRepo, {did}) + /* + * getRepo declares `application/vnd.ipld.car`, so lex-client hands back + * the raw bytes unparsed and does not surface the response content-type. + * The old code already fell back to this same constant when the header was + * absent, and the endpoint always returns CAR. + */ const saveRes = await saveBytesToDisk( 'repo.car', - downloadRes.data, - downloadRes.headers['content-type'] || 'application/vnd.ipld.car', + data, + 'application/vnd.ipld.car', ) if (saveRes) { @@ -48,28 +57,26 @@ export function ExportCarDialog({ } finally { setLoading(false) } - }, [l, agent]) + }, [l, currentAccount, pdsClient]) const downloadChatData = useCallback(async () => { - if (!agent.session) { + if (!currentAccount) { return } try { setLoading('chat') - // Using raw fetch because the XRPC client incorrectly tries to JSON-parse - // application/jsonl responses (substring match on application/json). - const res = await agent.sessionManager.fetchHandler( - '/xrpc/chat.bsky.actor.exportAccountData', - {headers: DM_SERVICE_HEADERS}, - ) - if (!res.ok) { - throw new Error(`HTTP ${res.status}`) - } - const data = new Uint8Array(await res.arrayBuffer()) + /* + * lex-client only JSON-parses a response when the declared output encoding + * is `application/json`; this endpoint declares `application/jsonl`, so it + * returns the raw bytes. That removes the reason for the old low-level + * fetchHandler workaround, and the chat client emits the proxy header + * itself, so the per-call DM headers go away too. + */ + const data = await chatClient.call(chat.bsky.actor.exportAccountData) const saveRes = await saveBytesToDisk( 'chat.jsonl', data, - res.headers.get('content-type') || 'application/jsonl', + 'application/jsonl', ) if (saveRes) { @@ -81,7 +88,7 @@ export function ExportCarDialog({ } finally { setLoading(false) } - }, [l, agent]) + }, [l, currentAccount, chatClient]) return ( diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index b83e627404..e0bf7f2edd 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {logger} from '#/logger' -import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' +import {isSignupQueued, usePdsClient, useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {Logo} from '#/view/icons/Logo' import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' @@ -15,6 +15,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Loader} from '#/components/Loader' import {P, Text} from '#/components/Typography' import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env' +import {com} from '#/lexicons' const COL_WIDTH = 400 @@ -24,8 +25,8 @@ export function SignupQueued() { const insets = useSafeAreaInsets() const {gtMobile} = useBreakpoints() const onboardingDispatch = useOnboardingDispatch() - const {logoutCurrentAccount} = useSessionApi() - const agent = useAgent() + const {logoutCurrentAccount, refreshSession} = useSessionApi() + const pdsClient = usePdsClient() const [isProcessing, setProcessing] = useState(false) const [estimatedTime, setEstimatedTime] = useState( @@ -38,18 +39,23 @@ export function SignupQueued() { const checkStatus = useCallback(async () => { setProcessing(true) try { - const res = await agent.com.atproto.temp.checkSignupQueue() - if (res.data.activated) { - // ready to go, exchange the access token for a usable one and kick off onboarding - await agent.sessionManager.refreshSession() - if (!isSignupQueued(agent.session?.accessJwt)) { + const res = await pdsClient.call(com.atproto.temp.checkSignupQueue) + if (res.activated) { + /* + * Ready to go, exchange the access token for a usable one and kick off + * onboarding. The refreshed snapshot carries the new scope; reading + * `currentAccount` here would still see the pre-refresh token, since the + * session's update hook dispatches a render away. + */ + const refreshed = await refreshSession() + if (!isSignupQueued(refreshed?.accessJwt)) { onboardingDispatch({type: 'start'}) } } else { // not ready, update UI - setEstimatedTime(msToString(res.data.estimatedTimeMs)) - if (typeof res.data.placeInQueue !== 'undefined') { - setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) + setEstimatedTime(msToString(res.estimatedTimeMs)) + if (typeof res.placeInQueue !== 'undefined') { + setPlaceInQueue(Math.max(res.placeInQueue, 1)) } } } catch (e: any) { @@ -62,7 +68,8 @@ export function SignupQueued() { setEstimatedTime, setPlaceInQueue, onboardingDispatch, - agent, + pdsClient, + refreshSession, ]) useEffect(() => {