From 5b38f1744e98e80093a1ac103bbf9bfc28d3b50c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 27 Jan 2026 15:05:00 +0200 Subject: [PATCH] clear on logout, bust on app version --- src/ageAssurance/data.tsx | 2 +- src/lib/persisted-query-storage.ts | 11 ++++- src/lib/react-query.tsx | 13 +++--- src/state/session/index.tsx | 71 ++++++++++++++++++------------ 4 files changed, 61 insertions(+), 36 deletions(-) diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index f2d934db17..d2abfb5f88 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -45,7 +45,7 @@ const qc = new QueryClient({ }, }) const persister = createAsyncStoragePersister({ - storage: createPersistedQueryStorage('age_assurance_cache'), + storage: createPersistedQueryStorage('age-assurance'), key: 'age-assurance-query-client', }) const [, cacheHydrationPromise] = persistQueryClient({ diff --git a/src/lib/persisted-query-storage.ts b/src/lib/persisted-query-storage.ts index 4177d11454..48818dd3fe 100644 --- a/src/lib/persisted-query-storage.ts +++ b/src/lib/persisted-query-storage.ts @@ -9,6 +9,10 @@ export interface PersistedQueryStorage { removeItem: (key: string) => Promise } +function createId(id: string) { + return `react-query-cache-${id}` +} + /** * Creates an MMKV-based storage adapter for persisting react-query cache on native platforms. * Each storage instance uses a separate MMKV store identified by the provided id. @@ -17,7 +21,7 @@ export interface PersistedQueryStorage { * @param id - Unique identifier for this storage instance (used as MMKV store id) */ export function createPersistedQueryStorage(id: string): PersistedQueryStorage { - const store = createArchiveDB({id}) + const store = createArchiveDB({id: createId(id)}) return { getItem: async (key: string): Promise => { return (await store.get(key)) ?? null @@ -30,3 +34,8 @@ export function createPersistedQueryStorage(id: string): PersistedQueryStorage { }, } } + +export async function clearPersistedQueryStorage(id: string) { + const store = createArchiveDB({id: createId(id)}) + await store.clear() +} diff --git a/src/lib/react-query.tsx b/src/lib/react-query.tsx index 9753147ccc..ec657b6c41 100644 --- a/src/lib/react-query.tsx +++ b/src/lib/react-query.tsx @@ -3,6 +3,7 @@ import {AppState, type AppStateStatus} from 'react-native' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' import {focusManager, onlineManager, QueryClient} from '@tanstack/react-query' import { + type PersistQueryClientOptions, PersistQueryClientProvider, type PersistQueryClientProviderProps, } from '@tanstack/react-query-persist-client' @@ -10,10 +11,12 @@ import { import {createPersistedQueryStorage} from '#/lib/persisted-query-storage' import {listenNetworkConfirmed, listenNetworkLost} from '#/state/events' import {PERSISTED_QUERY_ROOT} from '#/state/queries' +import * as env from '#/env' import {IS_NATIVE, IS_WEB} from '#/env' declare global { interface Window { + // eslint-disable-next-line @typescript-eslint/consistent-type-imports __TANSTACK_QUERY_CLIENT__: import('@tanstack/query-core').QueryClient } } @@ -157,8 +160,6 @@ export function QueryProvider({ ) } -const PERSIST_VERSION = 2 - function QueryProviderInner({ children, currentDid, @@ -176,16 +177,16 @@ function QueryProviderInner({ // Do not move the query client creation outside of this component. const [queryClient, _setQueryClient] = useState(() => createQueryClient()) const [persistOptions, _setPersistOptions] = useState(() => { - const storage = createPersistedQueryStorage('react-query-cache') + const storage = createPersistedQueryStorage(currentDid ?? 'logged-out') const asyncPersister = createAsyncStoragePersister({ storage, - key: - 'queryClient-' + (currentDid ?? 'logged-out') + `-v${PERSIST_VERSION}`, + key: 'queryClient-' + (currentDid ?? 'logged-out'), }) return { persister: asyncPersister, dehydrateOptions, - } + buster: env.APP_VERSION, + } satisfies Omit }) useEffect(() => { if (IS_WEB) { diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 5c667c39c0..4a70fc4c09 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -1,4 +1,13 @@ -import React from 'react' +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, + useSyncExternalStore, +} from 'react' import {type AtpSessionEvent, type BskyAgent} from '@atproto/api' import * as persisted from '#/state/persisted' @@ -19,6 +28,8 @@ import {type Action, getInitialState, reducer, type State} from './reducer' export {isSignupQueued} from './util' import {addSessionDebugLog} from './logging' export type {SessionAccount} from '#/state/session/types' + +import {clearPersistedQueryStorage} from '#/lib/persisted-query-storage' import { type SessionApiContext, type SessionStateContext, @@ -29,21 +40,21 @@ import { clearAgeAssuranceDataForDid, } from '#/ageAssurance/data' -const StateContext = React.createContext({ +const StateContext = createContext({ accounts: [], currentAccount: undefined, hasSession: false, }) StateContext.displayName = 'SessionStateContext' -const AgentContext = React.createContext(null) +const AgentContext = createContext(null) AgentContext.displayName = 'SessionAgentContext' -const ApiContext = React.createContext({ +const ApiContext = createContext({ createAccount: async () => {}, login: async () => {}, - logoutCurrentAccount: async () => {}, - logoutEveryAccount: async () => {}, + logoutCurrentAccount: () => {}, + logoutEveryAccount: () => {}, resumeSession: async () => {}, removeAccount: () => {}, partialRefreshSession: async () => {}, @@ -94,11 +105,11 @@ class SessionStore { export function Provider({children}: React.PropsWithChildren<{}>) { const ax = useAnalyticsBase() const cancelPendingTask = useOneTaskAtATime() - const [store] = React.useState(() => new SessionStore()) - const state = React.useSyncExternalStore(store.subscribe, store.getState) + const [store] = useState(() => new SessionStore()) + const state = useSyncExternalStore(store.subscribe, store.getState) const onboardingDispatch = useOnboardingDispatch() - const onAgentSessionChange = React.useCallback( + const onAgentSessionChange = useCallback( (agent: BskyAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { @@ -115,7 +126,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [store], ) - const createAccount = React.useCallback( + const createAccount = useCallback( async (params, metrics) => { addSessionDebugLog({type: 'method:start', method: 'createAccount'}) const signal = cancelPendingTask() @@ -141,7 +152,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [ax, store, onAgentSessionChange, cancelPendingTask], ) - const login = React.useCallback( + const login = useCallback( async (params, logContext) => { addSessionDebugLog({type: 'method:start', method: 'login'}) const signal = cancelPendingTask() @@ -168,7 +179,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [ax, store, onAgentSessionChange, cancelPendingTask], ) - const logoutCurrentAccount = React.useCallback< + const logoutCurrentAccount = useCallback< SessionApiContext['logoutCurrentAccount'] >( logContext => { @@ -192,6 +203,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { addSessionDebugLog({type: 'method:end', method: 'logout'}) if (prevState.currentAgentState.did) { clearAgeAssuranceDataForDid({did: prevState.currentAgentState.did}) + void clearPersistedQueryStorage(prevState.currentAgentState.did) } // reset onboarding flow on logout onboardingDispatch({type: 'skip'}) @@ -199,7 +211,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [ax, store, cancelPendingTask, onboardingDispatch], ) - const logoutEveryAccount = React.useCallback< + const logoutEveryAccount = useCallback< SessionApiContext['logoutEveryAccount'] >( logContext => { @@ -222,13 +234,16 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) addSessionDebugLog({type: 'method:end', method: 'logout'}) clearAgeAssuranceData() + for (const account of prevState.accounts) { + void clearPersistedQueryStorage(account.did) + } // reset onboarding flow on logout onboardingDispatch({type: 'skip'}) }, - [store, cancelPendingTask, onboardingDispatch], + [store, cancelPendingTask, onboardingDispatch, ax], ) - const resumeSession = React.useCallback( + const resumeSession = useCallback( async (storedAccount, isSwitchingAccounts = false) => { addSessionDebugLog({ type: 'method:start', @@ -258,7 +273,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [store, onAgentSessionChange, cancelPendingTask, onboardingDispatch], ) - const partialRefreshSession = React.useCallback< + const partialRefreshSession = useCallback< SessionApiContext['partialRefreshSession'] >(async () => { const agent = state.currentAgentState.agent as BskyAppAgent @@ -275,7 +290,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) }, [store, state, cancelPendingTask]) - const removeAccount = React.useCallback( + const removeAccount = useCallback( account => { addSessionDebugLog({ type: 'method:start', @@ -292,7 +307,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, [store, cancelPendingTask], ) - React.useEffect(() => { + useEffect(() => { return persisted.onUpdate('session', nextSession => { const synced = nextSession addSessionDebugLog({type: 'persisted:receive', data: synced}) @@ -322,7 +337,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) }, [store, state, resumeSession]) - const stateContext = React.useMemo( + const stateContext = useMemo( () => ({ accounts: state.accounts, currentAccount: state.accounts.find( @@ -333,7 +348,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { [state], ) - const api = React.useMemo( + const api = useMemo( () => ({ createAccount, login, @@ -358,8 +373,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent const agent = state.currentAgentState.agent as BskyAppAgent - const currentAgentRef = React.useRef(agent) - React.useEffect(() => { + const currentAgentRef = useRef(agent) + useEffect(() => { if (currentAgentRef.current !== agent) { // Read the previous value and immediately advance the pointer. const prevAgent = currentAgentRef.current @@ -390,8 +405,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } function useOneTaskAtATime() { - const abortController = React.useRef(null) - const cancelPendingTask = React.useCallback(() => { + const abortController = useRef(null) + const cancelPendingTask = useCallback(() => { if (abortController.current) { abortController.current.abort() } @@ -402,11 +417,11 @@ function useOneTaskAtATime() { } export function useSession() { - return React.useContext(StateContext) + return useContext(StateContext) } export function useSessionApi() { - return React.useContext(ApiContext) + return useContext(ApiContext) } export function useRequireAuth() { @@ -414,7 +429,7 @@ export function useRequireAuth() { const closeAll = useCloseAllActiveElements() const {signinDialogControl} = useGlobalDialogsControlContext() - return React.useCallback( + return useCallback( (fn: () => void) => { if (hasSession) { fn() @@ -428,7 +443,7 @@ export function useRequireAuth() { } export function useAgent(): BskyAgent { - const agent = React.useContext(AgentContext) + const agent = useContext(AgentContext) if (!agent) { throw Error('useAgent() must be below .') }