migrate the data export dialog and signup queue off raw transport

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-04 00:57:28 +03:00
parent 483e79497b
commit 7359700429
2 changed files with 48 additions and 34 deletions
@@ -1,11 +1,11 @@
import {useCallback, useState} from 'react' import {useCallback, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {type DidString} from '@atproto/syntax'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {saveBytesToDisk} from '#/lib/media/manip' import {saveBytesToDisk} from '#/lib/media/manip'
import {logger} from '#/logger' 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 {atoms as a, useTheme, web} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
@@ -14,6 +14,7 @@ import {InlineLinkText} from '#/components/Link'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast' import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {chat, com} from '#/lexicons'
export function ExportCarDialog({ export function ExportCarDialog({
control, control,
@@ -22,21 +23,29 @@ export function ExportCarDialog({
}) { }) {
const {t: l} = useLingui() const {t: l} = useLingui()
const t = useTheme() const t = useTheme()
const agent = useAgent() const {currentAccount} = useSession()
const pdsClient = usePdsClient()
const chatClient = useChatClient()
const [loading, setLoading] = useState<'repo' | 'chat' | false>(false) const [loading, setLoading] = useState<'repo' | 'chat' | false>(false)
const download = useCallback(async () => { const download = useCallback(async () => {
if (!agent.session) { if (!currentAccount) {
return // shouldn't ever happen return // shouldn't ever happen
} }
try { try {
setLoading('repo') setLoading('repo')
const did = agent.session.did const did = currentAccount.did as DidString
const downloadRes = await agent.com.atproto.sync.getRepo({did}) 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( const saveRes = await saveBytesToDisk(
'repo.car', 'repo.car',
downloadRes.data, data,
downloadRes.headers['content-type'] || 'application/vnd.ipld.car', 'application/vnd.ipld.car',
) )
if (saveRes) { if (saveRes) {
@@ -48,28 +57,26 @@ export function ExportCarDialog({
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [l, agent]) }, [l, currentAccount, pdsClient])
const downloadChatData = useCallback(async () => { const downloadChatData = useCallback(async () => {
if (!agent.session) { if (!currentAccount) {
return return
} }
try { try {
setLoading('chat') setLoading('chat')
// Using raw fetch because the XRPC client incorrectly tries to JSON-parse /*
// application/jsonl responses (substring match on application/json). * lex-client only JSON-parses a response when the declared output encoding
const res = await agent.sessionManager.fetchHandler( * is `application/json`; this endpoint declares `application/jsonl`, so it
'/xrpc/chat.bsky.actor.exportAccountData', * returns the raw bytes. That removes the reason for the old low-level
{headers: DM_SERVICE_HEADERS}, * fetchHandler workaround, and the chat client emits the proxy header
) * itself, so the per-call DM headers go away too.
if (!res.ok) { */
throw new Error(`HTTP ${res.status}`) const data = await chatClient.call(chat.bsky.actor.exportAccountData)
}
const data = new Uint8Array(await res.arrayBuffer())
const saveRes = await saveBytesToDisk( const saveRes = await saveBytesToDisk(
'chat.jsonl', 'chat.jsonl',
data, data,
res.headers.get('content-type') || 'application/jsonl', 'application/jsonl',
) )
if (saveRes) { if (saveRes) {
@@ -81,7 +88,7 @@ export function ExportCarDialog({
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [l, agent]) }, [l, currentAccount, chatClient])
return ( return (
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}> <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
+19 -12
View File
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro' import {Trans} from '@lingui/react/macro'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' import {isSignupQueued, usePdsClient, useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import {Logo} from '#/view/icons/Logo' import {Logo} from '#/view/icons/Logo'
import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' 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 {Loader} from '#/components/Loader'
import {P, Text} from '#/components/Typography' import {P, Text} from '#/components/Typography'
import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env' import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env'
import {com} from '#/lexicons'
const COL_WIDTH = 400 const COL_WIDTH = 400
@@ -24,8 +25,8 @@ export function SignupQueued() {
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const onboardingDispatch = useOnboardingDispatch() const onboardingDispatch = useOnboardingDispatch()
const {logoutCurrentAccount} = useSessionApi() const {logoutCurrentAccount, refreshSession} = useSessionApi()
const agent = useAgent() const pdsClient = usePdsClient()
const [isProcessing, setProcessing] = useState(false) const [isProcessing, setProcessing] = useState(false)
const [estimatedTime, setEstimatedTime] = useState<string | undefined>( const [estimatedTime, setEstimatedTime] = useState<string | undefined>(
@@ -38,18 +39,23 @@ export function SignupQueued() {
const checkStatus = useCallback(async () => { const checkStatus = useCallback(async () => {
setProcessing(true) setProcessing(true)
try { try {
const res = await agent.com.atproto.temp.checkSignupQueue() const res = await pdsClient.call(com.atproto.temp.checkSignupQueue)
if (res.data.activated) { if (res.activated) {
// ready to go, exchange the access token for a usable one and kick off onboarding /*
await agent.sessionManager.refreshSession() * Ready to go, exchange the access token for a usable one and kick off
if (!isSignupQueued(agent.session?.accessJwt)) { * 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'}) onboardingDispatch({type: 'start'})
} }
} else { } else {
// not ready, update UI // not ready, update UI
setEstimatedTime(msToString(res.data.estimatedTimeMs)) setEstimatedTime(msToString(res.estimatedTimeMs))
if (typeof res.data.placeInQueue !== 'undefined') { if (typeof res.placeInQueue !== 'undefined') {
setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) setPlaceInQueue(Math.max(res.placeInQueue, 1))
} }
} }
} catch (e: any) { } catch (e: any) {
@@ -62,7 +68,8 @@ export function SignupQueued() {
setEstimatedTime, setEstimatedTime,
setPlaceInQueue, setPlaceInQueue,
onboardingDispatch, onboardingDispatch,
agent, pdsClient,
refreshSession,
]) ])
useEffect(() => { useEffect(() => {