[Session] Remove global agent (#3852)

* Remove logs and outdated comments

* Move side effect upwards

* Pull refreshedAccount next to usage

* Simplify account refresh logic

* Extract setupPublicAgentState()

* Collapse setStates into one

* Ignore events from stale agents

* Use agent from state

* Remove clearCurrentAccount

* Move state to a reducer

* Remove global agent

* Fix stale agent reference in create flow

* Proceed to onboarding even if setting date fails

---------

Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
dan 2024-05-08 03:10:03 +01:00 committed by GitHub
parent 31a8356aef
commit 0c41b3188a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 265 additions and 360 deletions

View File

@ -8,16 +8,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import * as EmailValidator from 'email-validator' import * as EmailValidator from 'email-validator'
import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants' import {DEFAULT_SERVICE} from '#/lib/constants'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {createFullHandle, validateHandle} from '#/lib/strings/handles' import {createFullHandle, validateHandle} from '#/lib/strings/handles'
import {getAge} from '#/lib/strings/time' import {getAge} from '#/lib/strings/time'
import {logger} from '#/logger' import {logger} from '#/logger'
import {
DEFAULT_PROD_FEEDS,
usePreferencesSetBirthDateMutation,
useSetSaveFeedsMutation,
} from '#/state/queries/preferences'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
@ -207,8 +202,6 @@ export function useSubmitSignup({
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const {createAccount} = useSessionApi() const {createAccount} = useSessionApi()
const {mutateAsync: setBirthDate} = usePreferencesSetBirthDateMutation()
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
const onboardingDispatch = useOnboardingDispatch() const onboardingDispatch = useOnboardingDispatch()
return useCallback( return useCallback(
@ -265,13 +258,10 @@ export function useSubmitSignup({
email: state.email, email: state.email,
handle: createFullHandle(state.handle, state.userDomain), handle: createFullHandle(state.handle, state.userDomain),
password: state.password, password: state.password,
birthDate: state.dateOfBirth,
inviteCode: state.inviteCode.trim(), inviteCode: state.inviteCode.trim(),
verificationCode: verificationCode, verificationCode: verificationCode,
}) })
await setBirthDate({birthDate: state.dateOfBirth})
if (IS_PROD_SERVICE(state.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS)
}
} catch (e: any) { } catch (e: any) {
onboardingDispatch({type: 'skip'}) // undo starting the onboard onboardingDispatch({type: 'skip'}) // undo starting the onboard
let errMsg = e.toString() let errMsg = e.toString()
@ -314,8 +304,6 @@ export function useSubmitSignup({
_, _,
onboardingDispatch, onboardingDispatch,
createAccount, createAccount,
setBirthDate,
setSavedFeeds,
], ],
) )
} }

View File

@ -1,11 +1,10 @@
import React from 'react' import React from 'react'
import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' import {AtpSessionEvent, 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, tryFetchGates} from '#/lib/statsig/statsig'
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 {useCloseAllActiveElements} from '#/state/util'
@ -31,15 +30,14 @@ import {
export {isSessionDeactivated} export {isSessionDeactivated}
const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
configureModerationForGuest()
const StateContext = React.createContext<SessionStateContext>({ const StateContext = React.createContext<SessionStateContext>({
accounts: [], accounts: [],
currentAccount: undefined, currentAccount: undefined,
hasSession: false, hasSession: false,
}) })
const AgentContext = React.createContext<BskyAgent | null>(null)
const ApiContext = React.createContext<SessionApiContext>({ const ApiContext = React.createContext<SessionApiContext>({
createAccount: async () => {}, createAccount: async () => {},
login: async () => {}, login: async () => {},
@ -47,15 +45,8 @@ const ApiContext = React.createContext<SessionApiContext>({
initSession: async () => {}, initSession: async () => {},
removeAccount: () => {}, removeAccount: () => {},
updateCurrentAccount: () => {}, updateCurrentAccount: () => {},
clearCurrentAccount: () => {},
}) })
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
function __getAgent() {
return __globalAgent
}
type AgentState = { type AgentState = {
readonly agent: BskyAgent readonly agent: BskyAgent
readonly did: string | undefined readonly did: string | undefined
@ -67,127 +58,187 @@ type State = {
needsPersist: boolean needsPersist: boolean
} }
export function Provider({children}: React.PropsWithChildren<{}>) { type Action =
const [state, setState] = React.useState<State>(() => ({ | {
accounts: persisted.get('session').accounts, type: 'received-agent-event'
currentAgentState: { agent: BskyAgent
agent: PUBLIC_BSKY_AGENT, accountDid: string
did: undefined, // assume logged out to start refreshedAccount: SessionAccount | undefined
}, sessionEvent: AtpSessionEvent
needsPersist: false, }
})) | {
type: 'switched-to-account'
newAgent: BskyAgent
newAccount: SessionAccount
}
| {
type: 'updated-current-account'
updatedFields: Partial<
Pick<
SessionAccount,
'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor'
>
>
}
| {
type: 'removed-account'
accountDid: string
}
| {
type: 'logged-out'
}
| {
type: 'synced-accounts'
syncedAccounts: SessionAccount[]
syncedCurrentDid: string | undefined
}
const clearCurrentAccount = React.useCallback(() => { function createPublicAgentState() {
logger.warn(`session: clear current account`) configureModerationForGuest() // Side effect but only relevant for tests
__globalAgent = PUBLIC_BSKY_AGENT return {
configureModerationForGuest() agent: new BskyAgent({service: PUBLIC_BSKY_SERVICE}),
setState(s => ({ did: undefined,
accounts: s.accounts, }
currentAgentState: { }
agent: PUBLIC_BSKY_AGENT,
did: undefined, function getInitialState(): State {
}, return {
needsPersist: true, accounts: persisted.get('session').accounts,
})) currentAgentState: createPublicAgentState(),
}, [setState]) needsPersist: false,
}
}
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'received-agent-event': {
const {agent, accountDid, refreshedAccount, sessionEvent} = action
if (agent !== state.currentAgentState.agent) {
// Only consider events from the active agent.
return state
}
if (sessionEvent === 'network-error') {
// Don't change stored accounts but kick to the choose account screen.
return {
accounts: state.accounts,
currentAgentState: createPublicAgentState(),
needsPersist: true,
}
}
const existingAccount = state.accounts.find(a => a.did === accountDid)
if (
!existingAccount ||
JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount)
) {
// Fast path without a state update.
return state
}
return {
accounts: state.accounts.map(a => {
if (a.did === accountDid) {
if (refreshedAccount) {
return refreshedAccount
} else {
return {
...a,
// If we didn't receive a refreshed account, clear out the tokens.
accessJwt: undefined,
refreshJwt: undefined,
}
}
} else {
return a
}
}),
currentAgentState: refreshedAccount
? state.currentAgentState
: createPublicAgentState(), // Log out if expired.
needsPersist: true,
}
}
case 'switched-to-account': {
const {newAccount, newAgent} = action
return {
accounts: [
newAccount,
...state.accounts.filter(a => a.did !== newAccount.did),
],
currentAgentState: {
did: newAccount.did,
agent: newAgent,
},
needsPersist: true,
}
}
case 'updated-current-account': {
const {updatedFields} = action
return {
accounts: state.accounts.map(a => {
if (a.did === state.currentAgentState.did) {
return {
...a,
...updatedFields,
}
} else {
return a
}
}),
currentAgentState: state.currentAgentState,
needsPersist: true,
}
}
case 'removed-account': {
const {accountDid} = action
return {
accounts: state.accounts.filter(a => a.did !== accountDid),
currentAgentState:
state.currentAgentState.did === accountDid
? createPublicAgentState() // Log out if removing the current one.
: state.currentAgentState,
needsPersist: true,
}
}
case 'logged-out': {
return {
accounts: state.accounts.map(a => ({
...a,
// Clear tokens for *every* account (this is a hard logout).
refreshJwt: undefined,
accessJwt: undefined,
})),
currentAgentState: createPublicAgentState(),
needsPersist: true,
}
}
case 'synced-accounts': {
const {syncedAccounts, syncedCurrentDid} = action
return {
accounts: syncedAccounts,
currentAgentState:
syncedCurrentDid === state.currentAgentState.did
? state.currentAgentState
: createPublicAgentState(), // Log out if different user.
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
}
}
}
}
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, dispatch] = React.useReducer(reducer, null, getInitialState)
const onAgentSessionChange = React.useCallback( const onAgentSessionChange = React.useCallback(
( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
agent: BskyAgent, const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.
account: SessionAccount, if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
event: AtpSessionEvent,
session: AtpSessionData | undefined,
) => {
const expired = event === 'expired' || event === 'create-failed'
if (event === 'network-error') {
logger.warn(
`session: persistSessionHandler received network-error event`,
)
logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT
configureModerationForGuest()
setState(s => ({
accounts: s.accounts,
currentAgentState: {
agent: PUBLIC_BSKY_AGENT,
did: undefined,
},
needsPersist: true,
}))
return
}
// 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() emitSessionDropped()
__globalAgent = PUBLIC_BSKY_AGENT
configureModerationForGuest()
setState(s => ({
accounts: s.accounts,
currentAgentState: {
agent: PUBLIC_BSKY_AGENT,
did: undefined,
},
needsPersist: true,
}))
} }
dispatch({
/* type: 'received-agent-event',
* If the session expired, or it was successfully created/updated, we want agent,
* to update/persist the data. refreshedAccount,
* accountDid,
* If the session creation failed, it could be a network error, or it could sessionEvent,
* 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.
*/
setState(s => {
const existingAccount = s.accounts.find(
a => a.did === refreshedAccount.did,
)
if (
!expired &&
existingAccount &&
refreshedAccount &&
JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount)
) {
// Fast path without a state update.
return s
}
return {
accounts: [
refreshedAccount,
...s.accounts.filter(a => a.did !== refreshedAccount.did),
],
currentAgentState: s.currentAgentState,
needsPersist: true,
}
}) })
}, },
[], [],
@ -199,11 +250,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
email, email,
password, password,
handle, handle,
birthDate,
inviteCode, inviteCode,
verificationPhone, verificationPhone,
verificationCode, verificationCode,
}) => { }) => {
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(
@ -212,30 +263,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
email, email,
password, password,
handle, handle,
birthDate,
inviteCode, inviteCode,
verificationPhone, verificationPhone,
verificationCode, verificationCode,
}, },
) )
agent.setPersistSessionHandler(event => {
agent.setPersistSessionHandler((event, session) => { onAgentSessionChange(agent, account.did, event)
onAgentSessionChange(agent, account, event, session)
}) })
__globalAgent = agent
await fetchingGates await fetchingGates
setState(s => { dispatch({
return { type: 'switched-to-account',
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], newAgent: agent,
currentAgentState: { newAccount: account,
did: account.did,
agent: agent,
},
needsPersist: true,
}
}) })
logger.debug(`session: created account`, {}, logger.DebugContext.session)
track('Create Account') track('Create Account')
logEvent('account:create:success', {}) logEvent('account:create:success', {})
}, },
@ -244,35 +286,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
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)
const {agent, account, fetchingGates} = await createAgentAndLogin({ const {agent, account, fetchingGates} = await createAgentAndLogin({
service, service,
identifier, identifier,
password, password,
authFactorToken, authFactorToken,
}) })
agent.setPersistSessionHandler(event => {
agent.setPersistSessionHandler((event, session) => { onAgentSessionChange(agent, account.did, event)
onAgentSessionChange(agent, account, event, session)
}) })
__globalAgent = agent
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await fetchingGates await fetchingGates
setState(s => { dispatch({
return { type: 'switched-to-account',
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], newAgent: agent,
currentAgentState: { newAccount: account,
did: account.did,
agent: agent,
},
needsPersist: true,
}
}) })
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})
}, },
@ -281,52 +309,27 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const logout = React.useCallback<SessionApiContext['logout']>( const logout = React.useCallback<SessionApiContext['logout']>(
async logContext => { async logContext => {
logger.debug(`session: logout`) dispatch({
logger.warn(`session: clear current account`) type: 'logged-out',
__globalAgent = PUBLIC_BSKY_AGENT
configureModerationForGuest()
setState(s => {
return {
accounts: s.accounts.map(a => ({
...a,
refreshJwt: undefined,
accessJwt: undefined,
})),
currentAgentState: {
did: undefined,
agent: PUBLIC_BSKY_AGENT,
},
needsPersist: true,
}
}) })
logEvent('account:loggedOut', {logContext}) logEvent('account:loggedOut', {logContext})
}, },
[setState], [],
) )
const initSession = React.useCallback<SessionApiContext['initSession']>( const initSession = React.useCallback<SessionApiContext['initSession']>(
async account => { async account => {
logger.debug(`session: initSession`, {}, logger.DebugContext.session)
const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency') const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency')
const agent = new BskyAgent({service: account.service}) const agent = 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) agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl)
} }
agent.setPersistSessionHandler(event => {
agent.setPersistSessionHandler((event, session) => { onAgentSessionChange(agent, account.did, event)
onAgentSessionChange(agent, account, event, session)
}) })
// @ts-ignore
if (IS_DEV && isWeb) window.agent = agent
await configureModerationForAccount(agent, account) await configureModerationForAccount(agent, account)
const accountOrSessionDeactivated =
isSessionDeactivated(account.accessJwt) || account.deactivated
const prevSession = { const prevSession = {
accessJwt: account.accessJwt ?? '', accessJwt: account.accessJwt ?? '',
refreshJwt: account.refreshJwt ?? '', refreshJwt: account.refreshJwt ?? '',
@ -335,59 +338,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
} }
if (isSessionExpired(account)) { if (isSessionExpired(account)) {
logger.debug(`session: attempting to resume using previous session`)
const freshAccount = await resumeSessionWithFreshAccount() const freshAccount = await resumeSessionWithFreshAccount()
__globalAgent = agent
await fetchingGates await fetchingGates
setState(s => { dispatch({
return { type: 'switched-to-account',
accounts: [ newAgent: agent,
freshAccount, newAccount: freshAccount,
...s.accounts.filter(a => a.did !== freshAccount.did),
],
currentAgentState: {
did: freshAccount.did,
agent: agent,
},
needsPersist: true,
}
}) })
} else { } else {
logger.debug(`session: attempting to reuse previous session`)
agent.session = prevSession agent.session = prevSession
__globalAgent = agent
await fetchingGates await fetchingGates
setState(s => { dispatch({
return { type: 'switched-to-account',
accounts: [ newAgent: agent,
account, newAccount: account,
...s.accounts.filter(a => a.did !== account.did),
],
currentAgentState: {
did: account.did,
agent: agent,
},
needsPersist: true,
}
}) })
if (isSessionDeactivated(account.accessJwt) || account.deactivated) {
if (accountOrSessionDeactivated) {
// don't attempt to resume // don't attempt to resume
// use will be taken to the deactivated screen // use will be taken to the deactivated screen
logger.debug(`session: reusing session for deactivated account`)
return return
} }
// Intentionally not awaited to unblock the UI: // Intentionally not awaited to unblock the UI:
resumeSessionWithFreshAccount() resumeSessionWithFreshAccount()
} }
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
logger.debug(`session: resumeSessionWithFreshAccount`)
await networkRetry(1, () => agent.resumeSession(prevSession)) await networkRetry(1, () => agent.resumeSession(prevSession))
const sessionAccount = agentToSessionAccount(agent) const sessionAccount = agentToSessionAccount(agent)
/* /*
@ -405,50 +380,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
account => { account => {
setState(s => { dispatch({
return { type: 'removed-account',
accounts: s.accounts.filter(a => a.did !== account.did), accountDid: account.did,
currentAgentState: s.currentAgentState,
needsPersist: true,
}
}) })
}, },
[setState], [],
) )
const updateCurrentAccount = React.useCallback< const updateCurrentAccount = React.useCallback<
SessionApiContext['updateCurrentAccount'] SessionApiContext['updateCurrentAccount']
>( >(account => {
account => { dispatch({
setState(s => { type: 'updated-current-account',
const currentAccount = s.accounts.find( updatedFields: account,
a => a.did === s.currentAgentState.did, })
) }, [])
// ignore, should never happen
if (!currentAccount) return s
const updatedAccount = {
...currentAccount,
handle: account.handle ?? currentAccount.handle,
email: account.email ?? currentAccount.email,
emailConfirmed:
account.emailConfirmed ?? currentAccount.emailConfirmed,
emailAuthFactor:
account.emailAuthFactor ?? currentAccount.emailAuthFactor,
}
return {
accounts: [
updatedAccount,
...s.accounts.filter(a => a.did !== currentAccount.did),
],
currentAgentState: s.currentAgentState,
needsPersist: true,
}
})
},
[setState],
)
React.useEffect(() => { React.useEffect(() => {
if (state.needsPersist) { if (state.needsPersist) {
@ -464,70 +411,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
React.useEffect(() => { React.useEffect(() => {
return persisted.onUpdate(() => { return persisted.onUpdate(() => {
const persistedSession = persisted.get('session') const synced = persisted.get('session')
dispatch({
logger.debug(`session: persisted onUpdate`, {}) type: 'synced-accounts',
setState(s => ({ syncedAccounts: synced.accounts,
accounts: persistedSession.accounts, syncedCurrentDid: synced.currentAccount?.did,
currentAgentState: s.currentAgentState, })
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. const syncedAccount = synced.accounts.find(
})) a => a.did === synced.currentAccount?.did,
const selectedAccount = persistedSession.accounts.find(
a => a.did === persistedSession.currentAccount?.did,
) )
if (syncedAccount && syncedAccount.refreshJwt) {
if (selectedAccount && selectedAccount.refreshJwt) { if (syncedAccount.did !== state.currentAgentState.did) {
if (selectedAccount.did !== state.currentAgentState.did) { initSession(syncedAccount)
logger.debug(`session: persisted onUpdate, switching accounts`, {
from: {
did: state.currentAgentState.did,
},
to: {
did: selectedAccount.did,
},
})
initSession(selectedAccount)
} else { } else {
logger.debug(`session: persisted onUpdate, updating 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.
*/
// @ts-ignore we checked for `refreshJwt` above // @ts-ignore we checked for `refreshJwt` above
__globalAgent.session = selectedAccount state.currentAgentState.agent.session = syncedAccount
// TODO: This needs a setState.
} }
} else if (!selectedAccount && state.currentAgentState.did) {
logger.debug(
`session: persisted onUpdate, logging out`,
{},
logger.DebugContext.session,
)
/*
* No need to do a hard logout here. If we reach this, tokens for this
* account have already been cleared either by an `expired` event
* handled by `persistSession` (which nukes this accounts tokens only),
* or by a `logout` call which nukes all accounts tokens)
*/
logger.warn(`session: clear current account`)
__globalAgent = PUBLIC_BSKY_AGENT
configureModerationForGuest()
setState(s => ({
accounts: s.accounts,
currentAgentState: {
did: undefined,
agent: PUBLIC_BSKY_AGENT,
},
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
}))
} }
}) })
}, [state, setState, initSession]) }, [state, initSession])
const stateContext = React.useMemo( const stateContext = React.useMemo(
() => ({ () => ({
@ -548,7 +450,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
initSession, initSession,
removeAccount, removeAccount,
updateCurrentAccount, updateCurrentAccount,
clearCurrentAccount,
}), }),
[ [
createAccount, createAccount,
@ -557,14 +458,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
initSession, initSession,
removeAccount, removeAccount,
updateCurrentAccount, updateCurrentAccount,
clearCurrentAccount,
], ],
) )
// @ts-ignore
if (IS_DEV && isWeb) window.agent = state.currentAgentState.agent
return ( return (
<StateContext.Provider value={stateContext}> <AgentContext.Provider value={state.currentAgentState.agent}>
<ApiContext.Provider value={api}>{children}</ApiContext.Provider> <StateContext.Provider value={stateContext}>
</StateContext.Provider> <ApiContext.Provider value={api}>{children}</ApiContext.Provider>
</StateContext.Provider>
</AgentContext.Provider>
) )
} }
@ -594,6 +499,17 @@ export function useRequireAuth() {
) )
} }
export function useAgent() { export function useAgent(): {getAgent: () => BskyAgent} {
return React.useMemo(() => ({getAgent: __getAgent}), []) const agent = React.useContext(AgentContext)
if (!agent) {
throw Error('useAgent() must be below <SessionProvider>.')
}
return React.useMemo(
() => ({
getAgent() {
return agent
},
}),
[agent],
)
} }

View File

@ -14,6 +14,7 @@ export type SessionApiContext = {
email: string email: string
password: string password: string
handle: string handle: string
birthDate: Date
inviteCode?: string inviteCode?: string
verificationPhone?: string verificationPhone?: string
verificationCode?: string verificationCode?: string
@ -35,14 +36,6 @@ export type SessionApiContext = {
logout: ( logout: (
logContext: LogEvents['account:loggedOut']['logContext'], logContext: LogEvents['account:loggedOut']['logContext'],
) => Promise<void> ) => Promise<void>
/**
* A partial logout. Clears the `currentAccount` from session, but DOES NOT
* clear access tokens from accounts, allowing the user to return to their
* other accounts without logging in.
*
* Used when adding a new account, deleting an account.
*/
clearCurrentAccount: () => void
initSession: (account: SessionAccount) => Promise<void> initSession: (account: SessionAccount) => Promise<void>
removeAccount: (account: SessionAccount) => void removeAccount: (account: SessionAccount) => void
updateCurrentAccount: ( updateCurrentAccount: (

View File

@ -1,11 +1,12 @@
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
import {jwtDecode} from 'jwt-decode' import {jwtDecode} from 'jwt-decode'
import {IS_TEST_USER} from '#/lib/constants' import {IS_PROD_SERVICE, IS_TEST_USER} from '#/lib/constants'
import {tryFetchGates} from '#/lib/statsig/statsig' import {tryFetchGates} from '#/lib/statsig/statsig'
import {hasProp} from '#/lib/type-guards' import {hasProp} from '#/lib/type-guards'
import {logger} from '#/logger' import {logger} from '#/logger'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {DEFAULT_PROD_FEEDS} from '#/state/queries/preferences'
import {readLabelers} from '../agent-config' import {readLabelers} from '../agent-config'
import {SessionAccount, SessionApiContext} from '../types' import {SessionAccount, SessionApiContext} from '../types'
@ -132,6 +133,7 @@ export async function createAgentAndCreateAccount({
email, email,
password, password,
handle, handle,
birthDate,
inviteCode, inviteCode,
verificationPhone, verificationPhone,
verificationCode, verificationCode,
@ -167,6 +169,13 @@ export async function createAgentAndCreateAccount({
}) })
} }
// Not awaited so that we can still get into onboarding.
// This is OK because we won't let you toggle adult stuff until you set the date.
agent.setPersonalDetails({birthDate: birthDate.toISOString()})
if (IS_PROD_SERVICE(service)) {
agent.setSavedFeeds(DEFAULT_PROD_FEEDS.saved, DEFAULT_PROD_FEEDS.pinned)
}
await configureModerationForAccount(agent, account) await configureModerationForAccount(agent, account)
return { return {

View File

@ -31,7 +31,7 @@ export function Component({}: {}) {
const theme = useTheme() const theme = useTheme()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {getAgent} = useAgent() const {getAgent} = useAgent()
const {clearCurrentAccount, removeAccount} = useSessionApi() const {removeAccount} = useSessionApi()
const {_} = useLingui() const {_} = useLingui()
const {closeModal} = useModalControls() const {closeModal} = useModalControls()
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
@ -69,7 +69,6 @@ export function Component({}: {}) {
Toast.show(_(msg`Your account has been deleted`)) Toast.show(_(msg`Your account has been deleted`))
resetToTab('HomeTab') resetToTab('HomeTab')
removeAccount(currentAccount) removeAccount(currentAccount)
clearCurrentAccount()
closeModal() closeModal()
} catch (e: any) { } catch (e: any) {
setError(cleanError(e)) setError(cleanError(e))