From ef306bd4ce8afeccf365d90b7952fb9a26bcffb0 Mon Sep 17 00:00:00 2001 From: Dustin Do Date: Sat, 3 Aug 2024 11:33:04 +0700 Subject: [PATCH] feat(mobile): clear all cache when user change (#198) --- apps/mobile/app/(app)/(tabs)/settings.tsx | 5 -- apps/mobile/app/_layout.tsx | 47 ++++++++------- apps/mobile/lib/utils.ts | 14 +++++ apps/mobile/stores/budget/hooks.tsx | 2 +- apps/mobile/stores/budget/store.ts | 3 + apps/mobile/stores/category/hooks.tsx | 2 +- apps/mobile/stores/category/store.ts | 3 + apps/mobile/stores/store-provider.tsx | 64 +++++++++++++++++++++ apps/mobile/stores/transaction/hooks.tsx | 7 ++- apps/mobile/stores/transaction/store.ts | 3 + apps/mobile/stores/use-reset-all-stores.tsx | 18 ++++++ 11 files changed, 137 insertions(+), 31 deletions(-) create mode 100644 apps/mobile/stores/store-provider.tsx create mode 100644 apps/mobile/stores/use-reset-all-stores.tsx diff --git a/apps/mobile/app/(app)/(tabs)/settings.tsx b/apps/mobile/app/(app)/(tabs)/settings.tsx index 59c497a4..90849869 100644 --- a/apps/mobile/app/(app)/(tabs)/settings.tsx +++ b/apps/mobile/app/(app)/(tabs)/settings.tsx @@ -13,8 +13,6 @@ import { useLocale } from '@/locales/provider' import { useAuth } from '@clerk/clerk-expo' import { t } from '@lingui/macro' import { useLingui } from '@lingui/react' -import AsyncStorage from '@react-native-async-storage/async-storage' -import { useQueryClient } from '@tanstack/react-query' import { LinearGradient } from 'expo-linear-gradient' import { Link } from 'expo-router' import { @@ -40,7 +38,6 @@ export default function SettingsScreen() { const { i18n } = useLingui() const { language } = useLocale() const { colorScheme } = useColorScheme() - const queryClient = useQueryClient() return ( @@ -189,8 +186,6 @@ export default function SettingsScreen() { text: t(i18n)`Sign out`, style: 'destructive', onPress: async () => { - await AsyncStorage.clear() - queryClient.clear() await signOut() }, }, diff --git a/apps/mobile/app/_layout.tsx b/apps/mobile/app/_layout.tsx index 029c1b4b..49b10e6d 100644 --- a/apps/mobile/app/_layout.tsx +++ b/apps/mobile/app/_layout.tsx @@ -26,6 +26,7 @@ import { useWarmUpBrowser } from '@/hooks/use-warm-up-browser' import { useColorScheme } from '@/hooks/useColorScheme' import { queryClient } from '@/lib/client' import { LocaleProvider } from '@/locales/provider' +import { StoreProvider } from '@/stores/store-provider' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' import AsyncStorage from '@react-native-async-storage/async-storage' import NetInfo from '@react-native-community/netinfo' @@ -147,28 +148,30 @@ function RootLayout() { client={queryClient} persistOptions={{ persister: asyncStoragePersister }} > - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/lib/utils.ts b/apps/mobile/lib/utils.ts index e94d5e14..ec2a8357 100644 --- a/apps/mobile/lib/utils.ts +++ b/apps/mobile/lib/utils.ts @@ -1,4 +1,6 @@ +import AsyncStorage from '@react-native-async-storage/async-storage' import { type ClassValue, clsx } from 'clsx' +import { Platform } from 'react-native' import { twMerge } from 'tailwind-merge' export function cn(...inputs: ClassValue[]) { @@ -8,3 +10,15 @@ export function cn(...inputs: ClassValue[]) { export async function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } + +export async function clearAsyncStorage() { + const asyncStorageKeys = await AsyncStorage.getAllKeys() + if (asyncStorageKeys.length > 0) { + if (Platform.OS === 'android') { + await AsyncStorage.clear() + } + if (Platform.OS === 'ios') { + await AsyncStorage.multiRemove(asyncStorageKeys) + } + } +} diff --git a/apps/mobile/stores/budget/hooks.tsx b/apps/mobile/stores/budget/hooks.tsx index fddbe304..a346589c 100644 --- a/apps/mobile/stores/budget/hooks.tsx +++ b/apps/mobile/stores/budget/hooks.tsx @@ -15,7 +15,7 @@ export const useBudgetList = () => { const query = useQuery({ ...budgetQueries.all({ setBudgetsState }), - initialData: budgets, + initialData: budgets.length > 0 ? budgets : undefined, }) const { diff --git a/apps/mobile/stores/budget/store.ts b/apps/mobile/stores/budget/store.ts index 526c79f0..09661ce3 100644 --- a/apps/mobile/stores/budget/store.ts +++ b/apps/mobile/stores/budget/store.ts @@ -9,6 +9,7 @@ export type BudgetItem = Budget & { interface BudgetStore { budgets: BudgetItem[] + _reset: () => void setBudgets: (budgets: BudgetItem[]) => void updateBudget: (budget: BudgetItem) => void } @@ -17,6 +18,8 @@ export const useBudgetStore = create()( persist( (set) => ({ budgets: [], + // biome-ignore lint/style/useNamingConvention: + _reset: () => set({ budgets: [] }), setBudgets: (budgets: BudgetItem[]) => set({ budgets }), updateBudget: (budget: BudgetItem) => set((state) => { diff --git a/apps/mobile/stores/category/hooks.tsx b/apps/mobile/stores/category/hooks.tsx index 960eff88..dabe279d 100644 --- a/apps/mobile/stores/category/hooks.tsx +++ b/apps/mobile/stores/category/hooks.tsx @@ -19,7 +19,7 @@ export const useCategoryList = () => { const query = useQuery({ ...categoryQueries.all({ setCategoriesState }), - initialData: categories, + initialData: categories?.length > 0 ? categories : undefined, }) const { categoriesDict, incomeCategories, expenseCategories } = diff --git a/apps/mobile/stores/category/store.ts b/apps/mobile/stores/category/store.ts index f946a69b..785b960a 100644 --- a/apps/mobile/stores/category/store.ts +++ b/apps/mobile/stores/category/store.ts @@ -6,6 +6,7 @@ import { createJSONStorage, persist } from 'zustand/middleware' interface CategoryStore { categories: Category[] + _reset: () => void setCategories: (categories: Category[]) => void updateCategory: (category: Category) => void } @@ -18,6 +19,8 @@ export const useCategoryStore = create()( persist( (set) => ({ categories: [], + // biome-ignore lint/style/useNamingConvention: + _reset: () => set({ categories: [] }), setCategories: (categories: Category[]) => set({ categories: normalizeCategories(categories) }), updateCategory: (category: Category) => diff --git a/apps/mobile/stores/store-provider.tsx b/apps/mobile/stores/store-provider.tsx new file mode 100644 index 00000000..c21764ed --- /dev/null +++ b/apps/mobile/stores/store-provider.tsx @@ -0,0 +1,64 @@ +import { clearAsyncStorage } from '@/lib/utils' +import { useAuth } from '@clerk/clerk-expo' +import { useAsyncStorage } from '@react-native-async-storage/async-storage' +import { useQueryClient } from '@tanstack/react-query' +import { + type FC, + type ReactNode, + useCallback, + useEffect, + useState, +} from 'react' +import { useResetAllStores } from './use-reset-all-stores' + +export type StoreProviderProps = { + children: ReactNode +} + +export const StoreProvider: FC = ({ children }) => { + const [isReady, setIsReady] = useState(false) + const { userId } = useAuth() + const queryClient = useQueryClient() + const { getItem, setItem, removeItem } = useAsyncStorage('user-id') + const resetAllStores = useResetAllStores() + + const handleUserChange = useCallback(async () => { + const storedUserId = await getItem() + + if (storedUserId === userId) { + return + } + + // biome-ignore lint/suspicious/noConsoleLog: + console.log('User changed, clearing storage', userId) + + await clearAsyncStorage() + queryClient.clear() + queryClient.invalidateQueries() + resetAllStores() + + // biome-ignore lint/suspicious/noConsoleLog: + console.log('Storage cleared') + + if (userId) { + await setItem(userId) + } else { + await removeItem() + } + }, [getItem, queryClient, userId, removeItem, setItem, resetAllStores]) + + // Clear the async storage & queryClient when the user changes + useEffect(() => { + handleUserChange().catch((error) => { + console.error('Failed to clear storage', error) + }) + + setIsReady(true) + }, [handleUserChange]) + + if (!isReady) { + return null + } + + return children +} diff --git a/apps/mobile/stores/transaction/hooks.tsx b/apps/mobile/stores/transaction/hooks.tsx index 09a64b90..d7437192 100644 --- a/apps/mobile/stores/transaction/hooks.tsx +++ b/apps/mobile/stores/transaction/hooks.tsx @@ -35,7 +35,10 @@ export function useTransactionList({ filters: { from, to }, updateTransactionsByRangeInStore: updateTransactionsByRange, }), - initialData: transactionsInRangeFromStore, + initialData: + transactionsInRangeFromStore.length > 0 + ? transactionsInRangeFromStore + : undefined, }) const { transactions, transactionDict, totalExpense, totalIncome } = @@ -98,7 +101,7 @@ export function useTransaction(transactionId: string) { updateTransactionInStore: updateTransaction, removeTransactionInStore: removeTransaction, }), - initialData: transaction, + initialData: transaction || undefined, }) return { ...query, transaction } diff --git a/apps/mobile/stores/transaction/store.ts b/apps/mobile/stores/transaction/store.ts index 816e890b..c87e7fe6 100644 --- a/apps/mobile/stores/transaction/store.ts +++ b/apps/mobile/stores/transaction/store.ts @@ -8,6 +8,7 @@ type Transaction = TransactionPopulated export interface TransactionStore { transactions: Transaction[] + _reset: () => void setTransactions: (transactions: Transaction[]) => void addTransactions: (transactions: Transaction[]) => void updateTransactionsByRange: (args: { @@ -31,6 +32,8 @@ export const useTransactionStore = create()( persist( (set) => ({ transactions: [], + // biome-ignore lint/style/useNamingConvention: + _reset: () => set({ transactions: [] }), setTransactions: (transactions) => { set({ transactions: normalizeTransactions(transactions) }) }, diff --git a/apps/mobile/stores/use-reset-all-stores.tsx b/apps/mobile/stores/use-reset-all-stores.tsx new file mode 100644 index 00000000..00f10763 --- /dev/null +++ b/apps/mobile/stores/use-reset-all-stores.tsx @@ -0,0 +1,18 @@ +import { useCallback } from 'react' +import { useBudgetStore } from './budget/store' +import { useCategoryStore } from './category/store' +import { useTransactionStore } from './transaction/store' + +export const useResetAllStores = () => { + const resetBudgetStore = useBudgetStore((state) => state._reset) + const resetCategoryStore = useCategoryStore((state) => state._reset) + const resetTransactionStore = useTransactionStore((state) => state._reset) + + const resetAllStores = useCallback(() => { + resetBudgetStore() + resetCategoryStore() + resetTransactionStore() + }, [resetBudgetStore, resetCategoryStore, resetTransactionStore]) + + return resetAllStores +}