clear on logout, bust on app version

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