[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
parent
31a8356aef
commit
0c41b3188a
|
@ -8,16 +8,11 @@ import {msg} from '@lingui/macro'
|
|||
import {useLingui} from '@lingui/react'
|
||||
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 {createFullHandle, validateHandle} from '#/lib/strings/handles'
|
||||
import {getAge} from '#/lib/strings/time'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
DEFAULT_PROD_FEEDS,
|
||||
usePreferencesSetBirthDateMutation,
|
||||
useSetSaveFeedsMutation,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useSessionApi} from '#/state/session'
|
||||
import {useOnboardingDispatch} from '#/state/shell'
|
||||
|
||||
|
@ -207,8 +202,6 @@ export function useSubmitSignup({
|
|||
}) {
|
||||
const {_} = useLingui()
|
||||
const {createAccount} = useSessionApi()
|
||||
const {mutateAsync: setBirthDate} = usePreferencesSetBirthDateMutation()
|
||||
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
|
||||
const onboardingDispatch = useOnboardingDispatch()
|
||||
|
||||
return useCallback(
|
||||
|
@ -265,13 +258,10 @@ export function useSubmitSignup({
|
|||
email: state.email,
|
||||
handle: createFullHandle(state.handle, state.userDomain),
|
||||
password: state.password,
|
||||
birthDate: state.dateOfBirth,
|
||||
inviteCode: state.inviteCode.trim(),
|
||||
verificationCode: verificationCode,
|
||||
})
|
||||
await setBirthDate({birthDate: state.dateOfBirth})
|
||||
if (IS_PROD_SERVICE(state.serviceUrl)) {
|
||||
setSavedFeeds(DEFAULT_PROD_FEEDS)
|
||||
}
|
||||
} catch (e: any) {
|
||||
onboardingDispatch({type: 'skip'}) // undo starting the onboard
|
||||
let errMsg = e.toString()
|
||||
|
@ -314,8 +304,6 @@ export function useSubmitSignup({
|
|||
_,
|
||||
onboardingDispatch,
|
||||
createAccount,
|
||||
setBirthDate,
|
||||
setSavedFeeds,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React from 'react'
|
||||
import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api'
|
||||
import {AtpSessionEvent, 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 {logger} from '#/logger'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {useCloseAllActiveElements} from '#/state/util'
|
||||
|
@ -31,15 +30,14 @@ import {
|
|||
|
||||
export {isSessionDeactivated}
|
||||
|
||||
const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE})
|
||||
configureModerationForGuest()
|
||||
|
||||
const StateContext = React.createContext<SessionStateContext>({
|
||||
accounts: [],
|
||||
currentAccount: undefined,
|
||||
hasSession: false,
|
||||
})
|
||||
|
||||
const AgentContext = React.createContext<BskyAgent | null>(null)
|
||||
|
||||
const ApiContext = React.createContext<SessionApiContext>({
|
||||
createAccount: async () => {},
|
||||
login: async () => {},
|
||||
|
@ -47,15 +45,8 @@ const ApiContext = React.createContext<SessionApiContext>({
|
|||
initSession: async () => {},
|
||||
removeAccount: () => {},
|
||||
updateCurrentAccount: () => {},
|
||||
clearCurrentAccount: () => {},
|
||||
})
|
||||
|
||||
let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT
|
||||
|
||||
function __getAgent() {
|
||||
return __globalAgent
|
||||
}
|
||||
|
||||
type AgentState = {
|
||||
readonly agent: BskyAgent
|
||||
readonly did: string | undefined
|
||||
|
@ -67,127 +58,187 @@ type State = {
|
|||
needsPersist: boolean
|
||||
}
|
||||
|
||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||
const [state, setState] = React.useState<State>(() => ({
|
||||
accounts: persisted.get('session').accounts,
|
||||
currentAgentState: {
|
||||
agent: PUBLIC_BSKY_AGENT,
|
||||
did: undefined, // assume logged out to start
|
||||
},
|
||||
needsPersist: false,
|
||||
}))
|
||||
type Action =
|
||||
| {
|
||||
type: 'received-agent-event'
|
||||
agent: BskyAgent
|
||||
accountDid: string
|
||||
refreshedAccount: SessionAccount | undefined
|
||||
sessionEvent: AtpSessionEvent
|
||||
}
|
||||
| {
|
||||
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(() => {
|
||||
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,
|
||||
}))
|
||||
}, [setState])
|
||||
function createPublicAgentState() {
|
||||
configureModerationForGuest() // Side effect but only relevant for tests
|
||||
return {
|
||||
agent: new BskyAgent({service: PUBLIC_BSKY_SERVICE}),
|
||||
did: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function getInitialState(): State {
|
||||
return {
|
||||
accounts: persisted.get('session').accounts,
|
||||
currentAgentState: createPublicAgentState(),
|
||||
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(
|
||||
(
|
||||
agent: BskyAgent,
|
||||
account: SessionAccount,
|
||||
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`)
|
||||
(agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => {
|
||||
const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away.
|
||||
if (sessionEvent === 'expired' || sessionEvent === 'create-failed') {
|
||||
emitSessionDropped()
|
||||
__globalAgent = PUBLIC_BSKY_AGENT
|
||||
configureModerationForGuest()
|
||||
setState(s => ({
|
||||
accounts: s.accounts,
|
||||
currentAgentState: {
|
||||
agent: PUBLIC_BSKY_AGENT,
|
||||
did: undefined,
|
||||
},
|
||||
needsPersist: true,
|
||||
}))
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
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,
|
||||
}
|
||||
dispatch({
|
||||
type: 'received-agent-event',
|
||||
agent,
|
||||
refreshedAccount,
|
||||
accountDid,
|
||||
sessionEvent,
|
||||
})
|
||||
},
|
||||
[],
|
||||
|
@ -199,11 +250,11 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
email,
|
||||
password,
|
||||
handle,
|
||||
birthDate,
|
||||
inviteCode,
|
||||
verificationPhone,
|
||||
verificationCode,
|
||||
}) => {
|
||||
logger.info(`session: creating account`)
|
||||
track('Try Create Account')
|
||||
logEvent('account:create:begin', {})
|
||||
const {agent, account, fetchingGates} = await createAgentAndCreateAccount(
|
||||
|
@ -212,30 +263,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
email,
|
||||
password,
|
||||
handle,
|
||||
birthDate,
|
||||
inviteCode,
|
||||
verificationPhone,
|
||||
verificationCode,
|
||||
},
|
||||
)
|
||||
|
||||
agent.setPersistSessionHandler((event, session) => {
|
||||
onAgentSessionChange(agent, account, event, session)
|
||||
agent.setPersistSessionHandler(event => {
|
||||
onAgentSessionChange(agent, account.did, event)
|
||||
})
|
||||
|
||||
__globalAgent = agent
|
||||
await fetchingGates
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
|
||||
currentAgentState: {
|
||||
did: account.did,
|
||||
agent: agent,
|
||||
},
|
||||
needsPersist: true,
|
||||
}
|
||||
dispatch({
|
||||
type: 'switched-to-account',
|
||||
newAgent: agent,
|
||||
newAccount: account,
|
||||
})
|
||||
|
||||
logger.debug(`session: created account`, {}, logger.DebugContext.session)
|
||||
track('Create Account')
|
||||
logEvent('account:create:success', {})
|
||||
},
|
||||
|
@ -244,35 +286,21 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
|
||||
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,
|
||||
password,
|
||||
authFactorToken,
|
||||
})
|
||||
|
||||
agent.setPersistSessionHandler((event, session) => {
|
||||
onAgentSessionChange(agent, account, event, session)
|
||||
agent.setPersistSessionHandler(event => {
|
||||
onAgentSessionChange(agent, account.did, event)
|
||||
})
|
||||
|
||||
__globalAgent = agent
|
||||
// @ts-ignore
|
||||
if (IS_DEV && isWeb) window.agent = agent
|
||||
await fetchingGates
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: [account, ...s.accounts.filter(a => a.did !== account.did)],
|
||||
currentAgentState: {
|
||||
did: account.did,
|
||||
agent: agent,
|
||||
},
|
||||
needsPersist: true,
|
||||
}
|
||||
dispatch({
|
||||
type: 'switched-to-account',
|
||||
newAgent: agent,
|
||||
newAccount: account,
|
||||
})
|
||||
|
||||
logger.debug(`session: logged in`, {}, logger.DebugContext.session)
|
||||
|
||||
track('Sign In', {resumedSession: false})
|
||||
logEvent('account:loggedIn', {logContext, withPassword: true})
|
||||
},
|
||||
|
@ -281,52 +309,27 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
|
||||
const logout = React.useCallback<SessionApiContext['logout']>(
|
||||
async logContext => {
|
||||
logger.debug(`session: logout`)
|
||||
logger.warn(`session: clear current account`)
|
||||
__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,
|
||||
}
|
||||
dispatch({
|
||||
type: 'logged-out',
|
||||
})
|
||||
logEvent('account:loggedOut', {logContext})
|
||||
},
|
||||
[setState],
|
||||
[],
|
||||
)
|
||||
|
||||
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})
|
||||
|
||||
// restore the correct PDS URL if available
|
||||
if (account.pdsUrl) {
|
||||
agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl)
|
||||
}
|
||||
|
||||
agent.setPersistSessionHandler((event, session) => {
|
||||
onAgentSessionChange(agent, account, event, session)
|
||||
agent.setPersistSessionHandler(event => {
|
||||
onAgentSessionChange(agent, account.did, event)
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
if (IS_DEV && isWeb) window.agent = agent
|
||||
await configureModerationForAccount(agent, account)
|
||||
|
||||
const accountOrSessionDeactivated =
|
||||
isSessionDeactivated(account.accessJwt) || account.deactivated
|
||||
|
||||
const prevSession = {
|
||||
accessJwt: account.accessJwt ?? '',
|
||||
refreshJwt: account.refreshJwt ?? '',
|
||||
|
@ -335,59 +338,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
}
|
||||
|
||||
if (isSessionExpired(account)) {
|
||||
logger.debug(`session: attempting to resume using previous session`)
|
||||
|
||||
const freshAccount = await resumeSessionWithFreshAccount()
|
||||
__globalAgent = agent
|
||||
await fetchingGates
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: [
|
||||
freshAccount,
|
||||
...s.accounts.filter(a => a.did !== freshAccount.did),
|
||||
],
|
||||
currentAgentState: {
|
||||
did: freshAccount.did,
|
||||
agent: agent,
|
||||
},
|
||||
needsPersist: true,
|
||||
}
|
||||
dispatch({
|
||||
type: 'switched-to-account',
|
||||
newAgent: agent,
|
||||
newAccount: freshAccount,
|
||||
})
|
||||
} else {
|
||||
logger.debug(`session: attempting to reuse previous session`)
|
||||
|
||||
agent.session = prevSession
|
||||
|
||||
__globalAgent = agent
|
||||
await fetchingGates
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: [
|
||||
account,
|
||||
...s.accounts.filter(a => a.did !== account.did),
|
||||
],
|
||||
currentAgentState: {
|
||||
did: account.did,
|
||||
agent: agent,
|
||||
},
|
||||
needsPersist: true,
|
||||
}
|
||||
dispatch({
|
||||
type: 'switched-to-account',
|
||||
newAgent: agent,
|
||||
newAccount: account,
|
||||
})
|
||||
|
||||
if (accountOrSessionDeactivated) {
|
||||
if (isSessionDeactivated(account.accessJwt) || account.deactivated) {
|
||||
// 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()
|
||||
}
|
||||
|
||||
async function resumeSessionWithFreshAccount(): Promise<SessionAccount> {
|
||||
logger.debug(`session: resumeSessionWithFreshAccount`)
|
||||
|
||||
await networkRetry(1, () => agent.resumeSession(prevSession))
|
||||
const sessionAccount = agentToSessionAccount(agent)
|
||||
/*
|
||||
|
@ -405,50 +380,22 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
|
||||
const removeAccount = React.useCallback<SessionApiContext['removeAccount']>(
|
||||
account => {
|
||||
setState(s => {
|
||||
return {
|
||||
accounts: s.accounts.filter(a => a.did !== account.did),
|
||||
currentAgentState: s.currentAgentState,
|
||||
needsPersist: true,
|
||||
}
|
||||
dispatch({
|
||||
type: 'removed-account',
|
||||
accountDid: account.did,
|
||||
})
|
||||
},
|
||||
[setState],
|
||||
[],
|
||||
)
|
||||
|
||||
const updateCurrentAccount = React.useCallback<
|
||||
SessionApiContext['updateCurrentAccount']
|
||||
>(
|
||||
account => {
|
||||
setState(s => {
|
||||
const currentAccount = s.accounts.find(
|
||||
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],
|
||||
)
|
||||
>(account => {
|
||||
dispatch({
|
||||
type: 'updated-current-account',
|
||||
updatedFields: account,
|
||||
})
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.needsPersist) {
|
||||
|
@ -464,70 +411,25 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
|
||||
React.useEffect(() => {
|
||||
return persisted.onUpdate(() => {
|
||||
const persistedSession = persisted.get('session')
|
||||
|
||||
logger.debug(`session: persisted onUpdate`, {})
|
||||
setState(s => ({
|
||||
accounts: persistedSession.accounts,
|
||||
currentAgentState: s.currentAgentState,
|
||||
needsPersist: false, // Synced from another tab. Don't persist to avoid cycles.
|
||||
}))
|
||||
|
||||
const selectedAccount = persistedSession.accounts.find(
|
||||
a => a.did === persistedSession.currentAccount?.did,
|
||||
const synced = persisted.get('session')
|
||||
dispatch({
|
||||
type: 'synced-accounts',
|
||||
syncedAccounts: synced.accounts,
|
||||
syncedCurrentDid: synced.currentAccount?.did,
|
||||
})
|
||||
const syncedAccount = synced.accounts.find(
|
||||
a => a.did === synced.currentAccount?.did,
|
||||
)
|
||||
|
||||
if (selectedAccount && selectedAccount.refreshJwt) {
|
||||
if (selectedAccount.did !== state.currentAgentState.did) {
|
||||
logger.debug(`session: persisted onUpdate, switching accounts`, {
|
||||
from: {
|
||||
did: state.currentAgentState.did,
|
||||
},
|
||||
to: {
|
||||
did: selectedAccount.did,
|
||||
},
|
||||
})
|
||||
|
||||
initSession(selectedAccount)
|
||||
if (syncedAccount && syncedAccount.refreshJwt) {
|
||||
if (syncedAccount.did !== state.currentAgentState.did) {
|
||||
initSession(syncedAccount)
|
||||
} 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
|
||||
__globalAgent.session = selectedAccount
|
||||
// TODO: This needs a setState.
|
||||
state.currentAgentState.agent.session = syncedAccount
|
||||
}
|
||||
} 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(
|
||||
() => ({
|
||||
|
@ -548,7 +450,6 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
initSession,
|
||||
removeAccount,
|
||||
updateCurrentAccount,
|
||||
clearCurrentAccount,
|
||||
}),
|
||||
[
|
||||
createAccount,
|
||||
|
@ -557,14 +458,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
initSession,
|
||||
removeAccount,
|
||||
updateCurrentAccount,
|
||||
clearCurrentAccount,
|
||||
],
|
||||
)
|
||||
|
||||
// @ts-ignore
|
||||
if (IS_DEV && isWeb) window.agent = state.currentAgentState.agent
|
||||
|
||||
return (
|
||||
<StateContext.Provider value={stateContext}>
|
||||
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
|
||||
</StateContext.Provider>
|
||||
<AgentContext.Provider value={state.currentAgentState.agent}>
|
||||
<StateContext.Provider value={stateContext}>
|
||||
<ApiContext.Provider value={api}>{children}</ApiContext.Provider>
|
||||
</StateContext.Provider>
|
||||
</AgentContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -594,6 +499,17 @@ export function useRequireAuth() {
|
|||
)
|
||||
}
|
||||
|
||||
export function useAgent() {
|
||||
return React.useMemo(() => ({getAgent: __getAgent}), [])
|
||||
export function useAgent(): {getAgent: () => BskyAgent} {
|
||||
const agent = React.useContext(AgentContext)
|
||||
if (!agent) {
|
||||
throw Error('useAgent() must be below <SessionProvider>.')
|
||||
}
|
||||
return React.useMemo(
|
||||
() => ({
|
||||
getAgent() {
|
||||
return agent
|
||||
},
|
||||
}),
|
||||
[agent],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ export type SessionApiContext = {
|
|||
email: string
|
||||
password: string
|
||||
handle: string
|
||||
birthDate: Date
|
||||
inviteCode?: string
|
||||
verificationPhone?: string
|
||||
verificationCode?: string
|
||||
|
@ -35,14 +36,6 @@ export type SessionApiContext = {
|
|||
logout: (
|
||||
logContext: LogEvents['account:loggedOut']['logContext'],
|
||||
) => 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>
|
||||
removeAccount: (account: SessionAccount) => void
|
||||
updateCurrentAccount: (
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api'
|
||||
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 {hasProp} from '#/lib/type-guards'
|
||||
import {logger} from '#/logger'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {DEFAULT_PROD_FEEDS} from '#/state/queries/preferences'
|
||||
import {readLabelers} from '../agent-config'
|
||||
import {SessionAccount, SessionApiContext} from '../types'
|
||||
|
||||
|
@ -132,6 +133,7 @@ export async function createAgentAndCreateAccount({
|
|||
email,
|
||||
password,
|
||||
handle,
|
||||
birthDate,
|
||||
inviteCode,
|
||||
verificationPhone,
|
||||
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)
|
||||
|
||||
return {
|
||||
|
|
|
@ -31,7 +31,7 @@ export function Component({}: {}) {
|
|||
const theme = useTheme()
|
||||
const {currentAccount} = useSession()
|
||||
const {getAgent} = useAgent()
|
||||
const {clearCurrentAccount, removeAccount} = useSessionApi()
|
||||
const {removeAccount} = useSessionApi()
|
||||
const {_} = useLingui()
|
||||
const {closeModal} = useModalControls()
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
|
@ -69,7 +69,6 @@ export function Component({}: {}) {
|
|||
Toast.show(_(msg`Your account has been deleted`))
|
||||
resetToTab('HomeTab')
|
||||
removeAccount(currentAccount)
|
||||
clearCurrentAccount()
|
||||
closeModal()
|
||||
} catch (e: any) {
|
||||
setError(cleanError(e))
|
||||
|
|
Loading…
Reference in New Issue