Session V2

This commit is contained in:
Eric Bailey
2024-05-01 22:57:02 +01:00
committed by Dan Abramov
parent 5148b957b7
commit 269ae18570
3 changed files with 352 additions and 336 deletions
+307 -334
View File
@@ -1,17 +1,18 @@
import React from 'react' import React from 'react'
import {AtpPersistSessionHandler, BskyAgent} from '@atproto/api' import {BskyAgent} from '@atproto/api'
import {track} from '#/lib/analytics/analytics' import {track} from '#/lib/analytics/analytics'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
import {logEvent, tryFetchGates} from '#/lib/statsig/statsig' import {logEvent} from '#/lib/statsig/statsig'
import {logger} from '#/logger' import {logger} from '#/logger'
import {isWeb} from '#/platform/detection' import {isWeb} from '#/platform/detection'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {useCloseAllActiveElements} from '#/state/util' import {
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' SessionAccount,
import {IS_DEV} from '#/env' SessionApiContext,
import {emitSessionDropped} from '../events' SessionStateContext,
} from '#/state/session/types'
import { import {
agentToSessionAccount, agentToSessionAccount,
configureModerationForAccount, configureModerationForAccount,
@@ -20,21 +21,26 @@ import {
createAgentAndLogin, createAgentAndLogin,
isSessionDeactivated, isSessionDeactivated,
isSessionExpired, isSessionExpired,
} from './util' sessionAccountToAgentSession,
} from '#/state/session/util'
export type {SessionAccount} from '#/state/session/types' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import { import {useCloseAllActiveElements} from '#/state/util'
SessionAccount, import * as Toast from '#/view/com/util/Toast'
SessionApiContext, import {IS_DEV} from '#/env'
SessionStateContext, import {emitSessionDropped} from '../events'
} from '#/state/session/types'
export type {CurrentAccount, SessionAccount} from '#/state/session/types'
export {isSessionDeactivated} export {isSessionDeactivated}
const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) /**
* Only used for the initial agent values in state and context. Replaced
* immediately, and should not be reused.
*/
const INITIAL_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
configureModerationForGuest() configureModerationForGuest()
const StateContext = React.createContext<SessionStateContext>({ const StateContext = React.createContext<SessionStateContext>({
currentAgent: INITIAL_AGENT,
isInitialLoad: true, isInitialLoad: true,
isSwitchingAccounts: false, isSwitchingAccounts: false,
accounts: [], accounts: [],
@@ -50,127 +56,126 @@ const ApiContext = React.createContext<SessionApiContext>({
resumeSession: async () => {}, resumeSession: async () => {},
removeAccount: () => {}, removeAccount: () => {},
selectAccount: async () => {}, selectAccount: async () => {},
updateCurrentAccount: () => {}, refreshSession: () => {},
clearCurrentAccount: () => {}, clearCurrentAccount: () => {},
updateCurrentAccount: async () => {},
}) })
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
function __getAgent() {
return __globalAgent
}
type State = {
accounts: SessionStateContext['accounts']
currentAccountDid: string | undefined
needsPersist: boolean
}
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
const [isInitialLoad, setIsInitialLoad] = React.useState(true) const [isInitialLoad, setIsInitialLoad] = React.useState(true)
const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false) const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
const [state, setState] = React.useState<State>({ const [state, setState] = React.useState(() => ({
accounts: persisted.get('session').accounts, accounts: persisted.get('session').accounts,
currentAccountDid: undefined, // assume logged out to start currentAgent: INITIAL_AGENT,
needsPersist: false, needsPersist: false,
}) }))
const currentAccountDid = React.useMemo(
() => state.currentAgent.session?.did,
[state.currentAgent],
)
const upsertAccount = React.useCallback( const upsertAndPersistAccount = React.useCallback(
(account: SessionAccount, expired = false) => { (account: SessionAccount) => {
setState(s => { setState(s => ({
return { accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], currentAgent: s.currentAgent,
currentAccountDid: expired ? undefined : account.did, needsPersist: true,
needsPersist: true, }))
}
})
}, },
[setState], [setState],
) )
const clearCurrentAccount = React.useCallback(() => { const clearCurrentAccount = React.useCallback(() => {
logger.warn(`session: clear current account`) logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT
// immediate clear this so any pending requests don't use it
state.currentAgent.setPersistSessionHandler(() => {})
const newAgent = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
configureModerationForGuest() configureModerationForGuest()
setState(s => ({ setState(s => ({
accounts: s.accounts, accounts: s.accounts,
currentAccountDid: undefined, currentAgent: newAgent,
needsPersist: true, needsPersist: true,
})) }))
}, [setState]) }, [state.currentAgent, setState])
const createPersistSessionHandler = React.useCallback( React.useEffect(() => {
( /*
agent: BskyAgent, * This method is continually overwritten when `currentAgent` and dependent
account: SessionAccount, * methods local to this file change, so that the freshest agent and
persistSessionCallback: (props: { * handlers are always used.
expired: boolean */
refreshedAccount: SessionAccount state.currentAgent.setPersistSessionHandler(event => {
}) => void, logger.debug(
{ `session: persistSession`,
networkErrorCallback, {event},
}: { logger.DebugContext.session,
networkErrorCallback?: () => void )
} = {},
): AtpPersistSessionHandler => {
return function persistSession(event, session) {
const expired = event === 'expired' || event === 'create-failed'
if (event === 'network-error') { const expired = event === 'expired' || event === 'create-failed'
logger.warn(
`session: persistSessionHandler received network-error event`,
)
networkErrorCallback?.()
return
}
// TODO: use agentToSessionAccount for this too. /*
const refreshedAccount: SessionAccount = { * Special case for a network error that occurs when calling
service: account.service, * `resumeSession`, which happens on page load, when switching
did: session?.did || account.did, * accounts, or when refreshing user session data.
handle: session?.handle || account.handle, *
email: session?.email || account.email, * When this occurs, we drop the user back out to the login screen, but
emailConfirmed: session?.emailConfirmed || account.emailConfirmed, * we don't clear tokens, allowing them to quickly log back in when their
emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor, * connection improves.
deactivated: isSessionDeactivated(session?.accessJwt), */
pdsUrl: agent.pdsUrl?.toString(), if (event === 'network-error') {
logger.warn(
/* `session: persistSessionHandler received network-error event`,
* Tokens are undefined if the session expires, or if creation fails for )
* any reason e.g. tokens are invalid, network error, etc. emitSessionDropped()
*/ clearCurrentAccount()
refreshJwt: session?.refreshJwt, setTimeout(() => {
accessJwt: session?.accessJwt, Toast.show(`Your internet connection is unstable. Please try again.`)
} }, 100)
return
logger.debug(`session: persistSession`, {
event,
deactivated: refreshedAccount.deactivated,
})
if (expired) {
logger.warn(`session: expired`)
emitSessionDropped()
}
/*
* If the session expired, or it was successfully created/updated, we want
* to update/persist the data.
*
* If the session creation failed, it could be a network error, or it could
* be more serious like an invalid token(s). We can't differentiate, so in
* order to allow the user to get a fresh token (if they need it), we need
* to persist this data and wipe their tokens, effectively logging them
* out.
*/
persistSessionCallback({
expired,
refreshedAccount,
})
} }
},
[], /*
) * If the session was expired naturally, we want to drop the user back
* out to log in.
*/
if (expired) {
logger.warn(`session: expired`)
emitSessionDropped()
clearCurrentAccount()
setTimeout(() => {
Toast.show(`Sorry! We need you to enter your password.`)
}, 100)
}
/**
* The updated account object, derived from the updated session we just
* received from this callback.
*/
const refreshedAccount = agentToSessionAccount(state.currentAgent)
if (refreshedAccount) {
/*
* If the session expired naturally, or it was otherwise successfully
* created/updated, we want to update/persist the data.
*/
upsertAndPersistAccount(refreshedAccount)
} else {
/*
* This should never happen based on current `AtpAgent` handling, but
* it's here for TypeScript, and should result in the same handling as
* a session expiration.
*/
logger.error(`session: persistSession failed to get refreshed account`)
emitSessionDropped()
clearCurrentAccount()
setTimeout(() => {
Toast.show(`Sorry! We need you to enter your password.`)
}, 100)
}
})
}, [state.currentAgent, clearCurrentAccount, upsertAndPersistAccount])
const createAccount = React.useCallback<SessionApiContext['createAccount']>( const createAccount = React.useCallback<SessionApiContext['createAccount']>(
async ({ async ({
@@ -185,6 +190,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
logger.info(`session: creating account`) logger.info(`session: creating account`)
track('Try Create Account') track('Try Create Account')
logEvent('account:create:begin', {}) logEvent('account:create:begin', {})
const {agent, account, fetchingGates} = await createAgentAndCreateAccount( const {agent, account, fetchingGates} = await createAgentAndCreateAccount(
{ {
service, service,
@@ -197,31 +203,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}, },
) )
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
__globalAgent = agent
await fetchingGates await fetchingGates
upsertAccount(account) setState(s => ({
accounts: s.accounts,
currentAgent: agent,
needsPersist: true,
}))
upsertAndPersistAccount(account)
logger.debug(`session: created account`, {}, logger.DebugContext.session) logger.debug(`session: created account`, {}, logger.DebugContext.session)
track('Create Account') track('Create Account')
logEvent('account:create:success', {}) logEvent('account:create:success', {})
}, },
[upsertAccount, clearCurrentAccount, createPersistSessionHandler], [upsertAndPersistAccount],
) )
const login = React.useCallback<SessionApiContext['login']>( const login = React.useCallback<SessionApiContext['login']>(
async ({service, identifier, password, authFactorToken}, logContext) => { async ({service, identifier, password, authFactorToken}, logContext) => {
logger.debug(`session: login`, {}, logger.DebugContext.session) logger.debug(`session: login`, {}, logger.DebugContext.session)
const {agent, account, fetchingGates} = await createAgentAndLogin({ const {agent, account, fetchingGates} = await createAgentAndLogin({
service, service,
identifier, identifier,
@@ -229,46 +229,36 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
authFactorToken, authFactorToken,
}) })
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
__globalAgent = agent
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await fetchingGates await fetchingGates
upsertAccount(account) setState(s => ({
accounts: s.accounts,
currentAgent: agent,
needsPersist: true,
}))
upsertAndPersistAccount(account)
logger.debug(`session: logged in`, {}, logger.DebugContext.session) logger.debug(`session: logged in`, {}, logger.DebugContext.session)
track('Sign In', {resumedSession: false}) track('Sign In', {resumedSession: false})
logEvent('account:loggedIn', {logContext, withPassword: true}) logEvent('account:loggedIn', {logContext, withPassword: true})
}, },
[upsertAccount, clearCurrentAccount, createPersistSessionHandler], [upsertAndPersistAccount],
) )
const logout = React.useCallback<SessionApiContext['logout']>( const logout = React.useCallback<SessionApiContext['logout']>(
async logContext => { async logContext => {
logger.debug(`session: logout`) logger.debug(`session: logout`)
clearCurrentAccount() clearCurrentAccount()
setState(s => { setState(s => ({
return { accounts: s.accounts.map(a => ({
accounts: s.accounts.map(a => ({ ...a,
...a, accessJwt: undefined,
refreshJwt: undefined, refreshJwt: undefined,
accessJwt: undefined, })),
})), currentAgent: s.currentAgent,
currentAccountDid: s.currentAccountDid, needsPersist: true,
needsPersist: true, }))
}
})
logEvent('account:loggedOut', {logContext}) logEvent('account:loggedOut', {logContext})
}, },
[clearCurrentAccount, setState], [clearCurrentAccount, setState],
@@ -277,116 +267,67 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const initSession = React.useCallback<SessionApiContext['initSession']>( const initSession = React.useCallback<SessionApiContext['initSession']>(
async account => { async account => {
logger.debug(`session: initSession`, {}, logger.DebugContext.session) logger.debug(`session: initSession`, {}, logger.DebugContext.session)
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
const agent = new BskyAgent({service: account.service}) const newAgent = new BskyAgent({
service: account.service,
})
// restore the correct PDS URL if available // restore the correct PDS URL if available
if (account.pdsUrl) { if (account.pdsUrl) {
agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl) newAgent.pdsUrl = newAgent.api.xrpc.uri = new URL(account.pdsUrl)
} }
agent.setPersistSessionHandler(
createPersistSessionHandler(
agent,
account,
({expired, refreshedAccount}) => {
upsertAccount(refreshedAccount, expired)
},
{networkErrorCallback: clearCurrentAccount},
),
)
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await configureModerationForAccount(agent, account)
const accountOrSessionDeactivated =
isSessionDeactivated(account.accessJwt) || account.deactivated
const prevSession = { const prevSession = {
...account,
accessJwt: account.accessJwt || '', accessJwt: account.accessJwt || '',
refreshJwt: account.refreshJwt || '', refreshJwt: account.refreshJwt || '',
did: account.did,
handle: account.handle,
} }
/**
* Optimistically update moderation services so that when the new agent
* is applied, they're ready.
*
* If session resumption fails, this will be reset by
* `clearCurrentAccount`.
*/
await configureModerationForAccount(newAgent, account)
if (isSessionExpired(account)) { if (isSessionExpired(account)) {
logger.debug(`session: attempting to resume using previous session`)
try {
const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent
await fetchingGates
upsertAccount(freshAccount)
} catch (e) {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
message: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
// TODO: Should this update currentAccountDid?
}
} else {
logger.debug(`session: attempting to reuse previous session`)
agent.session = prevSession
__globalAgent = agent
await fetchingGates
upsertAccount(account)
if (accountOrSessionDeactivated) {
// don't attempt to resume
// use will be taken to the deactivated screen
logger.debug(`session: reusing session for deactivated account`)
return
}
// Intentionally not awaited to unblock the UI:
resumeSessionWithFreshAccount()
.then(freshAccount => {
if (JSON.stringify(account) !== JSON.stringify(freshAccount)) {
logger.info(
`session: reuse of previous session returned a fresh account, upserting`,
)
upsertAccount(freshAccount)
}
})
.catch(e => {
/*
* Note: `agent.persistSession` is also called when this fails, and
* we handle that failure via `createPersistSessionHandler`
*/
logger.info(`session: resumeSessionWithFreshAccount failed`, {
message: e,
})
__globalAgent = PUBLIC_BSKY_AGENT
// TODO: Should this update currentAccountDid?
})
}
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
logger.debug(`session: resumeSessionWithFreshAccount`)
await networkRetry(1, () => agent.resumeSession(prevSession))
const sessionAccount = agentToSessionAccount(agent)
/* /*
* If `agent.resumeSession` fails above, it'll throw. This is just to * If session is expired, attempt to refresh the session using the
* make TypeScript happy. * refresh token via `resumeSession`
*/ */
if (!sessionAccount) { logger.debug(
throw new Error(`session: initSession failed to establish a session`) `session: attempting to resumeSession using previous session`,
} {},
return sessionAccount logger.DebugContext.session,
)
await networkRetry(1, () => newAgent.resumeSession(prevSession))
setState(s => ({
accounts: s.accounts,
currentAgent: newAgent,
needsPersist: true,
}))
upsertAndPersistAccount(agentToSessionAccount(newAgent)!)
} else {
/*
* If the session is not expired, assume we can reuse it.
*/
logger.debug(
`session: attempting to reuse previous session`,
{},
logger.DebugContext.session,
)
newAgent.session = prevSession
setState(s => ({
accounts: s.accounts,
currentAgent: newAgent,
needsPersist: true,
}))
upsertAndPersistAccount(account)
} }
}, },
[upsertAccount, clearCurrentAccount, createPersistSessionHandler], [upsertAndPersistAccount],
) )
const resumeSession = React.useCallback<SessionApiContext['resumeSession']>( const resumeSession = React.useCallback<SessionApiContext['resumeSession']>(
@@ -401,59 +342,46 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
setIsInitialLoad(false) setIsInitialLoad(false)
} }
}, },
[initSession], [initSession, setIsInitialLoad],
) )
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => { account => {
setState(s => { setState(s => ({
return { accounts: s.accounts.filter(a => a.did !== account.did),
accounts: s.accounts.filter(a => a.did !== account.did), currentAgent: s.currentAgent,
currentAccountDid: s.currentAccountDid, needsPersist: true,
needsPersist: true, }))
}
})
}, },
[setState], [setState],
) )
const updateCurrentAccount = React.useCallback< const refreshSession = React.useCallback<
SessionApiContext['updateCurrentAccount'] SessionApiContext['refreshSession']
>( >(async () => {
account => { const {accounts: persistedAccounts} = persisted.get('session')
setState(s => { const selectedAccount = persistedAccounts.find(
const currentAccount = s.accounts.find( a => a.did === currentAccountDid,
a => a.did === s.currentAccountDid, )
) if (!selectedAccount) return
// ignore, should never happen
if (!currentAccount) return s
const updatedAccount = { // update and swap agent to trigger render refresh
...currentAccount, const newAgent = state.currentAgent.clone()
handle: account.handle || currentAccount.handle, await newAgent.resumeSession(sessionAccountToAgentSession(selectedAccount)!)
email: account.email || currentAccount.email, const refreshedAccount = agentToSessionAccount(newAgent)
emailConfirmed: await configureModerationForAccount(newAgent, refreshedAccount!)
account.emailConfirmed !== undefined
? account.emailConfirmed
: currentAccount.emailConfirmed,
emailAuthFactor:
account.emailAuthFactor !== undefined
? account.emailAuthFactor
: currentAccount.emailAuthFactor,
}
return { upsertAndPersistAccount(refreshedAccount!)
accounts: [ setState(s => ({
updatedAccount, accounts: s.accounts,
...s.accounts.filter(a => a.did !== currentAccount.did), currentAgent: newAgent,
], needsPersist: true,
currentAccountDid: s.currentAccountDid, }))
needsPersist: true, }, [currentAccountDid, state.currentAgent, setState, upsertAndPersistAccount])
}
}) const updateCurrentAccount = React.useCallback(async () => {
}, await refreshSession()
[setState], }, [refreshSession])
)
const selectAccount = React.useCallback<SessionApiContext['selectAccount']>( const selectAccount = React.useCallback<SessionApiContext['selectAccount']>(
async (account, logContext) => { async (account, logContext) => {
@@ -469,7 +397,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
throw e throw e
} }
}, },
[initSession], [setIsSwitchingAccounts, initSession],
) )
React.useEffect(() => { React.useEffect(() => {
@@ -477,47 +405,72 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
state.needsPersist = false state.needsPersist = false
persisted.write('session', { persisted.write('session', {
accounts: state.accounts, accounts: state.accounts,
currentAccount: state.accounts.find( currentAccount: state.accounts.find(a => a.did === currentAccountDid),
a => a.did === state.currentAccountDid,
),
}) })
} }
}, [state]) }, [state, currentAccountDid])
React.useEffect(() => { React.useEffect(() => {
return persisted.onUpdate(() => { return persisted.onUpdate(async () => {
const persistedSession = persisted.get('session') const persistedSession = persisted.get('session')
logger.debug(`session: persisted onUpdate`, {}) logger.debug(
`session: persisted onUpdate`,
{},
logger.DebugContext.session,
)
/*
* Accounts are already persisted on other side of broadcast, but we need
* to update them in memory in this tab.
*/
setState(s => ({
accounts: persistedSession.accounts,
currentAgent: s.currentAgent,
needsPersist: false,
}))
const selectedAccount = persistedSession.accounts.find( const selectedAccount = persistedSession.accounts.find(
a => a.did === persistedSession.currentAccount?.did, a => a.did === persistedSession.currentAccount?.did,
) )
if (selectedAccount && selectedAccount.refreshJwt) { if (selectedAccount && selectedAccount.refreshJwt) {
if (selectedAccount.did !== state.currentAccountDid) { if (selectedAccount?.did !== currentAccountDid) {
logger.debug(`session: persisted onUpdate, switching accounts`, { logger.debug(
from: { `session: persisted onUpdate, switching accounts`,
did: state.currentAccountDid, {
from: {
did: currentAccountDid,
},
to: {
did: selectedAccount.did,
},
}, },
to: { logger.DebugContext.session,
did: selectedAccount.did, )
},
})
initSession(selectedAccount) await initSession(selectedAccount)
} else { } else {
logger.debug(`session: persisted onUpdate, updating session`, {}) logger.debug(
`session: persisted onUpdate, updating session`,
{},
logger.DebugContext.session,
)
/* /*
* Use updated session in this tab's agent. Do not call * Create a new agent for the same account, with updated data from
* upsertAccount, since that will only persist the session that's * other side of broadcast. Update on state to re-derive
* already persisted, and we'll get a loop between tabs. * `currentAccount` and re-render the app.
*/ */
// @ts-ignore we checked for `refreshJwt` above const newAgent = state.currentAgent.clone()
__globalAgent.session = selectedAccount newAgent.session = sessionAccountToAgentSession(selectedAccount)
await configureModerationForAccount(newAgent, selectedAccount)
setState(s => ({
accounts: s.accounts,
currentAgent: newAgent,
needsPersist: false,
}))
} }
} else if (!selectedAccount && state.currentAccountDid) { } else if (!selectedAccount && currentAccountDid) {
logger.debug( logger.debug(
`session: persisted onUpdate, logging out`, `session: persisted onUpdate, logging out`,
{}, {},
@@ -532,26 +485,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
*/ */
clearCurrentAccount() clearCurrentAccount()
} }
setState(() => ({
accounts: persistedSession.accounts,
currentAccountDid: selectedAccount?.did,
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
}))
}) })
}, [state, setState, clearCurrentAccount, initSession]) }, [
currentAccountDid,
state.currentAgent,
setState,
initSession,
clearCurrentAccount,
])
const stateContext = React.useMemo( const stateContext = React.useMemo(
() => ({ () => ({
accounts: state.accounts,
currentAccount: state.accounts.find(
a => a.did === state.currentAccountDid,
),
isInitialLoad, isInitialLoad,
isSwitchingAccounts, isSwitchingAccounts,
hasSession: !!state.currentAccountDid, currentAccount: state.accounts.find(a => a.did === currentAccountDid),
accounts: state.accounts,
currentAgent: state.currentAgent,
hasSession: Boolean(currentAccountDid),
}), }),
[state, isInitialLoad, isSwitchingAccounts], [
isInitialLoad,
isSwitchingAccounts,
state.accounts,
state.currentAgent,
currentAccountDid,
],
) )
const api = React.useMemo( const api = React.useMemo(
@@ -563,8 +521,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
resumeSession, resumeSession,
removeAccount, removeAccount,
selectAccount, selectAccount,
updateCurrentAccount, refreshSession,
clearCurrentAccount, clearCurrentAccount,
updateCurrentAccount,
}), }),
[ [
createAccount, createAccount,
@@ -574,11 +533,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
resumeSession, resumeSession,
removeAccount, removeAccount,
selectAccount, selectAccount,
updateCurrentAccount, refreshSession,
clearCurrentAccount, clearCurrentAccount,
updateCurrentAccount,
], ],
) )
if (IS_DEV && isWeb) {
// @ts-ignore
window.agent = currentAgent
}
return ( return (
<StateContext.Provider value={stateContext}> <StateContext.Provider value={stateContext}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider> <ApiContext.Provider value={api}>{children}</ApiContext.Provider>
@@ -596,8 +561,8 @@ export function useSessionApi() {
export function useRequireAuth() { export function useRequireAuth() {
const {hasSession} = useSession() const {hasSession} = useSession()
const {setShowLoggedOut} = useLoggedOutViewControls()
const closeAll = useCloseAllActiveElements() const closeAll = useCloseAllActiveElements()
const {signinDialogControl} = useGlobalDialogsControlContext()
return React.useCallback( return React.useCallback(
(fn: () => void) => { (fn: () => void) => {
@@ -605,13 +570,21 @@ export function useRequireAuth() {
fn() fn()
} else { } else {
closeAll() closeAll()
signinDialogControl.open() setShowLoggedOut(true)
} }
}, },
[hasSession, signinDialogControl, closeAll], [hasSession, setShowLoggedOut, closeAll],
) )
} }
export function useAgent() { export function useAgent() {
return React.useMemo(() => ({getAgent: __getAgent}), []) const {currentAgent} = useSession()
return React.useMemo(
() => ({
getAgent() {
return currentAgent
},
}),
[currentAgent],
)
} }
+31 -2
View File
@@ -1,15 +1,37 @@
import {BskyAgent} from '@atproto/api'
import {LogEvents} from '#/lib/statsig/statsig' import {LogEvents} from '#/lib/statsig/statsig'
import {PersistedAccount} from '#/state/persisted' import {PersistedAccount} from '#/state/persisted'
/**
* Alias for `PersistedAccount` from persisted storage.
*/
export type SessionAccount = PersistedAccount export type SessionAccount = PersistedAccount
/**
* Subset of `SessionAccount` that excludes tokens.
*/
export type CurrentAccount = Omit<SessionAccount, 'accessJwt' | 'refreshJwt'>
/**
* Context shape returned from `useSession()`
*/
export type SessionStateContext = { export type SessionStateContext = {
accounts: SessionAccount[] currentAgent: BskyAgent
currentAccount: SessionAccount | undefined
isInitialLoad: boolean isInitialLoad: boolean
isSwitchingAccounts: boolean isSwitchingAccounts: boolean
hasSession: boolean hasSession: boolean
accounts: SessionAccount[]
/**
* Contains the full account object persisted to storage, minus access
* tokens.
*/
currentAccount: CurrentAccount | undefined
} }
/**
* Context shape returned from `useSessionApi()`
*/
export type SessionApiContext = { export type SessionApiContext = {
createAccount: (props: { createAccount: (props: {
service: string service: string
@@ -52,6 +74,13 @@ export type SessionApiContext = {
account: SessionAccount, account: SessionAccount,
logContext: LogEvents['account:loggedIn']['logContext'], logContext: LogEvents['account:loggedIn']['logContext'],
) => Promise<void> ) => Promise<void>
/**
* Refreshes the BskyAgent's session and derive a fresh `currentAccount`
*/
refreshSession: () => void
/**
* @deprecated Use `refreshSession` instead.
*/
updateCurrentAccount: ( updateCurrentAccount: (
account: Partial< account: Partial<
Pick< Pick<
+14
View File
@@ -43,6 +43,20 @@ export function agentToSessionAccount(
} }
} }
export function sessionAccountToAgentSession(
account: SessionAccount,
): BskyAgent['session'] {
return {
refreshJwt: account.refreshJwt || '',
accessJwt: account.accessJwt || '',
did: account.did,
handle: account.handle,
email: account.email,
emailConfirmed: account.emailConfirmed,
emailAuthFactor: account.emailAuthFactor,
}
}
export function configureModerationForGuest() { export function configureModerationForGuest() {
switchToBskyAppLabeler() switchToBskyAppLabeler()
} }