Bluesky app fork with some witchin' additions 💫

[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>

authored by danabra.mov

Eric Bailey and committed by
GitHub
0c41b318 31a8356a

+260 -355
+2 -14
src/screens/Signup/state.ts
··· 8 8 import {useLingui} from '@lingui/react' 9 9 import * as EmailValidator from 'email-validator' 10 10 11 - import {DEFAULT_SERVICE, IS_PROD_SERVICE} from '#/lib/constants' 11 + import {DEFAULT_SERVICE} from '#/lib/constants' 12 12 import {cleanError} from '#/lib/strings/errors' 13 13 import {createFullHandle, validateHandle} from '#/lib/strings/handles' 14 14 import {getAge} from '#/lib/strings/time' 15 15 import {logger} from '#/logger' 16 - import { 17 - DEFAULT_PROD_FEEDS, 18 - usePreferencesSetBirthDateMutation, 19 - useSetSaveFeedsMutation, 20 - } from '#/state/queries/preferences' 21 16 import {useSessionApi} from '#/state/session' 22 17 import {useOnboardingDispatch} from '#/state/shell' 23 18 ··· 207 202 }) { 208 203 const {_} = useLingui() 209 204 const {createAccount} = useSessionApi() 210 - const {mutateAsync: setBirthDate} = usePreferencesSetBirthDateMutation() 211 - const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() 212 205 const onboardingDispatch = useOnboardingDispatch() 213 206 214 207 return useCallback( ··· 265 258 email: state.email, 266 259 handle: createFullHandle(state.handle, state.userDomain), 267 260 password: state.password, 261 + birthDate: state.dateOfBirth, 268 262 inviteCode: state.inviteCode.trim(), 269 263 verificationCode: verificationCode, 270 264 }) 271 - await setBirthDate({birthDate: state.dateOfBirth}) 272 - if (IS_PROD_SERVICE(state.serviceUrl)) { 273 - setSavedFeeds(DEFAULT_PROD_FEEDS) 274 - } 275 265 } catch (e: any) { 276 266 onboardingDispatch({type: 'skip'}) // undo starting the onboard 277 267 let errMsg = e.toString() ··· 314 304 _, 315 305 onboardingDispatch, 316 306 createAccount, 317 - setBirthDate, 318 - setSavedFeeds, 319 307 ], 320 308 ) 321 309 }
+246 -330
src/state/session/index.tsx
··· 1 1 import React from 'react' 2 - import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api' 2 + import {AtpSessionEvent, BskyAgent} from '@atproto/api' 3 3 4 4 import {track} from '#/lib/analytics/analytics' 5 5 import {networkRetry} from '#/lib/async/retry' 6 6 import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' 7 7 import {logEvent, tryFetchGates} from '#/lib/statsig/statsig' 8 - import {logger} from '#/logger' 9 8 import {isWeb} from '#/platform/detection' 10 9 import * as persisted from '#/state/persisted' 11 10 import {useCloseAllActiveElements} from '#/state/util' ··· 31 30 32 31 export {isSessionDeactivated} 33 32 34 - const PUBLIC_BSKY_AGENT = new BskyAgent({service: PUBLIC_BSKY_SERVICE}) 35 - configureModerationForGuest() 36 - 37 33 const StateContext = React.createContext<SessionStateContext>({ 38 34 accounts: [], 39 35 currentAccount: undefined, 40 36 hasSession: false, 41 37 }) 42 38 39 + const AgentContext = React.createContext<BskyAgent | null>(null) 40 + 43 41 const ApiContext = React.createContext<SessionApiContext>({ 44 42 createAccount: async () => {}, 45 43 login: async () => {}, ··· 47 45 initSession: async () => {}, 48 46 removeAccount: () => {}, 49 47 updateCurrentAccount: () => {}, 50 - clearCurrentAccount: () => {}, 51 48 }) 52 49 53 - let __globalAgent: BskyAgent = PUBLIC_BSKY_AGENT 54 - 55 - function __getAgent() { 56 - return __globalAgent 57 - } 58 - 59 50 type AgentState = { 60 51 readonly agent: BskyAgent 61 52 readonly did: string | undefined ··· 67 58 needsPersist: boolean 68 59 } 69 60 70 - export function Provider({children}: React.PropsWithChildren<{}>) { 71 - const [state, setState] = React.useState<State>(() => ({ 72 - accounts: persisted.get('session').accounts, 73 - currentAgentState: { 74 - agent: PUBLIC_BSKY_AGENT, 75 - did: undefined, // assume logged out to start 76 - }, 77 - needsPersist: false, 78 - })) 61 + type Action = 62 + | { 63 + type: 'received-agent-event' 64 + agent: BskyAgent 65 + accountDid: string 66 + refreshedAccount: SessionAccount | undefined 67 + sessionEvent: AtpSessionEvent 68 + } 69 + | { 70 + type: 'switched-to-account' 71 + newAgent: BskyAgent 72 + newAccount: SessionAccount 73 + } 74 + | { 75 + type: 'updated-current-account' 76 + updatedFields: Partial< 77 + Pick< 78 + SessionAccount, 79 + 'handle' | 'email' | 'emailConfirmed' | 'emailAuthFactor' 80 + > 81 + > 82 + } 83 + | { 84 + type: 'removed-account' 85 + accountDid: string 86 + } 87 + | { 88 + type: 'logged-out' 89 + } 90 + | { 91 + type: 'synced-accounts' 92 + syncedAccounts: SessionAccount[] 93 + syncedCurrentDid: string | undefined 94 + } 79 95 80 - const clearCurrentAccount = React.useCallback(() => { 81 - logger.warn(`session: clear current account`) 82 - __globalAgent = PUBLIC_BSKY_AGENT 83 - configureModerationForGuest() 84 - setState(s => ({ 85 - accounts: s.accounts, 86 - currentAgentState: { 87 - agent: PUBLIC_BSKY_AGENT, 88 - did: undefined, 89 - }, 90 - needsPersist: true, 91 - })) 92 - }, [setState]) 96 + function createPublicAgentState() { 97 + configureModerationForGuest() // Side effect but only relevant for tests 98 + return { 99 + agent: new BskyAgent({service: PUBLIC_BSKY_SERVICE}), 100 + did: undefined, 101 + } 102 + } 93 103 94 - const onAgentSessionChange = React.useCallback( 95 - ( 96 - agent: BskyAgent, 97 - account: SessionAccount, 98 - event: AtpSessionEvent, 99 - session: AtpSessionData | undefined, 100 - ) => { 101 - const expired = event === 'expired' || event === 'create-failed' 104 + function getInitialState(): State { 105 + return { 106 + accounts: persisted.get('session').accounts, 107 + currentAgentState: createPublicAgentState(), 108 + needsPersist: false, 109 + } 110 + } 102 111 103 - if (event === 'network-error') { 104 - logger.warn( 105 - `session: persistSessionHandler received network-error event`, 106 - ) 107 - logger.warn(`session: clear current account`) 108 - __globalAgent = PUBLIC_BSKY_AGENT 109 - configureModerationForGuest() 110 - setState(s => ({ 111 - accounts: s.accounts, 112 - currentAgentState: { 113 - agent: PUBLIC_BSKY_AGENT, 114 - did: undefined, 115 - }, 112 + function reducer(state: State, action: Action): State { 113 + switch (action.type) { 114 + case 'received-agent-event': { 115 + const {agent, accountDid, refreshedAccount, sessionEvent} = action 116 + if (agent !== state.currentAgentState.agent) { 117 + // Only consider events from the active agent. 118 + return state 119 + } 120 + if (sessionEvent === 'network-error') { 121 + // Don't change stored accounts but kick to the choose account screen. 122 + return { 123 + accounts: state.accounts, 124 + currentAgentState: createPublicAgentState(), 116 125 needsPersist: true, 117 - })) 118 - return 126 + } 119 127 } 120 - 121 - // TODO: use agentToSessionAccount for this too. 122 - const refreshedAccount: SessionAccount = { 123 - service: account.service, 124 - did: session?.did ?? account.did, 125 - handle: session?.handle ?? account.handle, 126 - email: session?.email ?? account.email, 127 - emailConfirmed: session?.emailConfirmed ?? account.emailConfirmed, 128 - emailAuthFactor: session?.emailAuthFactor ?? account.emailAuthFactor, 129 - deactivated: isSessionDeactivated(session?.accessJwt), 130 - pdsUrl: agent.pdsUrl?.toString(), 131 - 132 - /* 133 - * Tokens are undefined if the session expires, or if creation fails for 134 - * any reason e.g. tokens are invalid, network error, etc. 135 - */ 136 - refreshJwt: session?.refreshJwt, 137 - accessJwt: session?.accessJwt, 128 + const existingAccount = state.accounts.find(a => a.did === accountDid) 129 + if ( 130 + !existingAccount || 131 + JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount) 132 + ) { 133 + // Fast path without a state update. 134 + return state 138 135 } 136 + return { 137 + accounts: state.accounts.map(a => { 138 + if (a.did === accountDid) { 139 + if (refreshedAccount) { 140 + return refreshedAccount 141 + } else { 142 + return { 143 + ...a, 144 + // If we didn't receive a refreshed account, clear out the tokens. 145 + accessJwt: undefined, 146 + refreshJwt: undefined, 147 + } 148 + } 149 + } else { 150 + return a 151 + } 152 + }), 153 + currentAgentState: refreshedAccount 154 + ? state.currentAgentState 155 + : createPublicAgentState(), // Log out if expired. 156 + needsPersist: true, 157 + } 158 + } 159 + case 'switched-to-account': { 160 + const {newAccount, newAgent} = action 161 + return { 162 + accounts: [ 163 + newAccount, 164 + ...state.accounts.filter(a => a.did !== newAccount.did), 165 + ], 166 + currentAgentState: { 167 + did: newAccount.did, 168 + agent: newAgent, 169 + }, 170 + needsPersist: true, 171 + } 172 + } 173 + case 'updated-current-account': { 174 + const {updatedFields} = action 175 + return { 176 + accounts: state.accounts.map(a => { 177 + if (a.did === state.currentAgentState.did) { 178 + return { 179 + ...a, 180 + ...updatedFields, 181 + } 182 + } else { 183 + return a 184 + } 185 + }), 186 + currentAgentState: state.currentAgentState, 187 + needsPersist: true, 188 + } 189 + } 190 + case 'removed-account': { 191 + const {accountDid} = action 192 + return { 193 + accounts: state.accounts.filter(a => a.did !== accountDid), 194 + currentAgentState: 195 + state.currentAgentState.did === accountDid 196 + ? createPublicAgentState() // Log out if removing the current one. 197 + : state.currentAgentState, 198 + needsPersist: true, 199 + } 200 + } 201 + case 'logged-out': { 202 + return { 203 + accounts: state.accounts.map(a => ({ 204 + ...a, 205 + // Clear tokens for *every* account (this is a hard logout). 206 + refreshJwt: undefined, 207 + accessJwt: undefined, 208 + })), 209 + currentAgentState: createPublicAgentState(), 210 + needsPersist: true, 211 + } 212 + } 213 + case 'synced-accounts': { 214 + const {syncedAccounts, syncedCurrentDid} = action 215 + return { 216 + accounts: syncedAccounts, 217 + currentAgentState: 218 + syncedCurrentDid === state.currentAgentState.did 219 + ? state.currentAgentState 220 + : createPublicAgentState(), // Log out if different user. 221 + needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. 222 + } 223 + } 224 + } 225 + } 139 226 140 - logger.debug(`session: persistSession`, { 141 - event, 142 - deactivated: refreshedAccount.deactivated, 143 - }) 227 + export function Provider({children}: React.PropsWithChildren<{}>) { 228 + const [state, dispatch] = React.useReducer(reducer, null, getInitialState) 144 229 145 - if (expired) { 146 - logger.warn(`session: expired`) 230 + const onAgentSessionChange = React.useCallback( 231 + (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { 232 + const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. 233 + if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { 147 234 emitSessionDropped() 148 - __globalAgent = PUBLIC_BSKY_AGENT 149 - configureModerationForGuest() 150 - setState(s => ({ 151 - accounts: s.accounts, 152 - currentAgentState: { 153 - agent: PUBLIC_BSKY_AGENT, 154 - did: undefined, 155 - }, 156 - needsPersist: true, 157 - })) 158 235 } 159 - 160 - /* 161 - * If the session expired, or it was successfully created/updated, we want 162 - * to update/persist the data. 163 - * 164 - * If the session creation failed, it could be a network error, or it could 165 - * be more serious like an invalid token(s). We can't differentiate, so in 166 - * order to allow the user to get a fresh token (if they need it), we need 167 - * to persist this data and wipe their tokens, effectively logging them 168 - * out. 169 - */ 170 - setState(s => { 171 - const existingAccount = s.accounts.find( 172 - a => a.did === refreshedAccount.did, 173 - ) 174 - if ( 175 - !expired && 176 - existingAccount && 177 - refreshedAccount && 178 - JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount) 179 - ) { 180 - // Fast path without a state update. 181 - return s 182 - } 183 - return { 184 - accounts: [ 185 - refreshedAccount, 186 - ...s.accounts.filter(a => a.did !== refreshedAccount.did), 187 - ], 188 - currentAgentState: s.currentAgentState, 189 - needsPersist: true, 190 - } 236 + dispatch({ 237 + type: 'received-agent-event', 238 + agent, 239 + refreshedAccount, 240 + accountDid, 241 + sessionEvent, 191 242 }) 192 243 }, 193 244 [], ··· 199 250 email, 200 251 password, 201 252 handle, 253 + birthDate, 202 254 inviteCode, 203 255 verificationPhone, 204 256 verificationCode, 205 257 }) => { 206 - logger.info(`session: creating account`) 207 258 track('Try Create Account') 208 259 logEvent('account:create:begin', {}) 209 260 const {agent, account, fetchingGates} = await createAgentAndCreateAccount( ··· 212 263 email, 213 264 password, 214 265 handle, 266 + birthDate, 215 267 inviteCode, 216 268 verificationPhone, 217 269 verificationCode, 218 270 }, 219 271 ) 220 - 221 - agent.setPersistSessionHandler((event, session) => { 222 - onAgentSessionChange(agent, account, event, session) 272 + agent.setPersistSessionHandler(event => { 273 + onAgentSessionChange(agent, account.did, event) 223 274 }) 224 - 225 - __globalAgent = agent 226 275 await fetchingGates 227 - setState(s => { 228 - return { 229 - accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], 230 - currentAgentState: { 231 - did: account.did, 232 - agent: agent, 233 - }, 234 - needsPersist: true, 235 - } 276 + dispatch({ 277 + type: 'switched-to-account', 278 + newAgent: agent, 279 + newAccount: account, 236 280 }) 237 - 238 - logger.debug(`session: created account`, {}, logger.DebugContext.session) 239 281 track('Create Account') 240 282 logEvent('account:create:success', {}) 241 283 }, ··· 244 286 245 287 const login = React.useCallback<SessionApiContext['login']>( 246 288 async ({service, identifier, password, authFactorToken}, logContext) => { 247 - logger.debug(`session: login`, {}, logger.DebugContext.session) 248 289 const {agent, account, fetchingGates} = await createAgentAndLogin({ 249 290 service, 250 291 identifier, 251 292 password, 252 293 authFactorToken, 253 294 }) 254 - 255 - agent.setPersistSessionHandler((event, session) => { 256 - onAgentSessionChange(agent, account, event, session) 295 + agent.setPersistSessionHandler(event => { 296 + onAgentSessionChange(agent, account.did, event) 257 297 }) 258 - 259 - __globalAgent = agent 260 - // @ts-ignore 261 - if (IS_DEV && isWeb) window.agent = agent 262 298 await fetchingGates 263 - setState(s => { 264 - return { 265 - accounts: [account, ...s.accounts.filter(a => a.did !== account.did)], 266 - currentAgentState: { 267 - did: account.did, 268 - agent: agent, 269 - }, 270 - needsPersist: true, 271 - } 299 + dispatch({ 300 + type: 'switched-to-account', 301 + newAgent: agent, 302 + newAccount: account, 272 303 }) 273 - 274 - logger.debug(`session: logged in`, {}, logger.DebugContext.session) 275 - 276 304 track('Sign In', {resumedSession: false}) 277 305 logEvent('account:loggedIn', {logContext, withPassword: true}) 278 306 }, ··· 281 309 282 310 const logout = React.useCallback<SessionApiContext['logout']>( 283 311 async logContext => { 284 - logger.debug(`session: logout`) 285 - logger.warn(`session: clear current account`) 286 - __globalAgent = PUBLIC_BSKY_AGENT 287 - configureModerationForGuest() 288 - setState(s => { 289 - return { 290 - accounts: s.accounts.map(a => ({ 291 - ...a, 292 - refreshJwt: undefined, 293 - accessJwt: undefined, 294 - })), 295 - currentAgentState: { 296 - did: undefined, 297 - agent: PUBLIC_BSKY_AGENT, 298 - }, 299 - needsPersist: true, 300 - } 312 + dispatch({ 313 + type: 'logged-out', 301 314 }) 302 315 logEvent('account:loggedOut', {logContext}) 303 316 }, 304 - [setState], 317 + [], 305 318 ) 306 319 307 320 const initSession = React.useCallback<SessionApiContext['initSession']>( 308 321 async account => { 309 - logger.debug(`session: initSession`, {}, logger.DebugContext.session) 310 322 const fetchingGates = tryFetchGates(account.did, 'prefer-low-latency') 311 - 312 323 const agent = new BskyAgent({service: account.service}) 313 - 314 324 // restore the correct PDS URL if available 315 325 if (account.pdsUrl) { 316 326 agent.pdsUrl = agent.api.xrpc.uri = new URL(account.pdsUrl) 317 327 } 318 - 319 - agent.setPersistSessionHandler((event, session) => { 320 - onAgentSessionChange(agent, account, event, session) 328 + agent.setPersistSessionHandler(event => { 329 + onAgentSessionChange(agent, account.did, event) 321 330 }) 322 - 323 - // @ts-ignore 324 - if (IS_DEV && isWeb) window.agent = agent 325 331 await configureModerationForAccount(agent, account) 326 - 327 - const accountOrSessionDeactivated = 328 - isSessionDeactivated(account.accessJwt) || account.deactivated 329 332 330 333 const prevSession = { 331 334 accessJwt: account.accessJwt ?? '', ··· 335 338 } 336 339 337 340 if (isSessionExpired(account)) { 338 - logger.debug(`session: attempting to resume using previous session`) 339 - 340 341 const freshAccount = await resumeSessionWithFreshAccount() 341 - __globalAgent = agent 342 342 await fetchingGates 343 - setState(s => { 344 - return { 345 - accounts: [ 346 - freshAccount, 347 - ...s.accounts.filter(a => a.did !== freshAccount.did), 348 - ], 349 - currentAgentState: { 350 - did: freshAccount.did, 351 - agent: agent, 352 - }, 353 - needsPersist: true, 354 - } 343 + dispatch({ 344 + type: 'switched-to-account', 345 + newAgent: agent, 346 + newAccount: freshAccount, 355 347 }) 356 348 } else { 357 - logger.debug(`session: attempting to reuse previous session`) 358 - 359 349 agent.session = prevSession 360 - 361 - __globalAgent = agent 362 350 await fetchingGates 363 - setState(s => { 364 - return { 365 - accounts: [ 366 - account, 367 - ...s.accounts.filter(a => a.did !== account.did), 368 - ], 369 - currentAgentState: { 370 - did: account.did, 371 - agent: agent, 372 - }, 373 - needsPersist: true, 374 - } 351 + dispatch({ 352 + type: 'switched-to-account', 353 + newAgent: agent, 354 + newAccount: account, 375 355 }) 376 - 377 - if (accountOrSessionDeactivated) { 356 + if (isSessionDeactivated(account.accessJwt) || account.deactivated) { 378 357 // don't attempt to resume 379 358 // use will be taken to the deactivated screen 380 - logger.debug(`session: reusing session for deactivated account`) 381 359 return 382 360 } 383 - 384 361 // Intentionally not awaited to unblock the UI: 385 362 resumeSessionWithFreshAccount() 386 363 } 387 364 388 365 async function resumeSessionWithFreshAccount(): Promise<SessionAccount> { 389 - logger.debug(`session: resumeSessionWithFreshAccount`) 390 - 391 366 await networkRetry(1, () => agent.resumeSession(prevSession)) 392 367 const sessionAccount = agentToSessionAccount(agent) 393 368 /* ··· 405 380 406 381 const removeAccount = React.useCallback<SessionApiContext['removeAccount']>( 407 382 account => { 408 - setState(s => { 409 - return { 410 - accounts: s.accounts.filter(a => a.did !== account.did), 411 - currentAgentState: s.currentAgentState, 412 - needsPersist: true, 413 - } 383 + dispatch({ 384 + type: 'removed-account', 385 + accountDid: account.did, 414 386 }) 415 387 }, 416 - [setState], 388 + [], 417 389 ) 418 390 419 391 const updateCurrentAccount = React.useCallback< 420 392 SessionApiContext['updateCurrentAccount'] 421 - >( 422 - account => { 423 - setState(s => { 424 - const currentAccount = s.accounts.find( 425 - a => a.did === s.currentAgentState.did, 426 - ) 427 - // ignore, should never happen 428 - if (!currentAccount) return s 429 - 430 - const updatedAccount = { 431 - ...currentAccount, 432 - handle: account.handle ?? currentAccount.handle, 433 - email: account.email ?? currentAccount.email, 434 - emailConfirmed: 435 - account.emailConfirmed ?? currentAccount.emailConfirmed, 436 - emailAuthFactor: 437 - account.emailAuthFactor ?? currentAccount.emailAuthFactor, 438 - } 439 - 440 - return { 441 - accounts: [ 442 - updatedAccount, 443 - ...s.accounts.filter(a => a.did !== currentAccount.did), 444 - ], 445 - currentAgentState: s.currentAgentState, 446 - needsPersist: true, 447 - } 448 - }) 449 - }, 450 - [setState], 451 - ) 393 + >(account => { 394 + dispatch({ 395 + type: 'updated-current-account', 396 + updatedFields: account, 397 + }) 398 + }, []) 452 399 453 400 React.useEffect(() => { 454 401 if (state.needsPersist) { ··· 464 411 465 412 React.useEffect(() => { 466 413 return persisted.onUpdate(() => { 467 - const persistedSession = persisted.get('session') 468 - 469 - logger.debug(`session: persisted onUpdate`, {}) 470 - setState(s => ({ 471 - accounts: persistedSession.accounts, 472 - currentAgentState: s.currentAgentState, 473 - needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. 474 - })) 475 - 476 - const selectedAccount = persistedSession.accounts.find( 477 - a => a.did === persistedSession.currentAccount?.did, 414 + const synced = persisted.get('session') 415 + dispatch({ 416 + type: 'synced-accounts', 417 + syncedAccounts: synced.accounts, 418 + syncedCurrentDid: synced.currentAccount?.did, 419 + }) 420 + const syncedAccount = synced.accounts.find( 421 + a => a.did === synced.currentAccount?.did, 478 422 ) 479 - 480 - if (selectedAccount && selectedAccount.refreshJwt) { 481 - if (selectedAccount.did !== state.currentAgentState.did) { 482 - logger.debug(`session: persisted onUpdate, switching accounts`, { 483 - from: { 484 - did: state.currentAgentState.did, 485 - }, 486 - to: { 487 - did: selectedAccount.did, 488 - }, 489 - }) 490 - 491 - initSession(selectedAccount) 423 + if (syncedAccount && syncedAccount.refreshJwt) { 424 + if (syncedAccount.did !== state.currentAgentState.did) { 425 + initSession(syncedAccount) 492 426 } else { 493 - logger.debug(`session: persisted onUpdate, updating session`, {}) 494 - 495 - /* 496 - * Use updated session in this tab's agent. Do not call 497 - * upsertAccount, since that will only persist the session that's 498 - * already persisted, and we'll get a loop between tabs. 499 - */ 500 427 // @ts-ignore we checked for `refreshJwt` above 501 - __globalAgent.session = selectedAccount 502 - // TODO: This needs a setState. 428 + state.currentAgentState.agent.session = syncedAccount 503 429 } 504 - } else if (!selectedAccount && state.currentAgentState.did) { 505 - logger.debug( 506 - `session: persisted onUpdate, logging out`, 507 - {}, 508 - logger.DebugContext.session, 509 - ) 510 - 511 - /* 512 - * No need to do a hard logout here. If we reach this, tokens for this 513 - * account have already been cleared either by an `expired` event 514 - * handled by `persistSession` (which nukes this accounts tokens only), 515 - * or by a `logout` call which nukes all accounts tokens) 516 - */ 517 - logger.warn(`session: clear current account`) 518 - __globalAgent = PUBLIC_BSKY_AGENT 519 - configureModerationForGuest() 520 - setState(s => ({ 521 - accounts: s.accounts, 522 - currentAgentState: { 523 - did: undefined, 524 - agent: PUBLIC_BSKY_AGENT, 525 - }, 526 - needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. 527 - })) 528 430 } 529 431 }) 530 - }, [state, setState, initSession]) 432 + }, [state, initSession]) 531 433 532 434 const stateContext = React.useMemo( 533 435 () => ({ ··· 548 450 initSession, 549 451 removeAccount, 550 452 updateCurrentAccount, 551 - clearCurrentAccount, 552 453 }), 553 454 [ 554 455 createAccount, ··· 557 458 initSession, 558 459 removeAccount, 559 460 updateCurrentAccount, 560 - clearCurrentAccount, 561 461 ], 562 462 ) 563 463 464 + // @ts-ignore 465 + if (IS_DEV && isWeb) window.agent = state.currentAgentState.agent 466 + 564 467 return ( 565 - <StateContext.Provider value={stateContext}> 566 - <ApiContext.Provider value={api}>{children}</ApiContext.Provider> 567 - </StateContext.Provider> 468 + <AgentContext.Provider value={state.currentAgentState.agent}> 469 + <StateContext.Provider value={stateContext}> 470 + <ApiContext.Provider value={api}>{children}</ApiContext.Provider> 471 + </StateContext.Provider> 472 + </AgentContext.Provider> 568 473 ) 569 474 } 570 475 ··· 594 499 ) 595 500 } 596 501 597 - export function useAgent() { 598 - return React.useMemo(() => ({getAgent: __getAgent}), []) 502 + export function useAgent(): {getAgent: () => BskyAgent} { 503 + const agent = React.useContext(AgentContext) 504 + if (!agent) { 505 + throw Error('useAgent() must be below <SessionProvider>.') 506 + } 507 + return React.useMemo( 508 + () => ({ 509 + getAgent() { 510 + return agent 511 + }, 512 + }), 513 + [agent], 514 + ) 599 515 }
+1 -8
src/state/session/types.ts
··· 14 14 email: string 15 15 password: string 16 16 handle: string 17 + birthDate: Date 17 18 inviteCode?: string 18 19 verificationPhone?: string 19 20 verificationCode?: string ··· 35 36 logout: ( 36 37 logContext: LogEvents['account:loggedOut']['logContext'], 37 38 ) => Promise<void> 38 - /** 39 - * A partial logout. Clears the `currentAccount` from session, but DOES NOT 40 - * clear access tokens from accounts, allowing the user to return to their 41 - * other accounts without logging in. 42 - * 43 - * Used when adding a new account, deleting an account. 44 - */ 45 - clearCurrentAccount: () => void 46 39 initSession: (account: SessionAccount) => Promise<void> 47 40 removeAccount: (account: SessionAccount) => void 48 41 updateCurrentAccount: (
+10 -1
src/state/session/util/index.ts
··· 1 1 import {BSKY_LABELER_DID, BskyAgent} from '@atproto/api' 2 2 import {jwtDecode} from 'jwt-decode' 3 3 4 - import {IS_TEST_USER} from '#/lib/constants' 4 + import {IS_PROD_SERVICE, IS_TEST_USER} from '#/lib/constants' 5 5 import {tryFetchGates} from '#/lib/statsig/statsig' 6 6 import {hasProp} from '#/lib/type-guards' 7 7 import {logger} from '#/logger' 8 8 import * as persisted from '#/state/persisted' 9 + import {DEFAULT_PROD_FEEDS} from '#/state/queries/preferences' 9 10 import {readLabelers} from '../agent-config' 10 11 import {SessionAccount, SessionApiContext} from '../types' 11 12 ··· 132 133 email, 133 134 password, 134 135 handle, 136 + birthDate, 135 137 inviteCode, 136 138 verificationPhone, 137 139 verificationCode, ··· 165 167 createdAt: new Date().toISOString(), 166 168 } 167 169 }) 170 + } 171 + 172 + // Not awaited so that we can still get into onboarding. 173 + // This is OK because we won't let you toggle adult stuff until you set the date. 174 + agent.setPersonalDetails({birthDate: birthDate.toISOString()}) 175 + if (IS_PROD_SERVICE(service)) { 176 + agent.setSavedFeeds(DEFAULT_PROD_FEEDS.saved, DEFAULT_PROD_FEEDS.pinned) 168 177 } 169 178 170 179 await configureModerationForAccount(agent, account)
+1 -2
src/view/com/modals/DeleteAccount.tsx
··· 31 31 const theme = useTheme() 32 32 const {currentAccount} = useSession() 33 33 const {getAgent} = useAgent() 34 - const {clearCurrentAccount, removeAccount} = useSessionApi() 34 + const {removeAccount} = useSessionApi() 35 35 const {_} = useLingui() 36 36 const {closeModal} = useModalControls() 37 37 const {isMobile} = useWebMediaQueries() ··· 69 69 Toast.show(_(msg`Your account has been deleted`)) 70 70 resetToTab('HomeTab') 71 71 removeAccount(currentAccount) 72 - clearCurrentAccount() 73 72 closeModal() 74 73 } catch (e: any) { 75 74 setError(cleanError(e))