diff --git a/client/app/_layout.tsx b/client/app/_layout.tsx index 9ca2ec0..e79865c 100644 --- a/client/app/_layout.tsx +++ b/client/app/_layout.tsx @@ -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 ( - + queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}> @@ -39,7 +56,7 @@ export default function RootLayout() { - + ) } diff --git a/client/components/LoginProvider.tsx b/client/components/LoginProvider.tsx index 4b48f12..0ceab3b 100644 --- a/client/components/LoginProvider.tsx +++ b/client/components/LoginProvider.tsx @@ -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} diff --git a/client/hooks/mutations/useLoginMutation.ts b/client/hooks/mutations/useLoginMutation.ts index 3c04bbd..ab22664 100644 --- a/client/hooks/mutations/useLoginMutation.ts +++ b/client/hooks/mutations/useLoginMutation.ts @@ -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 }) } diff --git a/client/package-lock.json b/client/package-lock.json index b1fee79..17a8047 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index 73e476b..df46b4e 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/utils/SecureStore.web.ts b/client/utils/SecureStore.web.ts index ed0822b..68a23a1 100644 --- a/client/utils/SecureStore.web.ts +++ b/client/utils/SecureStore.web.ts @@ -17,3 +17,27 @@ export function setItem(key: string, value: string): void { export async function setItemAsync(key: string, value: string): Promise { localStorage.setItem(key, value) } + +/** +import AsyncStorage from '@react-native-async-storage/async-storage' + +export async function deleteItemAsync(key: string): Promise { + return AsyncStorage.removeItem(key) +} + +export function getItem(key: string): string | null { + return Promise.apply(AsyncStorage.getItem(key)) +} + +export async function getItemAsync(key: string): Promise { + 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 { + return AsyncStorage.setItem(key, value) +} + */ \ No newline at end of file diff --git a/client/utils/features/location/authSlice.ts b/client/utils/features/location/authSlice.ts index e8ce9c7..88c9c5e 100644 --- a/client/utils/features/location/authSlice.ts +++ b/client/utils/features/location/authSlice.ts @@ -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({