[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 {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,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: (
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in New Issue