clear on logout, bust on app version
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
@@ -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>.')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user