Fix photos + API hooks + cleaning

This commit is contained in:
Sebastien Riviere
2026-02-17 14:32:37 +01:00
parent e0aaeb63f7
commit 05a60612c6
24 changed files with 341 additions and 231 deletions

View File

@@ -7,12 +7,30 @@ lang: en-GB
This tutorial will help you to set up your development environment, use a dev build and create an APK.
## Table of Contents
* [Environment](#environment) : Dependencies, packages, app key and device
* [Environment](#environment) : Dependencies, packages, app key, device and .env
* [Dev build](#dev-build) : Create, install and use
* [APK](#apk) : Create and install
## Environment
### .env files
Some infos like API keys or IP addresses cannot be pushed on the public repository, therefore you have to create the .env files that will store those values. Go in the `traque-app` folder, create those two files and replace the FILL_HERE with the correct values (you can ask someone already working on the project) :
* `.env.development` :
```.env
EXPO_PUBLIC_SERVER_URL=FILL_HERE
EXPO_PUBLIC_SOCKET_URL=FILL_HERE
```
* `.env.production` :
```.env
EXPO_PUBLIC_SERVER_URL=FILL_HERE
EXPO_PUBLIC_SOCKET_URL=FILL_HERE
```
### Installing dependencies and preparing the device
You will need to install Android Studio, some SDKs and prepare your device if you want to use a physical one. Follow this [tutorial](https://reactnative.dev/docs/set-up-your-environment?os=linux&platform=android).

View File

@@ -8,22 +8,22 @@ import { CustomButton } from '../components/button';
import { CustomImage } from '../components/image';
import { CustomTextInput } from '../components/input';
// Contexts
import { useSocket } from "../context/socketContext";
import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from "../context/teamContext";
// Hooks
import { usePickImage } from '../hook/usePickImage';
import { useImageApi } from '../hook/useImageApi';
// Util
import { Colors } from '../util/colors';
const Index = () => {
const router = useRouter();
const {SERVER_URL} = useSocket();
const {login, loggedIn, loading} = useTeamConnexion();
const {login, loggedIn} = useTeamConnexion();
const {getLocationAuthorization, stopLocationTracking} = useTeamContext();
const {image, pickImage, sendImage} = usePickImage();
const [teamID, setTeamID] = useState("");
const {image, pickImage} = usePickImage();
const [teamId, setTeamId] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const { uploadTeamImage } = useImageApi();
// Disbaling location tracking
useEffect(() => {
@@ -32,32 +32,37 @@ const Index = () => {
// Routeur
useEffect(() => {
if (!loading && loggedIn) {
if (loggedIn) {
uploadTeamImage(image?.uri);
router.replace("/interface");
}
}, [router, loggedIn, loading]);
}, [router, loggedIn, uploadTeamImage, image]);
function handleSubmit() {
if (!isSubmitting && !loading) {
setIsSubmitting(true);
if (getLocationAuthorization()) {
login(parseInt(teamID))
.then((response) => {
if (response.isLoggedIn) {
sendImage(`${SERVER_URL}/upload?team=${teamID}`);
} else {
Alert.alert("Échec", "L'ID d'équipe est inconnu.");
}
setIsSubmitting(false);
})
.catch(() => {
Alert.alert("Échec", "La connection au serveur a échoué.");
setIsSubmitting(false);
});
}
setTeamID("");
const handleSubmit = async () => {
if (isSubmitting || !getLocationAuthorization()) return;
setIsSubmitting(true);
const regex = /^\d{6}$/;
if (!regex.test(teamId)) {
setTimeout(() => Alert.alert("Erreur", "Veuillez entrer un ID d'équipe valide."), 100);
return;
}
}
try {
const response = await login(teamId);
if (response.isLoggedIn) {
setTeamId("");
} else {
setTimeout(() => Alert.alert("Échec", "L'ID d'équipe est inconnu."), 100);
}
} catch (error) {
setTimeout(() => Alert.alert("Échec", "La connexion au serveur a échoué."), 100);
} finally {
setIsSubmitting(false);
}
};
return (
<ScrollView contentContainerStyle={styles.container}>
@@ -67,7 +72,7 @@ const Index = () => {
<Text style={styles.logoText}>LA TRAQUE</Text>
</View>
<View style={styles.subContainer}>
<CustomTextInput value={teamID} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamID}/>
<CustomTextInput value={teamId} inputMode="numeric" placeholder="ID de l'équipe" style={styles.input} onChangeText={setTeamId}/>
</View>
<View style={styles.subContainer}>
<Text style={{fontSize: 15}}>Appuyer pour changer la photo d&apos;équipe</Text>
@@ -75,7 +80,7 @@ const Index = () => {
<CustomImage source={image ? {uri: image.uri} : require('../assets/images/missing_image.jpg')} onPress={pickImage}/>
</View>
<View style={styles.subContainer}>
<CustomButton label={(isSubmitting || loading) ? "..." : "Valider"} onPress={handleSubmit}/>
<CustomButton label={isSubmitting ? "..." : "Valider"} onPress={handleSubmit}/>
</View>
</View>
</ScrollView>

View File

@@ -20,9 +20,10 @@ import { Colors } from '../util/colors';
const Interface = () => {
const router = useRouter();
const {messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
const {loggedIn, logout, loading} = useTeamConnexion();
const {name, ready, captured, locationSendDeadline, sendCurrentPosition, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = useGame();
const {teamInfos, messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos;
const {loggedIn, logout} = useTeamConnexion();
const {sendCurrentPosition} = useGame();
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
@@ -48,12 +49,10 @@ const Interface = () => {
// Router
useEffect(() => {
if (!loading) {
if (!loggedIn) {
router.replace("/");
}
if (!loggedIn) {
router.replace("/");
}
}, [router, loggedIn, loading]);
}, [router, loggedIn]);
// Activating geolocation tracking
useEffect(() => {

View File

@@ -9,7 +9,6 @@ import { CustomTextInput } from './input';
import { Stat } from './stat';
// Contexts
import { useTeamContext } from '../context/teamContext';
import { useSocket } from '../context/socketContext';
// Hooks
import { useTimeDifference } from '../hook/useTimeDifference';
import { useGame } from '../hook/useGame';
@@ -17,17 +16,18 @@ import { useGame } from '../hook/useGame';
import { GameState } from '../util/gameState';
import { Colors } from '../util/colors';
import { secondsToHHMMSS } from '../util/functions';
import { useImageApi } from '../hook/useImageApi';
export const Drawer = ({ height }) => {
const [collapsibleState, setCollapsibleState] = useState(true);
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const {SERVER_URL} = useSocket();
const {gameState, startDate} = useTeamContext();
const {capture, enemyName, captureCode, name, teamId, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = useGame();
const {teamInfos, gameState, startDate} = useTeamContext();
const {enemyName, captureCode, name, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = teamInfos;
const {capture} = useGame();
const [timeSinceStart] = useTimeDifference(startDate, 1000);
const [enemyImageURI, setEnemyImageURI] = useState("../assets/images/missing_image.jpg");
const [captureStatus, setCaptureStatus] = useState(0); // 0 : no capture | 1 : waiting for response from server | 2 : capture failed | 3 : capture succesful
const captureStatusColor = {0: "#777", 1: "#FFA500", 2: "#FF6B6B", 3: "#81C784"};
const { enemyImage } = useImageApi();
const avgSpeed = useMemo(() => {
const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600;
@@ -48,11 +48,6 @@ export const Drawer = ({ height }) => {
}
}, [captureStatus]);
// Refresh the image
useEffect(() => {
setEnemyImageURI(`${SERVER_URL}/photo/enemy?team=${teamId}&t=${new Date().getTime()}`);
}, [SERVER_URL, enemyName, teamId]);
const handleCapture = () => {
if (captureStatus != 1) {
setCaptureStatus(1);
@@ -86,8 +81,8 @@ export const Drawer = ({ height }) => {
}
{ gameState == GameState.PLAYING && !hasHandicap && <Fragment>
<View style={styles.imageContainer}>
{<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>}
{<CustomImage source={{ uri : enemyImageURI }} canZoom/>}
<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>
<CustomImage source={enemyImage} canZoom/>
</View>
<View style={styles.actionsContainer}>
<View style={styles.actionsLeftContainer}>

View File

@@ -1,3 +1,4 @@
// React
import { Fragment } from 'react';
import { Polygon } from 'react-native-maps';
import { circleToPolygon } from '../util/functions';

View File

@@ -7,15 +7,13 @@ import LinearGradient from 'react-native-linear-gradient';
import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer';
// Contexts
import { useTeamContext } from '../context/teamContext';
// Hooks
import { useGame } from '../hook/useGame';
// Util
import { GameState } from '../util/gameState';
import { ZoneTypes, InitialRegions } from '../util/constants';
export const CustomMap = () => {
const {zoneType, zoneExtremities, location, gameState} = useTeamContext();
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = useGame();
const {teamInfos, zoneType, zoneExtremities, location, gameState} = useTeamContext();
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos;
const [centerMap, setCenterMap] = useState(true);
const mapRef = useRef(null);

View File

@@ -1,18 +1,16 @@
// React
import { createContext, useContext, useMemo } from "react";
// IO
import { io } from "socket.io-client";
// Util
import { SOCKET_URL } from "../util/constants";
const IP = "172.16.1.180";
const SOCKET_URL = `ws://${IP}/player`;
const SERVER_URL = `http://${IP}/back`;
const SocketContext = createContext();
export const teamSocket = io(SOCKET_URL, {
path: "/back/socket.io",
});
export const SocketContext = createContext();
const teamSocket = io(SOCKET_URL, {path: "/back/socket.io"});
export const SocketProvider = ({ children }) => {
const value = useMemo(() => ({ teamSocket, SERVER_URL }), []);
const value = useMemo(() => ({ teamSocket }), []);
return (
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>

View File

@@ -1,22 +1,22 @@
// React
import { createContext, useContext, useMemo } from "react";
import { useSocket } from "./socketContext";
// Hook
import { useSocketAuth } from "../hook/useSocketAuth";
const teamConnexionContext = createContext();
const TeamConnexionContext = createContext();
export const TeamConnexionProvider = ({ children }) => {
const { teamSocket } = useSocket();
const { login, password: teamId, loggedIn, loading, logout } = useSocketAuth(teamSocket, "team_password");
const { login, password: teamId, loggedIn, logout } = useSocketAuth();
const value = useMemo(() => ({ teamId, login, logout, loggedIn, loading}), [teamId, login, logout, loggedIn, loading]);
const value = useMemo(() => ({ teamId, login, logout, loggedIn}), [teamId, login, logout, loggedIn]);
return (
<teamConnexionContext.Provider value={value}>
<TeamConnexionContext.Provider value={value}>
{children}
</teamConnexionContext.Provider>
</TeamConnexionContext.Provider>
);
};
export const useTeamConnexion = () => {
return useContext(teamConnexionContext);
return useContext(TeamConnexionContext);
};

View File

@@ -1,12 +1,16 @@
// React
import { createContext, useContext, useMemo, useState } from "react";
// Context
import { useSocket } from "./socketContext";
import { useTeamConnexion } from "./teamConnexionContext";
import { GameState } from "../util/gameState";
// Hook
import { useSendDeviceInfo } from "../hook/useSendDeviceInfo";
import { useLocation } from "../hook/useLocation";
import { useSocketListener } from "../hook/useSocketListener";
// Util
import { GameState } from "../util/gameState";
const teamContext = createContext();
const TeamContext = createContext();
export const TeamProvider = ({children}) => {
const {teamSocket} = useSocket();
@@ -51,18 +55,17 @@ export const TeamProvider = ({children}) => {
useSocketListener(teamSocket, "logout", logout);
const value = useMemo(() => (
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking}
), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages, location, getLocationAuthorization, startLocationTracking, stopLocationTracking]);
return (
<teamContext.Provider value={value}>
<TeamContext.Provider value={value}>
{children}
</teamContext.Provider>
</TeamContext.Provider>
);
};
export const useTeamContext = () => {
return useContext(teamContext);
return useContext(TeamContext);
};

View File

@@ -1,33 +1,28 @@
import { useSocket } from "../context/socketContext";
import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from "../context/teamContext";
// React
import { useCallback } from "react";
// Hook
import { useSocketCommands } from "./useSocketCommands";
export const useGame = () => {
const { teamSocket } = useSocket();
const { teamId } = useTeamConnexion();
const { teamInfos } = useTeamContext();
const { emitSendPosition, emitCapture } = useSocketCommands();
function sendCurrentPosition() {
console.log("Reveal position.");
teamSocket.emit("send_position");
}
const sendCurrentPosition = useCallback(() => {
emitSendPosition();
}, [emitSendPosition]);
function capture(captureCode) {
console.log("Try to capture :", captureCode);
const capture = useCallback((captureCode) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
console.warn("Server did not respond to capture emit.");
reject();
console.warn("Server timeout: capture", captureCode);
reject(new Error("Timeout"));
}, 3000);
teamSocket.emit("capture", captureCode, (response) => {
emitCapture(captureCode, (response) => {
clearTimeout(timeout);
console.log(response.message);
resolve(response);
});
});
}
}, [emitCapture]);
return {...teamInfos, sendCurrentPosition, capture, teamId};
return { sendCurrentPosition, capture };
};

View File

@@ -0,0 +1,45 @@
// Rect
import { useCallback, useMemo } from "react";
// Contexts
import { useTeamConnexion } from "../context/teamConnexionContext";
import { useTeamContext } from '../context/teamContext';
// Util
import { SERVER_URL } from "../util/constants";
export const useImageApi = () => {
const { teamId } = useTeamConnexion();
const { teamInfos } = useTeamContext();
const { enemyName } = teamInfos;
const uploadTeamImage = useCallback(async (imageUri) => {
if (!imageUri || !teamId) return;
const data = new FormData();
data.append('file', {
uri: imageUri,
name: 'photo.jpg',
type: 'image/jpeg',
});
try {
const response = await fetch(`${SERVER_URL}/upload?team=${teamId}`, {
method: 'POST',
body: data,
});
if (!response.ok) throw new Error("Échec de l'upload");
return await response.blob();
} catch (error) {
console.error("Erreur uploadImage :", error);
throw error;
}
}, [teamId]);
const enemyImage = useMemo(() => {
if (!teamId || !enemyName) return require('../assets/images/missing_image.jpg');
return {uri: `${SERVER_URL}/photo/enemy?team=${teamId}`};
}, [teamId, enemyName]);
return { enemyImage, uploadTeamImage };
};

View File

@@ -1,34 +1,43 @@
// React
import { useEffect, useState, useCallback } from "react";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect, useState } from "react";
export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(initialValue);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchData() {
let isMounted = true;
const fetchData = async () => {
try {
const item = await AsyncStorage.getItem(key);
setStoredValue(item ? JSON.parse(item) : initialValue);
if (isMounted && item !== null) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.log(error);
console.error(`Error loading key "${key}":`, error);
}
setLoading(false);
}
};
fetchData();
}, [initialValue, key]);
return () => { isMounted = false; };
}, [key]);
const setValue = async value => {
const setValue = useCallback(async (value) => {
try {
setLoading(true);
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
await AsyncStorage.setItem(key, JSON.stringify(valueToStore));
setLoading(false);
setStoredValue((prevValue) => {
const valueToStore = value instanceof Function ? value(prevValue) : value;
AsyncStorage.setItem(key, JSON.stringify(valueToStore)).catch(err =>
console.error(`Error saving key "${key}":`, err)
);
return valueToStore;
});
} catch (error) {
console.log(error);
console.error(error);
}
};
}, [key]);
return [storedValue, setValue, loading];
return [storedValue, setValue];
};

View File

@@ -1,14 +1,17 @@
import { useEffect, useState } from "react";
// React
import { useEffect, useState, useCallback, useMemo } from "react";
import { Alert } from "react-native";
// Expo
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
import * as Location from 'expo-location';
import { useSocket } from "../context/socketContext";
// Hook
import { useSocketCommands } from "./useSocketCommands";
export const useLocation = (timeInterval, distanceInterval) => {
const { emitUpdatePosition } = useSocketCommands();
const [location, setLocation] = useState(null); // [latitude, longitude]
const { teamSocket } = useSocket();
const LOCATION_TASK_NAME = "background-location-task";
const locationUpdateParameters = {
const locationUpdateParameters = useMemo(() => ({
accuracy: Location.Accuracy.High,
distanceInterval: distanceInterval, // Update every 10 meters
timeInterval: timeInterval, // Minimum interval in ms
@@ -19,7 +22,7 @@ export const useLocation = (timeInterval, distanceInterval) => {
notificationBody: "L'application utilise votre position en arrière plan.",
notificationColor: "#FF0000", // (Android) Notification icon color
},
};
}), [distanceInterval, timeInterval]);
defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) {
@@ -37,19 +40,14 @@ export const useLocation = (timeInterval, distanceInterval) => {
} catch (e) {
console.warn("setLocation failed (probably in background):", e);
}
console.log("Sending position :", new_location);
teamSocket.emit("update_position", new_location);
emitUpdatePosition(new_location);
} else {
console.log("No location measured.");
}
}
});
useEffect(() => {
getLocationAuthorization();
}, []);
async function getLocationAuthorization() {
const getLocationAuthorization = useCallback(async () => {
const { status : statusForeground } = await Location.requestForegroundPermissionsAsync();
const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync();
if (statusForeground !== "granted" || statusBackground !== "granted") {
@@ -58,23 +56,27 @@ export const useLocation = (timeInterval, distanceInterval) => {
} else {
return true;
}
}
}, []);
async function startLocationTracking() {
const startLocationTracking = useCallback(async () => {
if (await getLocationAuthorization()) {
if (!(await isTaskRegisteredAsync(LOCATION_TASK_NAME))) {
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, locationUpdateParameters);
console.log("Location tracking started.");
}
}
}
}, [getLocationAuthorization, locationUpdateParameters]);
async function stopLocationTracking() {
const stopLocationTracking = useCallback(async () => {
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
console.log("Location tracking stopped.");
}
}
}, []);
useEffect(() => {
getLocationAuthorization();
}, [getLocationAuthorization]);
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
};

View File

@@ -1,11 +1,13 @@
import { useState, } from 'react';
// React
import { useState, useCallback } from 'react';
import { Alert } from 'react-native';
// Expo
import { launchImageLibraryAsync, requestMediaLibraryPermissionsAsync } from 'expo-image-picker';
export const usePickImage = () => {
const [image, setImage] = useState(null);
const pickImage = async () => {
const pickImage = useCallback(async () => {
try {
const permissionResult = await requestMediaLibraryPermissionsAsync();
@@ -13,7 +15,6 @@ export const usePickImage = () => {
Alert.alert("Permission refusée", "Activez l'accès au stockage ou à la gallerie dans les paramètres.");
return;
}
let result = await launchImageLibraryAsync({
mediaTypes: ['images'],
allowsMultipleSelection: false,
@@ -28,31 +29,11 @@ export const usePickImage = () => {
else {
console.log('Image picker cancelled.');
}
}
catch (error) {
} catch (error) {
console.error('Error picking image;', error);
Alert.alert('Erreur', "Une erreur est survenue lors de la sélection d'une image.");
}
};
}, []);
function sendImage(location) {
if (image) {
let data = new FormData();
data.append('file', {
uri: image.uri,
name: 'photo.jpg',
type: 'image/jpeg',
});
fetch(location , {
method: 'POST',
body: data,
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
}
return {image, pickImage, sendImage};
return {image, pickImage};
};

View File

@@ -1,35 +1,47 @@
import { useEffect } from 'react';
// React
import { useEffect, useRef } from 'react';
import DeviceInfo from 'react-native-device-info';
import { useSocket } from "../context/socketContext";
// Context
import { useTeamConnexion } from "../context/teamConnexionContext";
// Hook
import { useSocketCommands } from "./useSocketCommands";
export const useSendDeviceInfo = () => {
const batteryUpdateTimeout = 5*60*1000;
const { teamSocket } = useSocket();
const {loggedIn} = useTeamConnexion();
const { emitBattery, emitDeviceInfo } = useSocketCommands();
const { loggedIn } = useTeamConnexion();
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
if (!loggedIn) return;
const sendInfo = async () => {
const brand = DeviceInfo.getBrand();
const model = DeviceInfo.getModel();
const name = await DeviceInfo.getDeviceName();
teamSocket.emit('device_info', {model: brand + " " + model, name: name});
const [brand, model, name] = await Promise.all([
DeviceInfo.getBrand(),
DeviceInfo.getModel(),
DeviceInfo.getDeviceName()
]);
if (!isMounted) return;
emitDeviceInfo({model: brand + " " + model, name: name});
};
const sendBattery = async () => {
const level = await DeviceInfo.getBatteryLevel();
teamSocket.emit('battery_update', Math.round(level * 100));
if (!isMounted) return;
emitBattery(Math.round(level * 100));
};
sendInfo();
sendBattery();
const batteryCheckInterval = setInterval(() => sendBattery(), batteryUpdateTimeout);
const batteryCheckInterval = setInterval(() => sendBattery(), 5*60*1000); // 5 minutes
return () => clearInterval(batteryCheckInterval);
}, [batteryUpdateTimeout, loggedIn, teamSocket]);
return () => {
isMounted.current = false;
clearInterval(batteryCheckInterval);
};
}, [emitBattery, emitDeviceInfo, loggedIn]);
return null;
};

View File

@@ -1,72 +1,60 @@
import { useEffect, useState } from 'react';
// React
import { useState, useEffect, useCallback, useRef } from 'react';
// Hook
import { useLocalStorage } from './useLocalStorage';
import { useSocketCommands } from "./useSocketCommands";
const LOGIN_MESSAGE = "login";
const LOGOUT_MESSAGE = "logout";
export const useSocketAuth = (socket, passwordName) => {
export const useSocketAuth = () => {
const { emitLogin, emitLogout } = useSocketCommands();
const [loggedIn, setLoggedIn] = useState(false);
const [loading, setLoading] = useState(true);
const [waitingForResponse, setWaitingForResponse] = useState(false);
const [hasTriedSavedPassword, setHasTriedSavedPassword] = useState(false);
const [savedPassword, setSavedPassword, savedPasswordLoading] = useLocalStorage(passwordName, null);
const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null);
const isMounted = useRef(true);
useEffect(() => {
if (!loading && !hasTriedSavedPassword) {
console.log("Try to log in with saved password :", savedPassword);
setWaitingForResponse(true);
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
const timeout = setTimeout(() => {
console.warn("Server did not respond to login emit.");
setWaitingForResponse(false);
}, 3000);
socket.emit(LOGIN_MESSAGE, savedPassword, (response) => {
clearTimeout(timeout);
console.log(response.message);
setLoggedIn(response.isLoggedIn);
setWaitingForResponse(false);
});
setHasTriedSavedPassword(true);
}
}, [hasTriedSavedPassword, loading, savedPassword, socket]);
function login(password) {
console.log("Try to log in with :", password);
setSavedPassword(password);
setWaitingForResponse(true);
const login = useCallback((password) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (!isMounted.current) return;
console.warn("Server did not respond to login emit.");
setWaitingForResponse(false);
reject();
}, 3000);
}, 2000);
socket.emit(LOGIN_MESSAGE, password, (response) => {
emitLogin(password, (response) => {
clearTimeout(timeout);
console.log(response.message);
setLoggedIn(response.isLoggedIn);
setWaitingForResponse(false);
resolve(response);
if (!isMounted.current) return;
if (response.isLoggedIn) {
console.log("Log in accepted.");
setLoggedIn(true);
setSavedPassword(password);
resolve(response);
} else {
console.log("Log in refused.");
setLoggedIn(false);
reject();
}
});
});
}
function logout() {
console.log("Logout");
setSavedPassword(null);
setLoggedIn(false);
socket.emit(LOGOUT_MESSAGE);
}
}, [emitLogin, setSavedPassword]);
useEffect(() => {
if(!waitingForResponse && !savedPasswordLoading) {
setLoading(false);
} else {
setLoading(true);
if (!loggedIn && savedPassword) {
login(savedPassword);
}
}, [waitingForResponse, savedPasswordLoading]);
}, [loggedIn, login, savedPassword]);
return {login, logout, password: savedPassword, loggedIn, loading};
const logout = useCallback(() => {
setLoggedIn(false);
setSavedPassword(null);
emitLogout();
}, [emitLogout, setSavedPassword]);
return {login, logout, password: savedPassword, loggedIn};
};

View File

@@ -0,0 +1,52 @@
// React
import { useCallback } from 'react';
// Context
import { useSocket } from "../context/socketContext";
export const useSocketCommands = () => {
const { teamSocket } = useSocket();
const emitLogin = useCallback((password, callback) => {
if (!teamSocket?.connected) return;
console.log("Try to log in with :", password);
teamSocket.emit("login", password, callback);
}, [teamSocket]);
const emitLogout = useCallback(() => {
if (!teamSocket?.connected) return;
console.log("Logout.");
teamSocket.emit("logout");
}, [teamSocket]);
const emitSendPosition = useCallback(() => {
if (!teamSocket?.connected) return;
console.log("Reveal position.");
teamSocket.emit("send_position");
}, [teamSocket]);
const emitUpdatePosition = useCallback((location) => {
if (!teamSocket?.connected) return;
console.log("Update position :", location);
teamSocket.emit("update_position", location);
}, [teamSocket]);
const emitCapture = useCallback((captureCode, callback) => {
if (!teamSocket?.connected) return;
console.log("Try to capture :", captureCode);
teamSocket.emit("capture", captureCode, callback);
}, [teamSocket]);
const emitBattery = useCallback((level) => {
if (!teamSocket?.connected) return;
console.log("Send battery level.");
teamSocket.emit('battery_update', level);
}, [teamSocket]);
const emitDeviceInfo = useCallback((infos) => {
if (!teamSocket?.connected) return;
console.log("Send device infos.");
teamSocket.emit('device_info', infos);
}, [teamSocket]);
return { emitLogin, emitLogout, emitSendPosition, emitUpdatePosition, emitCapture, emitBattery, emitDeviceInfo };
};

View File

@@ -1,8 +1,9 @@
// React
import { useEffect } from "react";
export const useSocketListener = (socket, event, callback) => {
useEffect(() => {
socket.on(event,callback);
socket.on(event, callback);
return () => {
socket.off(event, callback);
};

View File

@@ -1,3 +1,4 @@
// React
import { useEffect, useState } from "react";
export const useTimeDifference = (refTime, timeout) => {

View File

@@ -1,3 +1,6 @@
export const SERVER_URL = process.env.EXPO_PUBLIC_SERVER_URL;
export const SOCKET_URL = process.env.EXPO_PUBLIC_SOCKET_URL;
export const InitialRegions = {
paris : {
latitude: 48.864,
@@ -10,4 +13,4 @@ export const InitialRegions = {
export const ZoneTypes = {
circle: "circle",
polygon: "polygon"
};
};