[SDK] Add refreshSession and migrate the session-pinned infra (#11381)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+22
-17
@@ -7,10 +7,11 @@ import {Trans} from '@lingui/react/macro'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||
import {isErrorMaybeAppPasswordPermissions} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
type SessionAccount,
|
||||
useAgent,
|
||||
usePdsClient,
|
||||
useSession,
|
||||
useSessionApi,
|
||||
} from '#/state/session'
|
||||
@@ -25,6 +26,7 @@ import * as Layout from '#/components/Layout'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {IS_WEB} from '#/env'
|
||||
import {com} from '#/lexicons'
|
||||
|
||||
const COL_WIDTH = 400
|
||||
|
||||
@@ -36,8 +38,8 @@ export function Deactivated() {
|
||||
const {onPressSwitchAccount, pendingDid} = useAccountSwitcher()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const hasOtherAccounts = accounts.length > 1
|
||||
const {logoutCurrentAccount} = useSessionApi()
|
||||
const agent = useAgent()
|
||||
const {logoutCurrentAccount, refreshSession} = useSessionApi()
|
||||
const pdsClient = usePdsClient()
|
||||
const [pending, setPending] = useState(false)
|
||||
const [error, setError] = useState<string | undefined>()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -70,21 +72,24 @@ export function Deactivated() {
|
||||
const handleActivate = useCallback(async () => {
|
||||
try {
|
||||
setPending(true)
|
||||
await agent.com.atproto.server.activateAccount()
|
||||
await pdsClient.call(com.atproto.server.activateAccount)
|
||||
await queryClient.resetQueries()
|
||||
await agent.resumeSession(agent.session!)
|
||||
await refreshSession()
|
||||
} catch (e: any) {
|
||||
switch (e.message) {
|
||||
case 'Bad token scope':
|
||||
setError(
|
||||
_(
|
||||
msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`,
|
||||
),
|
||||
)
|
||||
break
|
||||
default:
|
||||
setError(_(msg`Something went wrong, please try again`))
|
||||
break
|
||||
/*
|
||||
* `activateAccount` declares no lexicon errors, so the app-password case
|
||||
* arrives as an undeclared code plus a message. The shared helper matches
|
||||
* both that and the plain-string form the old exact `e.message` switch
|
||||
* relied on.
|
||||
*/
|
||||
if (isErrorMaybeAppPasswordPermissions(e)) {
|
||||
setError(
|
||||
_(
|
||||
msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
setError(_(msg`Something went wrong, please try again`))
|
||||
}
|
||||
|
||||
logger.error(e, {
|
||||
@@ -93,7 +98,7 @@ export function Deactivated() {
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}, [_, agent, setPending, setError, queryClient])
|
||||
}, [_, pdsClient, refreshSession, setPending, setError, queryClient])
|
||||
|
||||
return (
|
||||
<View style={[a.util_screen_outer, a.flex_1]}>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
|
||||
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
|
||||
import {useServiceQuery} from '#/state/queries/service'
|
||||
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {useAgent, useSession, useSessionApi} from '#/state/session'
|
||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Admonition} from '#/components/Admonition'
|
||||
@@ -152,7 +152,7 @@ function ProvidedHandlePage({
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const [subdomain, setSubdomain] = useState('')
|
||||
const agent = useAgent()
|
||||
const {refreshSession} = useSessionApi()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {currentAccount} = useSession()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -173,7 +173,7 @@ function ProvidedHandlePage({
|
||||
queryKey: RQKEY_PROFILE(currentAccount.did),
|
||||
})
|
||||
}
|
||||
agent.resumeSession(agent.session!).then(() => control.close())
|
||||
refreshSession().then(() => control.close())
|
||||
},
|
||||
})
|
||||
|
||||
@@ -311,7 +311,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
const {currentAccount} = useSession()
|
||||
const [dnsPanel, setDNSPanel] = useState(true)
|
||||
const [domain, setDomain] = useState('')
|
||||
const agent = useAgent()
|
||||
const {refreshSession} = useSessionApi()
|
||||
const control = Dialog.useDialogContext()
|
||||
const fetchDid = useFetchDid()
|
||||
const queryClient = useQueryClient()
|
||||
@@ -328,7 +328,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) {
|
||||
queryKey: RQKEY_PROFILE(currentAccount.did),
|
||||
})
|
||||
}
|
||||
agent.resumeSession(agent.session!).then(() => control.close())
|
||||
refreshSession().then(() => control.close())
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {useLingui} from '@lingui/react'
|
||||
import {Trans} from '@lingui/react/macro'
|
||||
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {useAgent, useSession} from '#/state/session'
|
||||
import {matchXrpcError} from '#/lib/xrpc-error'
|
||||
import {usePdsClient, useSession, useSessionApi} from '#/state/session'
|
||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
@@ -16,6 +17,7 @@ import {Loader} from '#/components/Loader'
|
||||
import * as Toast from '#/components/Toast'
|
||||
import {P, Text} from '#/components/Typography'
|
||||
import {IS_NATIVE} from '#/env'
|
||||
import {com} from '#/lexicons'
|
||||
|
||||
enum Stages {
|
||||
Email,
|
||||
@@ -31,7 +33,8 @@ export function DisableEmail2FADialog({
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {currentAccount} = useSession()
|
||||
const agent = useAgent()
|
||||
const pdsClient = usePdsClient()
|
||||
const {refreshSession} = useSessionApi()
|
||||
|
||||
const [stage, setStage] = useState<Stages>(Stages.Email)
|
||||
const [confirmationCode, setConfirmationCode] = useState<string>('')
|
||||
@@ -42,7 +45,7 @@ export function DisableEmail2FADialog({
|
||||
setError('')
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
await agent.com.atproto.server.requestEmailUpdate()
|
||||
await pdsClient.call(com.atproto.server.requestEmailUpdate)
|
||||
setStage(Stages.ConfirmCode)
|
||||
} catch (e) {
|
||||
setError(cleanError(String(e)))
|
||||
@@ -56,21 +59,26 @@ export function DisableEmail2FADialog({
|
||||
setIsProcessing(true)
|
||||
try {
|
||||
if (currentAccount?.email) {
|
||||
await agent.com.atproto.server.updateEmail({
|
||||
await pdsClient.call(com.atproto.server.updateEmail, {
|
||||
email: currentAccount.email,
|
||||
token: confirmationCode.trim(),
|
||||
emailAuthFactor: false,
|
||||
})
|
||||
await agent.resumeSession(agent.session!)
|
||||
await refreshSession()
|
||||
Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'})))
|
||||
}
|
||||
control.close()
|
||||
} catch (e) {
|
||||
const errMsg = String(e)
|
||||
if (errMsg.includes('Token is invalid')) {
|
||||
/*
|
||||
* The old check matched the PDS message "Token is invalid"; the lexicon
|
||||
* declares that case as `InvalidToken`, so match the code instead.
|
||||
*/
|
||||
if (
|
||||
matchXrpcError(e, com.atproto.server.updateEmail) === 'InvalidToken'
|
||||
) {
|
||||
setError(_(msg`Invalid 2FA confirmation code.`))
|
||||
} else {
|
||||
setError(cleanError(errMsg))
|
||||
setError(cleanError(e))
|
||||
}
|
||||
} finally {
|
||||
setIsProcessing(false)
|
||||
|
||||
@@ -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 (
|
||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
||||
|
||||
@@ -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<string | undefined>(
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user