Stockage persistent des requêtes

This commit is contained in:
Emmy D'Anello 2024-12-11 00:30:20 +01:00
parent 1c52ff5a10
commit db7a0b970d
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
7 changed files with 153 additions and 20 deletions

View File

@ -3,7 +3,10 @@ import { Stack, useNavigationContainerRef } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import { Provider as StoreProvider } from 'react-redux'
import { MD3DarkTheme, MD3LightTheme, PaperProvider } from 'react-native-paper'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { QueryClient } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
import { useReactNavigationDevTools } from '@dev-plugins/react-navigation'
import { useReactQueryDevTools } from '@dev-plugins/react-query'
import { useColorScheme } from '@/hooks/useColorScheme'
@ -12,7 +15,15 @@ import { useStartBackgroundFetchServiceEffect } from '@/utils/background'
import { useStartGeolocationServiceEffect } from '@/utils/geolocation'
import LoginProvider from '@/components/LoginProvider'
const queryClient = new QueryClient()
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 24 * 60 * 60 * 1000, // 24h
staleTime: 2000,
retry: 5,
}
}
})
export default function RootLayout() {
useStartGeolocationServiceEffect()
@ -21,12 +32,18 @@ export default function RootLayout() {
const navigationRef = useNavigationContainerRef()
useReactNavigationDevTools(navigationRef)
useReactQueryDevTools(queryClient)
const asyncStoragePersister = createAsyncStoragePersister({
storage: AsyncStorage,
})
return (
<StoreProvider store={store}>
<QueryClientProvider client={queryClient}>
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister }}
onSuccess={() => queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}>
<LoginProvider loginRedirect={'/login'}>
<PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
@ -39,7 +56,7 @@ export default function RootLayout() {
</ThemeProvider>
</PaperProvider>
</LoginProvider>
</QueryClientProvider>
</PersistQueryClientProvider>
</StoreProvider>
)
}

View File

@ -25,8 +25,23 @@ export default function LoginProvider({ loginRedirect, children }: Props) {
}
})
// Renouvellement auto du jeton d'authentification
useEffect(() => {
(async () => {
const storedName = await SecureStore.getItemAsync('apiName')
const storedToken = await SecureStore.getItemAsync('apiToken')
if (!auth.loggedIn && storedName !== null && storedName !== auth.name && storedToken !== auth.token) {
authLogin({ name: storedName, token: storedToken })
return
}
// Si on est pas connecté⋅e, on reste sur la fenêtre de connexion
if (!auth.loggedIn && route.pathname !== loginRedirect)
router.navigate(loginRedirect)
})()
}, [auth, authLogin, router, route])
useEffect(() => {
// Renouvellement auto du jeton d'authentification
const { name, token } = auth
const password = SecureStore.getItem('apiPassword')
if (name === null || (password === null && token === null))
@ -45,13 +60,7 @@ export default function LoginProvider({ loginRedirect, children }: Props) {
loginMutation.mutate({ name, password })
}, waitTime)
return () => clearTimeout(timeout)
}, [auth])
// Si on est pas connecté⋅e, on reste sur la fenêtre de connexion
useEffect(() => {
if (!auth.loggedIn && route.pathname !== loginRedirect)
router.navigate(loginRedirect)
}, [auth, route, router])
}, [auth, authLogin])
return <>
{children}

View File

@ -24,14 +24,15 @@ type LoginProps = {
export const useLoginMutation = ({ authLogin, onPostSuccess, onError }: LoginProps) => {
return useMutation({
mutationFn: ({ name, password }: LoginForm) => {
mutationFn: async ({ name, password }: LoginForm) => {
return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/auth/login/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: name, password: password })
}).then(resp => resp.json())
},
onSuccess: (data, { name, password }: LoginForm) => {
networkMode: 'always',
onSuccess: async (data, { name, password }: LoginForm) => {
if (data.error) {
if (onError)
onError({ response: data })
@ -41,7 +42,7 @@ export const useLoginMutation = ({ authLogin, onPostSuccess, onError }: LoginPro
if (onPostSuccess)
onPostSuccess()
},
onError: (error: Error) => {
onError: async (error: Error) => {
if (onError)
onError({ error: error })
}

View File

@ -13,10 +13,13 @@
"@expo/vector-icons": "^14.0.2",
"@maplibre/maplibre-react-native": "^10.0.0-alpha.28",
"@pchmn/expo-material3-theme": "github:pchmn/expo-material3-theme",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@reduxjs/toolkit": "^2.4.0",
"@tanstack/query-async-storage-persister": "^5.62.7",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-persist-client": "^5.62.7",
"@turf/circle": "^7.1.0",
"expo": "~52.0.11",
"expo-background-fetch": "~13.0.3",
@ -3846,6 +3849,18 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz",
"integrity": "sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==",
"license": "MIT",
"dependencies": {
"merge-options": "^3.0.4"
},
"peerDependencies": {
"react-native": "^0.0.0-0 || >=0.60 <1.0"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.76.3",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.3.tgz",
@ -4507,6 +4522,19 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@tanstack/query-async-storage-persister": {
"version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.62.7.tgz",
"integrity": "sha512-8qSJ1oTnGhCikPADWd35xrswyYtmkqYnakWgqeXjxL+F+qPGgsfexNUlBu9TNqo9eAP/1ia4Lt5Ks2fTsMzBgg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-persist-client-core": "5.62.7"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.7.tgz",
@ -4517,6 +4545,19 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-persist-client-core": {
"version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.62.7.tgz",
"integrity": "sha512-9HcaD9rEp2nGWnrw2osK5UCSKJbJKEdn+MEhVVfnUPSFN7MZFpFZxpRCHJi3fRpWOYsVeH9EFODX+aoJaniJMA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.62.7"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.7.tgz",
@ -4533,6 +4574,23 @@
"react": "^18 || ^19"
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "5.62.7",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-5.62.7.tgz",
"integrity": "sha512-RmEJ3YvsK7lv1Of3CCXBgHDtoZVwHMtTKCTegZz+xijVJsgJaNNfel4YTpbQ0ydnWT2IcohdqnHUtBE6p1KCIA==",
"license": "MIT",
"dependencies": {
"@tanstack/query-persist-client-core": "5.62.7"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^5.62.7",
"react": "^18 || ^19"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -9472,6 +9530,15 @@
"node": ">=8"
}
},
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -11406,6 +11473,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
"node_modules/merge-options": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
"license": "MIT",
"dependencies": {
"is-plain-obj": "^2.1.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",

View File

@ -22,7 +22,9 @@
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@reduxjs/toolkit": "^2.4.0",
"@tanstack/query-async-storage-persister": "^5.62.7",
"@tanstack/react-query": "^5.62.7",
"@tanstack/react-query-persist-client": "^5.62.7",
"@turf/circle": "^7.1.0",
"expo": "~52.0.11",
"expo-background-fetch": "~13.0.3",
@ -56,7 +58,8 @@
"react-native-screens": "~4.1.0",
"react-native-web": "~0.19.13",
"react-native-webview": "13.12.2",
"react-redux": "^9.1.2"
"react-redux": "^9.1.2",
"@react-native-async-storage/async-storage": "1.23.1"
},
"devDependencies": {
"@babel/core": "^7.25.2",

View File

@ -17,3 +17,27 @@ export function setItem(key: string, value: string): void {
export async function setItemAsync(key: string, value: string): Promise<void> {
localStorage.setItem(key, value)
}
/**
import AsyncStorage from '@react-native-async-storage/async-storage'
export async function deleteItemAsync(key: string): Promise<void> {
return AsyncStorage.removeItem(key)
}
export function getItem(key: string): string | null {
return Promise.apply(AsyncStorage.getItem(key))
}
export async function getItemAsync(key: string): Promise<string | null> {
return AsyncStorage.getItem(key)
}
export function setItem(key: string, value: string): void {
AsyncStorage.setItem(key, value)
}
export async function setItemAsync(key: string, value: string): Promise<void> {
return AsyncStorage.setItem(key, value)
}
*/

View File

@ -15,9 +15,9 @@ export interface AuthPayload {
}
const initialState: AuthState = {
loggedIn: SecureStore.getItem('apiName') !== null && SecureStore.getItem('apiToken') !== null,
name: SecureStore.getItem('apiName'),
token: SecureStore.getItem('apiToken'),
loggedIn: false,
name: null,
token: null,
}
export const authSlice = createSlice({