Session V2
This commit is contained in:
+307
-334
@@ -1,17 +1,18 @@
|
||||
import React from 'react'
|
||||
import {AtpPersistSessionHandler, BskyAgent} from '@atproto/api'
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
|
||||
import {track} from '#/lib/analytics/analytics'
|
||||
import {networkRetry} from '#/lib/async/retry'
|
||||
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 {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {emitSessionDropped} from '../events'
|
||||
import {
|
||||
SessionAccount,
|
||||
SessionApiContext,
|
||||
SessionStateContext,
|
||||
} from '#/state/session/types'
|
||||
import {
|
||||
agentToSessionAccount,
|
||||
configureModerationForAccount,
|
||||
@@ -20,21 +21,26 @@ import {
|
||||
createAgentAndLogin,
|
||||
isSessionDeactivated,
|
||||
isSessionExpired,
|
||||
} from './util'
|
||||
|
||||
export type {SessionAccount} from '#/state/session/types'
|
||||
import {
|
||||
SessionAccount,
|
||||
SessionApiContext,
|
||||
SessionStateContext,
|
||||
} from '#/state/session/types'
|
||||
sessionAccountToAgentSession,
|
||||
} from '#/state/session/util'
|
||||
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {IS_DEV} from '#/env'
|
||||
import {emitSessionDropped} from '../events'
|
||||
|
||||
export type {CurrentAccount, SessionAccount} from '#/state/session/types'
|
||||
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()
|
||||
|
||||
const StateContext = React.createContext<SessionStateContext>({
|
||||
currentAgent: INITIAL_AGENT,
|
||||
isInitialLoad: true,
|
||||
isSwitchingAccounts: false,
|
||||
accounts: [],
|
||||
@@ -50,127 +56,126 @@ const ApiContext = React.createContext<SessionApiContext>({
|
||||
resumeSession: async () => {},
|
||||
removeAccount: () => {},
|
||||
selectAccount: async () => {},
|
||||
updateCurrentAccount: () => {},
|
||||
refreshSession: () => {},
|
||||
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<{}>) {
|
||||
const [isInitialLoad, setIsInitialLoad] = React.useState(true)
|
||||
const [isSwitchingAccounts, setIsSwitchingAccounts] = React.useState(false)
|
||||
const [state, setState] = React.useState<State>({
|
||||
const [state, setState] = React.useState(() => ({
|
||||
accounts: persisted.get('session').accounts,
|
||||
currentAccountDid: undefined, // assume logged out to start
|
||||
currentAgent: INITIAL_AGENT,
|
||||
needsPersist: false,
|
||||
})
|
||||
}))
|
||||
const currentAccountDid = React.useMemo(
|
||||
() => state.currentAgent.session?.did,
|
||||
[state.currentAgent],
|
||||
)
|
||||
|
||||
const upsertAccount = React.useCallback(
|
||||
(account: SessionAccount, expired = false) => {
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
|
||||
currentAccountDid: expired ? undefined : account.did,
|
||||
needsPersist: true,
|
||||
}
|
||||
})
|
||||
const upsertAndPersistAccount = React.useCallback(
|
||||
(account: SessionAccount) => {
|
||||
setState(s => ({
|
||||
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
|
||||
currentAgent: s.currentAgent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
},
|
||||
[setState],
|
||||
)
|
||||
|
||||
const clearCurrentAccount = React.useCallback(() => {
|
||||
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()
|
||||
setState(s => ({
|
||||
accounts: s.accounts,
|
||||
currentAccountDid: undefined,
|
||||
currentAgent: newAgent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
}, [setState])
|
||||
}, [state.currentAgent, setState])
|
||||
|
||||
const createPersistSessionHandler = React.useCallback(
|
||||
(
|
||||
agent: BskyAgent,
|
||||
account: SessionAccount,
|
||||
persistSessionCallback: (props: {
|
||||
expired: boolean
|
||||
refreshedAccount: SessionAccount
|
||||
}) => void,
|
||||
{
|
||||
networkErrorCallback,
|
||||
}: {
|
||||
networkErrorCallback?: () => void
|
||||
} = {},
|
||||
): AtpPersistSessionHandler => {
|
||||
return function persistSession(event, session) {
|
||||
const expired = event === 'expired' || event === 'create-failed'
|
||||
React.useEffect(() => {
|
||||
/*
|
||||
* This method is continually overwritten when `currentAgent` and dependent
|
||||
* methods local to this file change, so that the freshest agent and
|
||||
* handlers are always used.
|
||||
*/
|
||||
state.currentAgent.setPersistSessionHandler(event => {
|
||||
logger.debug(
|
||||
`session: persistSession`,
|
||||
{event},
|
||||
logger.DebugContext.session,
|
||||
)
|
||||
|
||||
if (event === 'network-error') {
|
||||
logger.warn(
|
||||
`session: persistSessionHandler received network-error event`,
|
||||
)
|
||||
networkErrorCallback?.()
|
||||
return
|
||||
}
|
||||
const expired = event === 'expired' || event === 'create-failed'
|
||||
|
||||
// TODO: use agentToSessionAccount for this too.
|
||||
const refreshedAccount: SessionAccount = {
|
||||
service: account.service,
|
||||
did: session?.did || account.did,
|
||||
handle: session?.handle || account.handle,
|
||||
email: session?.email || account.email,
|
||||
emailConfirmed: session?.emailConfirmed || account.emailConfirmed,
|
||||
emailAuthFactor: session?.emailAuthFactor || account.emailAuthFactor,
|
||||
deactivated: isSessionDeactivated(session?.accessJwt),
|
||||
pdsUrl: agent.pdsUrl?.toString(),
|
||||
|
||||
/*
|
||||
* Tokens are undefined if the session expires, or if creation fails for
|
||||
* any reason e.g. tokens are invalid, network error, etc.
|
||||
*/
|
||||
refreshJwt: session?.refreshJwt,
|
||||
accessJwt: session?.accessJwt,
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
/*
|
||||
* Special case for a network error that occurs when calling
|
||||
* `resumeSession`, which happens on page load, when switching
|
||||
* accounts, or when refreshing user session data.
|
||||
*
|
||||
* When this occurs, we drop the user back out to the login screen, but
|
||||
* we don't clear tokens, allowing them to quickly log back in when their
|
||||
* connection improves.
|
||||
*/
|
||||
if (event === 'network-error') {
|
||||
logger.warn(
|
||||
`session: persistSessionHandler received network-error event`,
|
||||
)
|
||||
emitSessionDropped()
|
||||
clearCurrentAccount()
|
||||
setTimeout(() => {
|
||||
Toast.show(`Your internet connection is unstable. Please try again.`)
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
/*
|
||||
* 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']>(
|
||||
async ({
|
||||
@@ -185,6 +190,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
logger.info(`session: creating account`)
|
||||
track('Try Create Account')
|
||||
logEvent('account:create:begin', {})
|
||||
|
||||
const {agent, account, fetchingGates} = await createAgentAndCreateAccount(
|
||||
{
|
||||
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
|
||||
upsertAccount(account)
|
||||
setState(s => ({
|
||||
accounts: s.accounts,
|
||||
currentAgent: agent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
upsertAndPersistAccount(account)
|
||||
|
||||
logger.debug(`session: created account`, {}, logger.DebugContext.session)
|
||||
track('Create Account')
|
||||
logEvent('account:create:success', {})
|
||||
},
|
||||
[upsertAccount, clearCurrentAccount, createPersistSessionHandler],
|
||||
[upsertAndPersistAccount],
|
||||
)
|
||||
|
||||
const login = React.useCallback<SessionApiContext['login']>(
|
||||
async ({service, identifier, password, authFactorToken}, logContext) => {
|
||||
logger.debug(`session: login`, {}, logger.DebugContext.session)
|
||||
|
||||
const {agent, account, fetchingGates} = await createAgentAndLogin({
|
||||
service,
|
||||
identifier,
|
||||
@@ -229,46 +229,36 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
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
|
||||
upsertAccount(account)
|
||||
setState(s => ({
|
||||
accounts: s.accounts,
|
||||
currentAgent: agent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
upsertAndPersistAccount(account)
|
||||
|
||||
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
|
||||
|
||||
track('Sign In', {resumedSession: false})
|
||||
logEvent('account:loggedIn', {logContext, withPassword: true})
|
||||
},
|
||||
[upsertAccount, clearCurrentAccount, createPersistSessionHandler],
|
||||
[upsertAndPersistAccount],
|
||||
)
|
||||
|
||||
const logout = React.useCallback<SessionApiContext['logout']>(
|
||||
async logContext => {
|
||||
logger.debug(`session: logout`)
|
||||
|
||||
clearCurrentAccount()
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: s.accounts.map(a => ({
|
||||
...a,
|
||||
refreshJwt: undefined,
|
||||
accessJwt: undefined,
|
||||
})),
|
||||
currentAccountDid: s.currentAccountDid,
|
||||
needsPersist: true,
|
||||
}
|
||||
})
|
||||
setState(s => ({
|
||||
accounts: s.accounts.map(a => ({
|
||||
...a,
|
||||
accessJwt: undefined,
|
||||
refreshJwt: undefined,
|
||||
})),
|
||||
currentAgent: s.currentAgent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
|
||||
logEvent('account:loggedOut', {logContext})
|
||||
},
|
||||
[clearCurrentAccount, setState],
|
||||
@@ -277,116 +267,67 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const initSession = React.useCallback<SessionApiContext['initSession']>(
|
||||
async account => {
|
||||
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
|
||||
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 = {
|
||||
...account,
|
||||
accessJwt: account.accessJwt || '',
|
||||
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)) {
|
||||
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
|
||||
* make TypeScript happy.
|
||||
* If session is expired, attempt to refresh the session using the
|
||||
* refresh token via `resumeSession`
|
||||
*/
|
||||
if (!sessionAccount) {
|
||||
throw new Error(`session: initSession failed to establish a session`)
|
||||
}
|
||||
return sessionAccount
|
||||
logger.debug(
|
||||
`session: attempting to resumeSession using previous session`,
|
||||
{},
|
||||
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']>(
|
||||
@@ -401,59 +342,46 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
setIsInitialLoad(false)
|
||||
}
|
||||
},
|
||||
[initSession],
|
||||
[initSession, setIsInitialLoad],
|
||||
)
|
||||
|
||||
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
||||
account => {
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: s.accounts.filter(a => a.did !== account.did),
|
||||
currentAccountDid: s.currentAccountDid,
|
||||
needsPersist: true,
|
||||
}
|
||||
})
|
||||
setState(s => ({
|
||||
accounts: s.accounts.filter(a => a.did !== account.did),
|
||||
currentAgent: s.currentAgent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
},
|
||||
[setState],
|
||||
)
|
||||
|
||||
const updateCurrentAccount = React.useCallback<
|
||||
SessionApiContext['updateCurrentAccount']
|
||||
>(
|
||||
account => {
|
||||
setState(s => {
|
||||
const currentAccount = s.accounts.find(
|
||||
a => a.did === s.currentAccountDid,
|
||||
)
|
||||
// ignore, should never happen
|
||||
if (!currentAccount) return s
|
||||
const refreshSession = React.useCallback<
|
||||
SessionApiContext['refreshSession']
|
||||
>(async () => {
|
||||
const {accounts: persistedAccounts} = persisted.get('session')
|
||||
const selectedAccount = persistedAccounts.find(
|
||||
a => a.did === currentAccountDid,
|
||||
)
|
||||
if (!selectedAccount) return
|
||||
|
||||
const updatedAccount = {
|
||||
...currentAccount,
|
||||
handle: account.handle || currentAccount.handle,
|
||||
email: account.email || currentAccount.email,
|
||||
emailConfirmed:
|
||||
account.emailConfirmed !== undefined
|
||||
? account.emailConfirmed
|
||||
: currentAccount.emailConfirmed,
|
||||
emailAuthFactor:
|
||||
account.emailAuthFactor !== undefined
|
||||
? account.emailAuthFactor
|
||||
: currentAccount.emailAuthFactor,
|
||||
}
|
||||
// update and swap agent to trigger render refresh
|
||||
const newAgent = state.currentAgent.clone()
|
||||
await newAgent.resumeSession(sessionAccountToAgentSession(selectedAccount)!)
|
||||
const refreshedAccount = agentToSessionAccount(newAgent)
|
||||
await configureModerationForAccount(newAgent, refreshedAccount!)
|
||||
|
||||
return {
|
||||
accounts: [
|
||||
updatedAccount,
|
||||
...s.accounts.filter(a => a.did !== currentAccount.did),
|
||||
],
|
||||
currentAccountDid: s.currentAccountDid,
|
||||
needsPersist: true,
|
||||
}
|
||||
})
|
||||
},
|
||||
[setState],
|
||||
)
|
||||
upsertAndPersistAccount(refreshedAccount!)
|
||||
setState(s => ({
|
||||
accounts: s.accounts,
|
||||
currentAgent: newAgent,
|
||||
needsPersist: true,
|
||||
}))
|
||||
}, [currentAccountDid, state.currentAgent, setState, upsertAndPersistAccount])
|
||||
|
||||
const updateCurrentAccount = React.useCallback(async () => {
|
||||
await refreshSession()
|
||||
}, [refreshSession])
|
||||
|
||||
const selectAccount = React.useCallback<SessionApiContext['selectAccount']>(
|
||||
async (account, logContext) => {
|
||||
@@ -469,7 +397,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
throw e
|
||||
}
|
||||
},
|
||||
[initSession],
|
||||
[setIsSwitchingAccounts, initSession],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -477,47 +405,72 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
state.needsPersist = false
|
||||
persisted.write('session', {
|
||||
accounts: state.accounts,
|
||||
currentAccount: state.accounts.find(
|
||||
a => a.did === state.currentAccountDid,
|
||||
),
|
||||
currentAccount: state.accounts.find(a => a.did === currentAccountDid),
|
||||
})
|
||||
}
|
||||
}, [state])
|
||||
}, [state, currentAccountDid])
|
||||
|
||||
React.useEffect(() => {
|
||||
return persisted.onUpdate(() => {
|
||||
return persisted.onUpdate(async () => {
|
||||
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(
|
||||
a => a.did === persistedSession.currentAccount?.did,
|
||||
)
|
||||
|
||||
if (selectedAccount && selectedAccount.refreshJwt) {
|
||||
if (selectedAccount.did !== state.currentAccountDid) {
|
||||
logger.debug(`session: persisted onUpdate, switching accounts`, {
|
||||
from: {
|
||||
did: state.currentAccountDid,
|
||||
if (selectedAccount?.did !== currentAccountDid) {
|
||||
logger.debug(
|
||||
`session: persisted onUpdate, switching accounts`,
|
||||
{
|
||||
from: {
|
||||
did: currentAccountDid,
|
||||
},
|
||||
to: {
|
||||
did: selectedAccount.did,
|
||||
},
|
||||
},
|
||||
to: {
|
||||
did: selectedAccount.did,
|
||||
},
|
||||
})
|
||||
logger.DebugContext.session,
|
||||
)
|
||||
|
||||
initSession(selectedAccount)
|
||||
await initSession(selectedAccount)
|
||||
} 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
|
||||
* upsertAccount, since that will only persist the session that's
|
||||
* already persisted, and we'll get a loop between tabs.
|
||||
* Create a new agent for the same account, with updated data from
|
||||
* other side of broadcast. Update on state to re-derive
|
||||
* `currentAccount` and re-render the app.
|
||||
*/
|
||||
// @ts-ignore we checked for `refreshJwt` above
|
||||
__globalAgent.session = selectedAccount
|
||||
const newAgent = state.currentAgent.clone()
|
||||
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(
|
||||
`session: persisted onUpdate, logging out`,
|
||||
{},
|
||||
@@ -532,26 +485,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
*/
|
||||
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(
|
||||
() => ({
|
||||
accounts: state.accounts,
|
||||
currentAccount: state.accounts.find(
|
||||
a => a.did === state.currentAccountDid,
|
||||
),
|
||||
isInitialLoad,
|
||||
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(
|
||||
@@ -563,8 +521,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
resumeSession,
|
||||
removeAccount,
|
||||
selectAccount,
|
||||
updateCurrentAccount,
|
||||
refreshSession,
|
||||
clearCurrentAccount,
|
||||
updateCurrentAccount,
|
||||
}),
|
||||
[
|
||||
createAccount,
|
||||
@@ -574,11 +533,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
resumeSession,
|
||||
removeAccount,
|
||||
selectAccount,
|
||||
updateCurrentAccount,
|
||||
refreshSession,
|
||||
clearCurrentAccount,
|
||||
updateCurrentAccount,
|
||||
],
|
||||
)
|
||||
|
||||
if (IS_DEV && isWeb) {
|
||||
// @ts-ignore
|
||||
window.agent = currentAgent
|
||||
}
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={stateContext}>
|
||||
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
|
||||
@@ -596,8 +561,8 @@ export function useSessionApi() {
|
||||
|
||||
export function useRequireAuth() {
|
||||
const {hasSession} = useSession()
|
||||
const {setShowLoggedOut} = useLoggedOutViewControls()
|
||||
const closeAll = useCloseAllActiveElements()
|
||||
const {signinDialogControl} = useGlobalDialogsControlContext()
|
||||
|
||||
return React.useCallback(
|
||||
(fn: () => void) => {
|
||||
@@ -605,13 +570,21 @@ export function useRequireAuth() {
|
||||
fn()
|
||||
} else {
|
||||
closeAll()
|
||||
signinDialogControl.open()
|
||||
setShowLoggedOut(true)
|
||||
}
|
||||
},
|
||||
[hasSession, signinDialogControl, closeAll],
|
||||
[hasSession, setShowLoggedOut, closeAll],
|
||||
)
|
||||
}
|
||||
|
||||
export function useAgent() {
|
||||
return React.useMemo(() => ({getAgent: __getAgent}), [])
|
||||
const {currentAgent} = useSession()
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
getAgent() {
|
||||
return currentAgent
|
||||
},
|
||||
}),
|
||||
[currentAgent],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
import {BskyAgent} from '@atproto/api'
|
||||
|
||||
import {LogEvents} from '#/lib/statsig/statsig'
|
||||
import {PersistedAccount} from '#/state/persisted'
|
||||
|
||||
/**
|
||||
* Alias for `PersistedAccount` from persisted storage.
|
||||
*/
|
||||
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 = {
|
||||
accounts: SessionAccount[]
|
||||
currentAccount: SessionAccount | undefined
|
||||
currentAgent: BskyAgent
|
||||
isInitialLoad: boolean
|
||||
isSwitchingAccounts: 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 = {
|
||||
createAccount: (props: {
|
||||
service: string
|
||||
@@ -52,6 +74,13 @@ export type SessionApiContext = {
|
||||
account: SessionAccount,
|
||||
logContext: LogEvents['account:loggedIn']['logContext'],
|
||||
) => Promise<void>
|
||||
/**
|
||||
* Refreshes the BskyAgent's session and derive a fresh `currentAccount`
|
||||
*/
|
||||
refreshSession: () => void
|
||||
/**
|
||||
* @deprecated Use `refreshSession` instead.
|
||||
*/
|
||||
updateCurrentAccount: (
|
||||
account: Partial<
|
||||
Pick<
|
||||
|
||||
@@ -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() {
|
||||
switchToBskyAppLabeler()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user