Refactoring

This commit is contained in:
Sebastien Riviere
2026-02-18 10:57:08 +01:00
parent 4d8dcd241c
commit 776bbcd723
23 changed files with 191 additions and 265 deletions

View File

@@ -1,16 +1,16 @@
// Expo
import { Slot } from 'expo-router';
// Contexts
import { TeamConnexionProvider } from "../src/context/teamConnexionContext";
import { TeamProvider } from "../src/context/teamContext";
import { AuthProvider } from "../src/contexts/authContext";
import { TeamProvider } from "../src/contexts/teamContext";
const Layout = () => {
return (
<TeamConnexionProvider>
<AuthProvider>
<TeamProvider>
<Slot/>
</TeamProvider>
</TeamConnexionProvider>
</AuthProvider>
);
};

View File

@@ -8,19 +8,19 @@ import { CustomButton } from '../src/components/button';
import { CustomImage } from '../src/components/image';
import { CustomTextInput } from '../src/components/input';
// Contexts
import { useTeamConnexion } from "../src/context/teamConnexionContext";
import { useAuth } from "../src/contexts/authContext";
// Hooks
import { usePickImage } from '../src/hook/usePickImage';
import { usePickImage } from '../src/hooks/usePickImage';
// Services
import { uploadTeamImage } from '../src/services/imageService';
import { getLocationAuthorization, stopLocationTracking } from '../src/services/backgroundLocationTask';
import { uploadTeamImage } from '../src/services/api/image';
import { getLocationAuthorization, stopLocationTracking } from '../src/services/tasks/backgroundLocation';
// Constants
import { COLORS } from '../src/constants';
const Index = () => {
const router = useRouter();
const { login, loggedIn } = useTeamConnexion();
const {image, pickImage} = usePickImage();
const { loggedIn, login } = useAuth();
const { image, pickImage } = usePickImage();
const [teamId, setTeamId] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);

View File

@@ -8,24 +8,23 @@ import { CustomMap } from '../src/components/map';
import { Drawer } from '../src/components/drawer';
import { TimerMMSS } from '../src/components/timer';
// Contexts
import { useTeamConnexion } from '../src/context/teamConnexionContext';
import { useTeamContext } from '../src/context/teamContext';
import { useAuth } from '../src/contexts/authContext';
import { useTeam } from '../src/contexts/teamContext';
// Hooks
import { useGame } from '../src/hook/useGame';
import { useTimeDifference } from '../src/hook/useTimeDifference';
import { useTimeDifference } from '../src/hooks/useTimeDifference';
// Services
import { startLocationTracking } from '../src/services/backgroundLocationTask';
import { emitSendPosition } from '../src/services/socket/emitters';
import { startLocationTracking } from '../src/services/tasks/backgroundLocation';
// Util
import { secondsToMMSS } from '../src/util/functions';
import { secondsToMMSS } from '../src/utils/functions';
// Constants
import { GAME_STATE, COLORS } from '../src/constants';
const Interface = () => {
const router = useRouter();
const {teamInfos, messages, nextZoneDate, isShrinking, gameState} = useTeamContext();
const {teamInfos, messages, nextZoneDate, gameState} = useTeam();
const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos;
const {loggedIn, logout} = useTeamConnexion();
const {sendCurrentPosition} = useGame();
const { loggedIn, logout } = useAuth();
const [timeLeftSendLocation] = useTimeDifference(locationSendDeadline, 1000);
const [timeLeftNextZone] = useTimeDifference(nextZoneDate, 1000);
const [timeLeftOutOfZone] = useTimeDifference(outOfZoneDeadline, 1000);
@@ -87,7 +86,7 @@ const Interface = () => {
</View>
}
{ gameState == GAME_STATE.PLAYING && !captured && <Fragment>
<TimerMMSS style={{width: "50%"}} title={isShrinking ? "Réduction de la zone" : "Durée de la zone"} seconds={-timeLeftNextZone} />
<TimerMMSS style={{width: "50%"}} title={"Réduction de la zone dans"} seconds={-timeLeftNextZone} />
<TimerMMSS style={{width: "50%"}} title={"Position envoyée dans"} seconds={!hasHandicap ? -timeLeftSendLocation: 0} />
</Fragment>}
</View>
@@ -98,7 +97,7 @@ const Interface = () => {
<View style={styles.bottomContainer} onLayout={(event) => setBottomContainerHeight(event.nativeEvent.layout.height)}>
<CustomMap/>
{ gameState == GAME_STATE.PLAYING && !captured && !hasHandicap &&
<TouchableOpacity style={styles.updatePosition} onPress={sendCurrentPosition}>
<TouchableOpacity style={styles.updatePosition} onPress={emitSendPosition}>
<Image source={require("../src/assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
}

View File

@@ -8,25 +8,24 @@ import { CustomImage } from './image';
import { CustomTextInput } from './input';
import { Stat } from './stat';
// Contexts
import { useTeamConnexion } from '../context/teamConnexionContext';
import { useTeamContext } from '../context/teamContext';
import { useAuth } from '../contexts/authContext';
import { useTeam } from '../contexts/teamContext';
// Hooks
import { useTimeDifference } from '../hook/useTimeDifference';
import { useGame } from '../hook/useGame';
import { useTimeDifference } from '../hooks/useTimeDifference';
// Services
import { enemyImage } from '../services/imageService';
import { emitCapture } from '../services/socket/emitters';
import { enemyImage } from '../services/api/image';
// Util
import { secondsToHHMMSS } from '../util/functions';
import { secondsToHHMMSS } from '../utils/functions';
// Constants
import { GAME_STATE, COLORS } from '../constants';
export const Drawer = ({ height }) => {
const { teamId } = useTeamConnexion();
const { teamId } = useAuth();
const [collapsibleState, setCollapsibleState] = useState(true);
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const {teamInfos, gameState, startDate} = useTeamContext();
const {teamInfos, gameState, startDate} = useTeam();
const {enemyName, captureCode, name, distance, finishDate, nCaptures, nSentLocation, hasHandicap} = teamInfos;
const {capture} = useGame();
const [timeSinceStart] = useTimeDifference(startDate, 1000);
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"};
@@ -53,7 +52,7 @@ export const Drawer = ({ height }) => {
const handleCapture = () => {
if (captureStatus != 1) {
setCaptureStatus(1);
capture(enemyCaptureCode)
emitCapture(enemyCaptureCode)
.then((response) => {
if (response.hasCaptured) {
setCaptureStatus(3);

View File

@@ -1,7 +1,7 @@
// React
import { Fragment } from 'react';
import { Polygon } from 'react-native-maps';
import { circleToPolygon } from '../util/functions';
import { circleToPolygon } from '../utils/functions';
export const InvertedPolygon = ({id, coordinates, fillColor}) => {
// We create 3 rectangles covering earth, with the first rectangle centered on the hole

View File

@@ -6,15 +6,15 @@ import LinearGradient from 'react-native-linear-gradient';
// Components
import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer';
// Contexts
import { useTeamContext } from '../context/teamContext';
import { useTeam } from '../contexts/teamContext';
// Hook
import { useLocation } from '../hook/useLocation';
import { useLocation } from '../hooks/useLocation';
// Util
import { ZONE_TYPES, INITIAL_REGIONS, GAME_STATE } from '../constants';
export const CustomMap = () => {
const { location } = useLocation();
const {teamInfos, zoneType, zoneExtremities, gameState} = useTeamContext();
const {teamInfos, zoneType, zoneExtremities, gameState} = useTeam();
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos;
const [centerMap, setCenterMap] = useState(true);
const mapRef = useRef(null);

View File

@@ -1,7 +1,7 @@
// React
import { View, Text, StyleSheet } from 'react-native';
// Util
import { secondsToMMSS } from '../util/functions';
import { secondsToMMSS } from '../utils/functions';
export const TimerMMSS = ({ title, seconds, style }) => {
return (

View File

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

View File

@@ -0,0 +1,80 @@
// React
import { createContext, useContext, useState, useEffect, useCallback, useMemo } from "react";
import DeviceInfo from 'react-native-device-info';
// Hook
import { useLocalStorage } from '../hooks/useLocalStorage';
// Services
import { emitLogin, emitLogout, emitBattery, emitDeviceInfo } from "../services/socket/emitters";
const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [loggedIn, setLoggedIn] = useState(false);
const [teamId, setTeamId] = useLocalStorage("team_id", null);
const login = useCallback(async (password) => {
if (loggedIn != false) return;
try {
const response = await emitLogin(password);
setLoggedIn(response.isLoggedIn);
if (response.isLoggedIn) setTeamId(password);
return response.isLoggedIn;
} catch (error) {
setLoggedIn(false);
throw error;
}
}, [loggedIn, setTeamId]);
const logout = useCallback(() => {
if (loggedIn != true) return;
setLoggedIn(false);
setTeamId(null);
emitLogout();
}, [loggedIn, setTeamId]);
// Try to log in with saved teamId
useEffect(() => {
if (!loggedIn && teamId) {
login(teamId);
}
}, [loggedIn, teamId, login]);
// Emit battery level and phone model at log in
useEffect(() => {
if (!loggedIn) return;
const sendInfo = async () => {
const [brand, model, name] = await Promise.all([
DeviceInfo.getBrand(),
DeviceInfo.getModel(),
DeviceInfo.getDeviceName()
]);
emitDeviceInfo({model: brand + " " + model, name: name});
};
const sendBattery = async () => {
const level = await DeviceInfo.getBatteryLevel();
emitBattery(Math.round(level * 100));
};
sendInfo();
sendBattery();
const batteryCheckInterval = setInterval(() => sendBattery(), 5*60*1000); // 5 minutes
return () => clearInterval(batteryCheckInterval);
}, [loggedIn]);
const value = useMemo(() => ({ teamId, loggedIn, login, logout}), [teamId, loggedIn, login, logout]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
return useContext(AuthContext);
};

View File

@@ -1,26 +1,25 @@
// React
import { createContext, useContext, useMemo, useState, useEffect } from "react";
// Context
import { useTeamConnexion } from "./teamConnexionContext";
// Hook
import { useSendDeviceInfo } from "../hook/useSendDeviceInfo";
import { useAuth } from "./authContext";
// Services
import { socket } from "../services/socket";
import { socket } from "../services/socket/connection";
// Constants
import { GAME_STATE } from "../constants";
const TeamContext = createContext();
const useSocketListener = (event, callback) => {
const useOnEvent = (event, callback) => {
useEffect(() => {
socket.on(event, callback);
return () => {
socket.off(event, callback);
};
}, [callback, event]);
}, [event, callback]);
};
export const TeamProvider = ({children}) => {
const { logout } = useAuth();
// update_team
const [teamInfos, setTeamInfos] = useState({});
// game_state
@@ -32,21 +31,17 @@ export const TeamProvider = ({children}) => {
// settings
const [messages, setMessages] = useState(null);
const [zoneType, setZoneType] = useState(null);
// logout
const { logout } = useTeamConnexion();
useSendDeviceInfo();
useSocketListener("update_team", (data) => {
useOnEvent("update_team", (data) => {
setTeamInfos(teamInfos => ({...teamInfos, ...data}));
});
useSocketListener("game_state", (data) => {
useOnEvent("game_state", (data) => {
setGAME_STATE(data.state);
setStartDate(data.date);
});
useSocketListener("settings", (data) => {
useOnEvent("settings", (data) => {
setMessages(data.messages);
setZoneType(data.zone.type);
//TODO
@@ -54,12 +49,12 @@ export const TeamProvider = ({children}) => {
//setOutOfZoneDelay(data.outOfZoneDelay);
});
useSocketListener("current_zone", (data) => {
useOnEvent("current_zone", (data) => {
setZoneExtremities({begin: data.begin, end: data.end});
setNextZoneDate(data.endDate);
});
useSocketListener("logout", logout);
useOnEvent("logout", logout);
const value = useMemo(() => (
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages}
@@ -72,6 +67,6 @@ export const TeamProvider = ({children}) => {
);
};
export const useTeamContext = () => {
export const useTeam = () => {
return useContext(TeamContext);
};

View File

@@ -1,27 +0,0 @@
// React
import { useCallback } from "react";
// Services
import { emitSendPosition, emitCapture } from "../services/socketEmitter";
export const useGame = () => {
const sendCurrentPosition = useCallback(() => {
emitSendPosition();
}, []);
const capture = useCallback((captureCode) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
console.warn("Server timeout: capture", captureCode);
reject(new Error("Timeout"));
}, 3000);
emitCapture(captureCode, (response) => {
clearTimeout(timeout);
resolve(response);
});
});
}, []);
return { sendCurrentPosition, capture };
};

View File

@@ -1,46 +0,0 @@
// React
import { useEffect, useRef } from 'react';
import DeviceInfo from 'react-native-device-info';
// Context
import { useTeamConnexion } from "../context/teamConnexionContext";
// Services
import { emitBattery, emitDeviceInfo } from "../services/socketEmitter";
export const useSendDeviceInfo = () => {
const { loggedIn } = useTeamConnexion();
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
if (!loggedIn) return;
const sendInfo = async () => {
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();
if (!isMounted) return;
emitBattery(Math.round(level * 100));
};
sendInfo();
sendBattery();
const batteryCheckInterval = setInterval(() => sendBattery(), 5*60*1000); // 5 minutes
return () => {
isMounted.current = false;
clearInterval(batteryCheckInterval);
};
}, [loggedIn]);
return null;
};

View File

@@ -1,60 +0,0 @@
// React
import { useState, useEffect, useCallback, useRef } from 'react';
// Hook
import { useLocalStorage } from './useLocalStorage';
// Services
import { emitLogin, emitLogout } from "../services/socketEmitter";
export const useSocketAuth = () => {
const [loggedIn, setLoggedIn] = useState(false);
const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null);
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
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.");
reject();
}, 2000);
emitLogin(password, (response) => {
clearTimeout(timeout);
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();
}
});
});
}, [setSavedPassword]);
useEffect(() => {
if (!loggedIn && savedPassword) {
login(savedPassword);
}
}, [loggedIn, login, savedPassword]);
const logout = useCallback(() => {
setLoggedIn(false);
setSavedPassword(null);
emitLogout();
}, [setSavedPassword]);
return {login, logout, password: savedPassword, loggedIn};
};

View File

@@ -6,21 +6,16 @@ export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(initialValue);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
const item = await AsyncStorage.getItem(key);
if (isMounted && item !== null) {
setStoredValue(JSON.parse(item));
}
if (item !== null) setStoredValue(JSON.parse(item));
} catch (error) {
console.error(`Error loading key "${key}":`, error);
}
};
fetchData();
return () => { isMounted = false; };
}, [key]);
const setValue = useCallback(async (value) => {

View File

@@ -35,5 +35,5 @@ export const usePickImage = () => {
}
}, []);
return {image, pickImage};
return { image, pickImage };
};

View File

@@ -1,10 +1,12 @@
// React
import { useEffect, useState } from "react";
/**
* If refTime is in the past, time will be positive
* If refTime is in the future, time will be negative
* The time is updated every timeout milliseconds
*/
export const useTimeDifference = (refTime, timeout) => {
// If refTime is in the past, time will be positive
// If refTime is in the future, time will be negative
// The time is updated every timeout milliseconds
const [time, setTime] = useState(0);
useEffect(() => {

View File

@@ -1,5 +1,5 @@
// Constants
import { SERVER_URL } from "../constants";
import { SERVER_URL } from "../../constants";
export const uploadTeamImage = async (id, imageUri) => {
if (!imageUri || !id) return;

View File

@@ -1,7 +1,7 @@
// Socket
import { io } from "socket.io-client";
// Constants
import { SOCKET_URL } from "../constants";
import { SOCKET_URL } from "../../constants";
export const socket = io(SOCKET_URL, {
path: "/back/socket.io"

View File

@@ -0,0 +1,55 @@
// Services
import { socket } from "./connection";
const customEmit = (event, ...args) => {
if (!socket?.connected) return false;
console.log("Emit", event);
socket.emit(event, ...args);
return true;
};
const customEmitCallback = (event, ...args) => {
return new Promise((resolve, reject) => {
if (!socket?.connected) return reject(new Error("Socket not connected"));
console.log("Emit", event);
const timeout = setTimeout(() => {
console.warn("Server timeout");
reject(new Error("Timeout"));
}, 3000);
socket.emit(event, ...args, (response) => {
clearTimeout(timeout);
resolve(response.length > 1 ? response : response[0]);
});
});
};
export const emitLogin = (password) => {
return customEmitCallback("login", password);
};
export const emitLogout = () => {
return customEmit("logout");
};
export const emitSendPosition = () => {
return customEmit("send_position");
};
export const emitUpdatePosition = (location) => {
return customEmit("update_position", location);
};
export const emitCapture = (captureCode) => {
return customEmitCallback("capture", captureCode);
};
export const emitBattery = (level) => {
return customEmit("battery_update", level);
};
export const emitDeviceInfo = (infos) => {
return customEmit("device_info", infos);
};

View File

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

View File

@@ -2,9 +2,9 @@
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
import * as Location from 'expo-location';
// Services
import { emitUpdatePosition } from "./socketEmitter";
import { emitUpdatePosition } from "../socket/emitters";
// Constants
import { TASKS, LOCATION_PARAMETERS } from "../constants";
import { TASKS, LOCATION_PARAMETERS } from "../../constants";
// Task