diff --git a/client/app/_layout.tsx b/client/app/_layout.tsx
index fe93acd..9ca2ec0 100644
--- a/client/app/_layout.tsx
+++ b/client/app/_layout.tsx
@@ -1,33 +1,45 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
-import { Stack } from "expo-router"
-import { useColorScheme } from '@/hooks/useColorScheme'
+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 { useReactNavigationDevTools } from '@dev-plugins/react-navigation'
+import { useReactQueryDevTools } from '@dev-plugins/react-query'
+import { useColorScheme } from '@/hooks/useColorScheme'
import store from '@/utils/store'
import { useStartBackgroundFetchServiceEffect } from '@/utils/background'
import { useStartGeolocationServiceEffect } from '@/utils/geolocation'
import LoginProvider from '@/components/LoginProvider'
+const queryClient = new QueryClient()
+
export default function RootLayout() {
useStartGeolocationServiceEffect()
useStartBackgroundFetchServiceEffect()
const colorScheme = useColorScheme()
+ const navigationRef = useNavigationContainerRef()
+ useReactNavigationDevTools(navigationRef)
+
+ useReactQueryDevTools(queryClient)
+
return (
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/client/app/login.tsx b/client/app/login.tsx
index f3a464c..7281b39 100644
--- a/client/app/login.tsx
+++ b/client/app/login.tsx
@@ -1,8 +1,8 @@
+import { useLoginMutation } from "@/hooks/mutations/useLoginMutation"
import { useAuth, useAuthLogin, useAuthLogout } from "@/hooks/useAuth"
-import * as SecureStore from "@/utils/SecureStore"
+import { useMutation } from "@tanstack/react-query"
import { useRouter } from "expo-router"
import { useRef, useState } from "react"
-import { Platform } from "react-native"
import { Appbar, Button, Dialog, Portal, Surface, Text, TextInput } from "react-native-paper"
export default function Login() {
@@ -13,7 +13,6 @@ export default function Login() {
const authLogout = useAuthLogout()
const isLoggedIn = auth.loggedIn
- const [loggingIn, setLoggingIn] = useState(false)
const [name, setName] = useState(auth.name ?? "")
const [password, setPassword] = useState("")
const [errorDialogVisible, setErrorDialogVisible] = useState(false)
@@ -25,80 +24,55 @@ export default function Login() {
const hideErrorDialog = () => setErrorDialogVisible(false)
- async function login() {
- if (loggingIn)
- return
- setLoggingIn(true)
- const resp = await 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())
- .catch(err => {
+ const loginMutation = useLoginMutation({
+ authLogin,
+ onPostSuccess: () => {
+ if (router.canGoBack())
+ router.back()
+ else
+ router.navigate('/')
+ },
+ onError: ({ response, error }) => {
setErrorDialogVisible(true)
- setErrorTitle("Erreur")
- setErrorText("Une erreur inconnue est survenue lors de la connexion. Veuillez réessayer plus tard. " + err)
- setLoggingIn(false)
- })
- if (!resp)
- return
- else if (resp.error) {
- setErrorDialogVisible(true)
- setErrorTitle(resp.error)
- setErrorText(resp.message)
- setLoggingIn(false)
- return
+ if (response) {
+ setErrorTitle(response.error)
+ setErrorText(response.message)
+ }
+ else {
+ setErrorTitle("Erreur")
+ setErrorText(`Une erreur est survenue lors de la connexion : ${error}`)
+ }
}
- setLoggingIn(false)
- authLogin({ name: name, token: resp.accessToken })
- SecureStore.setItem("apiName", name)
- if (Platform.OS !== "web") {
- // Le stockage navigateur n'est pas sûr, on évite de stocker un mot de passe à l'intérieur
- SecureStore.setItem("apiPassword", password)
- }
- SecureStore.setItem("apiToken", resp.accessToken)
- if (router.canGoBack())
- router.back()
- else
- router.navigate('/')
- }
-
- async function logout() {
- authLogout()
- await SecureStore.deleteItemAsync("apiName")
- await SecureStore.deleteItemAsync("apiPassword")
- await SecureStore.deleteItemAsync("apiToken")
- }
+ })
return (
{isLoggedIn && router.canGoBack() ? router.back()} /> : undefined}
- {isLoggedIn ? : undefined}
+ {isLoggedIn ? : undefined}
setName(text)}
+ onChangeText={setName}
onSubmitEditing={() => passwordRef?.current.focus()}
style={{ margin: 8 }} />
setPassword(text)}
- onSubmitEditing={login}
+ onChangeText={setPassword}
+ onSubmitEditing={() => loginMutation.mutate({ name, password })}
secureTextEntry={true}
style={{ margin: 8 }} />
diff --git a/client/components/LoginProvider.tsx b/client/components/LoginProvider.tsx
index 6d23c65..4b48f12 100644
--- a/client/components/LoginProvider.tsx
+++ b/client/components/LoginProvider.tsx
@@ -3,6 +3,7 @@ import { useRouteInfo } from 'expo-router/build/hooks'
import { ReactNode, useEffect } from 'react'
import { useAuth, useAuthLogin } from '@/hooks/useAuth'
import * as SecureStore from '@/utils/SecureStore'
+import { useLoginMutation } from '@/hooks/mutations/useLoginMutation'
type Props = {
loginRedirect: Href
@@ -14,6 +15,15 @@ export default function LoginProvider({ loginRedirect, children }: Props) {
const route = useRouteInfo()
const auth = useAuth()
const authLogin = useAuthLogin()
+ const loginMutation = useLoginMutation({
+ authLogin,
+ onError: ({ response }) => {
+ if (response)
+ authLogin({ name: auth.name ?? "", password: null, token: null })
+ else
+ authLogin({ name: auth.name ?? "", token: null })
+ }
+ })
// Renouvellement auto du jeton d'authentification
useEffect(() => {
@@ -31,23 +41,8 @@ export default function LoginProvider({ loginRedirect, children }: Props) {
}
const timeout = setTimeout(async () => {
const password = SecureStore.getItem('apiPassword')
- if (password) {
- await 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())
- .then(resp => {
- if (!resp.error)
- authLogin({ name: name, token: resp.accessToken })
- else
- authLogin({ name: name, token: null })
- })
- }
- else {
- authLogin({ name: name, token: null })
- }
+ if (password)
+ loginMutation.mutate({ name, password })
}, waitTime)
return () => clearTimeout(timeout)
}, [auth])
diff --git a/client/hooks/mutations/useLoginMutation.ts b/client/hooks/mutations/useLoginMutation.ts
new file mode 100644
index 0000000..3c04bbd
--- /dev/null
+++ b/client/hooks/mutations/useLoginMutation.ts
@@ -0,0 +1,49 @@
+import { AuthPayload } from "@/utils/features/location/authSlice"
+import { useMutation } from "@tanstack/react-query"
+
+type ErrorResponse = {
+ error: string
+ message: string
+ statusCode: number
+}
+
+type LoginForm = {
+ name: string
+ password: string
+}
+
+type onPostSuccessFunc = () => void
+type ErrorFuncProps = { response?: ErrorResponse, error?: Error }
+type onErrorFunc = (props: ErrorFuncProps) => void
+
+type LoginProps = {
+ authLogin: (payload: AuthPayload) => { payload: AuthPayload, type: "auth/login" }
+ onPostSuccess?: onPostSuccessFunc
+ onError?: onErrorFunc
+}
+
+export const useLoginMutation = ({ authLogin, onPostSuccess, onError }: LoginProps) => {
+ return useMutation({
+ mutationFn: ({ 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) => {
+ if (data.error) {
+ if (onError)
+ onError({ response: data })
+ return
+ }
+ authLogin({ name: name, password: password, token: data.accessToken })
+ if (onPostSuccess)
+ onPostSuccess()
+ },
+ onError: (error: Error) => {
+ if (onError)
+ onError({ error: error })
+ }
+ })
+}
diff --git a/client/package-lock.json b/client/package-lock.json
index 7ccf17c..b1fee79 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -8,12 +8,15 @@
"name": "traintrape-moi-client",
"version": "1.0.0",
"dependencies": {
+ "@dev-plugins/react-navigation": "^0.1.0",
+ "@dev-plugins/react-query": "^0.1.0",
"@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-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@reduxjs/toolkit": "^2.4.0",
+ "@tanstack/react-query": "^5.62.7",
"@turf/circle": "^7.1.0",
"expo": "~52.0.11",
"expo-background-fetch": "~13.0.3",
@@ -2313,6 +2316,51 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
+ "node_modules/@dev-plugins/react-navigation": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@dev-plugins/react-navigation/-/react-navigation-0.1.0.tgz",
+ "integrity": "sha512-eV6gkA/wH2E7LS6UfH5Kw1Qhk4zcMU3OMhQSqn4XCZn33HoT/Q2tcBHrUiiWcBVX9MwB9NyJB0hPnJ8lNAIQxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@react-navigation/devtools": "^7.0.1",
+ "nanoid": "^5.0.8"
+ },
+ "peerDependencies": {
+ "@react-navigation/core": "*",
+ "expo": "^52.0.0"
+ }
+ },
+ "node_modules/@dev-plugins/react-navigation/node_modules/nanoid": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
+ "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.js"
+ },
+ "engines": {
+ "node": "^18 || >=20"
+ }
+ },
+ "node_modules/@dev-plugins/react-query": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@dev-plugins/react-query/-/react-query-0.1.0.tgz",
+ "integrity": "sha512-GZeF9+AHI98DjUuga6YCwbhlq2BpaQ3rGUc7o9weO4oEXNFF7rv2asWpMDdBa0BXvVZjdERSW7HrRx2JAWF+6w==",
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.3.1"
+ },
+ "peerDependencies": {
+ "@tanstack/react-query": "*",
+ "expo": "^52.0.0"
+ }
+ },
"node_modules/@egjs/hammerjs": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz",
@@ -4207,6 +4255,20 @@
"react": ">= 18.2.0"
}
},
+ "node_modules/@react-navigation/devtools": {
+ "version": "7.0.14",
+ "resolved": "https://registry.npmjs.org/@react-navigation/devtools/-/devtools-7.0.14.tgz",
+ "integrity": "sha512-Pb80D3kO84wW/VezHXTks1Bpbk0O6XTEJSIyzanXC3h4clICq/sVQIcXjHbHrtQnNB0SJdhf6/LG9l+SRyatYw==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "nanoid": "3.3.7",
+ "stacktrace-parser": "^0.1.10"
+ },
+ "peerDependencies": {
+ "react": ">= 18.2.0"
+ }
+ },
"node_modules/@react-navigation/elements": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.2.0.tgz",
@@ -4445,6 +4507,32 @@
"@sinonjs/commons": "^3.0.0"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.62.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.7.tgz",
+ "integrity": "sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA==",
+ "license": "MIT",
+ "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",
+ "integrity": "sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.62.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@@ -8381,6 +8469,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/flatted": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
+ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
+ "license": "ISC"
+ },
"node_modules/flow-enums-runtime": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz",
diff --git a/client/package.json b/client/package.json
index cbc1aef..73e476b 100644
--- a/client/package.json
+++ b/client/package.json
@@ -14,12 +14,15 @@
"preset": "jest-expo"
},
"dependencies": {
+ "@dev-plugins/react-navigation": "^0.1.0",
+ "@dev-plugins/react-query": "^0.1.0",
"@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-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0",
"@reduxjs/toolkit": "^2.4.0",
+ "@tanstack/react-query": "^5.62.7",
"@turf/circle": "^7.1.0",
"expo": "~52.0.11",
"expo-background-fetch": "~13.0.3",
@@ -31,7 +34,6 @@
"expo-linking": "~7.0.3",
"expo-location": "^18.0.2",
"expo-notifications": "~0.29.11",
- "expo-updates": "~0.26.10",
"expo-router": "~4.0.9",
"expo-secure-store": "~14.0.0",
"expo-splash-screen": "~0.29.13",
@@ -39,6 +41,7 @@
"expo-symbols": "~0.2.0",
"expo-system-ui": "~4.0.4",
"expo-task-manager": "^12.0.3",
+ "expo-updates": "~0.26.10",
"expo-web-browser": "~14.0.1",
"maplibre-gl": "^4.7.1",
"maplibre-react-components": "^0.1.9",
diff --git a/client/utils/features/location/authSlice.ts b/client/utils/features/location/authSlice.ts
index 8f0028f..e8ce9c7 100644
--- a/client/utils/features/location/authSlice.ts
+++ b/client/utils/features/location/authSlice.ts
@@ -1,5 +1,6 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import * as SecureStore from '@/utils/SecureStore'
+import { Platform } from 'react-native'
interface AuthState {
loggedIn: boolean,
@@ -9,6 +10,7 @@ interface AuthState {
export interface AuthPayload {
name: string,
+ password?: string | null,
token: string | null,
}
@@ -26,12 +28,28 @@ export const authSlice = createSlice({
state.loggedIn = action.payload.token !== null
state.name = action.payload.name
state.token = action.payload.token
- console.log(state)
+
+ SecureStore.setItem('apiName', action.payload.name)
+ if (action.payload.password !== undefined && Platform.OS !== "web") {
+ // Le stockage navigateur n'est pas sûr, on évite de stocker un mot de passe à l'intérieur
+ if (action.payload.password)
+ SecureStore.setItem('apiPassword', action.payload.password)
+ else
+ SecureStore.deleteItemAsync('apiPassword')
+ }
+ if (action.payload.token)
+ SecureStore.setItem('apiToken', action.payload.token)
+ else
+ SecureStore.deleteItemAsync('apiToken')
},
logout: (state) => {
state.loggedIn = false
state.name = null
state.token = null
+
+ SecureStore.deleteItemAsync('apiName')
+ SecureStore.deleteItemAsync('apiPassword')
+ SecureStore.deleteItemAsync('apiToken')
}
},
})