Fix background task + socket in services + cleaning

This commit is contained in:
Sebastien Riviere
2026-02-17 23:48:42 +01:00
parent 05a60612c6
commit 2dfddd86e6
25 changed files with 301 additions and 318 deletions

View File

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

View File

@@ -9,34 +9,33 @@ import { CustomImage } from '../components/image';
import { CustomTextInput } from '../components/input';
// Contexts
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';
// Services
import { uploadTeamImage } from '../services/imageService';
import { getLocationAuthorization, stopLocationTracking } from '../services/backgroundLocationTask';
// Constants
import { COLORS } from '../constants';
const Index = () => {
const router = useRouter();
const {login, loggedIn} = useTeamConnexion();
const {getLocationAuthorization, stopLocationTracking} = useTeamContext();
const { login, loggedIn } = useTeamConnexion();
const {image, pickImage} = usePickImage();
const [teamId, setTeamId] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const { uploadTeamImage } = useImageApi();
// Disbaling location tracking
// Disbaling location tracking and asking permissions
useEffect(() => {
stopLocationTracking();
}, [stopLocationTracking]);
getLocationAuthorization();
}, []);
// Routeur
useEffect(() => {
if (loggedIn) {
uploadTeamImage(image?.uri);
router.replace("/interface");
}
}, [router, loggedIn, uploadTeamImage, image]);
}, [router, loggedIn, image]);
const handleSubmit = async () => {
if (isSubmitting || !getLocationAuthorization()) return;
@@ -53,6 +52,7 @@ const Index = () => {
const response = await login(teamId);
if (response.isLoggedIn) {
uploadTeamImage(teamId, image?.uri);
setTeamId("");
} else {
setTimeout(() => Alert.alert("Échec", "L'ID d'équipe est inconnu."), 100);
@@ -94,7 +94,7 @@ const styles = StyleSheet.create({
flexGrow: 1,
alignItems: 'center',
paddingVertical: 20,
backgroundColor: Colors.background
backgroundColor: COLORS.background
},
transitionContainer: {
flexGrow: 1,

View File

@@ -6,21 +6,23 @@ import { useRouter } from 'expo-router';
// Components
import { CustomMap } from '../components/map';
import { Drawer } from '../components/drawer';
import { TimerMMSS } from '../components/timer';
// Contexts
import { useTeamConnexion } from '../context/teamConnexionContext';
import { useTeamContext } from '../context/teamContext';
// Hooks
import { useGame } from '../hook/useGame';
import { useTimeDifference } from '../hook/useTimeDifference';
// Services
import { startLocationTracking } from '../services/backgroundLocationTask';
// Util
import { GameState } from '../util/gameState';
import { TimerMMSS } from '../components/timer';
import { secondsToMMSS } from '../util/functions';
import { Colors } from '../util/colors';
// Constants
import { GAME_STATE, COLORS } from '../constants';
const Interface = () => {
const router = useRouter();
const {teamInfos, messages, nextZoneDate, isShrinking, startLocationTracking, stopLocationTracking, gameState} = useTeamContext();
const {teamInfos, messages, nextZoneDate, isShrinking, gameState} = useTeamContext();
const {name, ready, captured, locationSendDeadline, outOfZone, outOfZoneDeadline, hasHandicap, enemyHasHandicap} = teamInfos;
const {loggedIn, logout} = useTeamConnexion();
const {sendCurrentPosition} = useGame();
@@ -31,16 +33,16 @@ const Interface = () => {
const statusMessage = useMemo(() => {
switch (gameState) {
case GameState.SETUP:
case GAME_STATE.SETUP:
return messages?.waiting || "Préparation de la partie";
case GameState.PLACEMENT:
case GAME_STATE.PLACEMENT:
return "Phase de placement";
case GameState.PLAYING:
case GAME_STATE.PLAYING:
if (captured) return messages?.captured || "Vous avez été éliminé...";
if (!outOfZone) return "La partie est en cours";
if (!hasHandicap) return `Veuillez retourner dans la zone\nHandicap dans ${secondsToMMSS(-timeLeftOutOfZone)}`;
else return `Veuillez retourner dans la zone\nVotre position est révélée en continue`;
case GameState.FINISHED:
case GAME_STATE.FINISHED:
return `Vous avez ${captured ? (messages?.loser || "perdu...") : (messages?.winner || "gagné !")}`;
default:
return "Inconnue";
@@ -56,12 +58,8 @@ const Interface = () => {
// Activating geolocation tracking
useEffect(() => {
if (loggedIn) {
startLocationTracking();
} else {
stopLocationTracking();
}
}, [startLocationTracking, stopLocationTracking, loggedIn]);
}, []);
return (
<View style={styles.globalContainer}>
@@ -83,12 +81,12 @@ const Interface = () => {
</TouchableOpacity>
</View>
<View style={styles.infoContainer}>
{ gameState == GameState.PLACEMENT &&
{ gameState == GAME_STATE.PLACEMENT &&
<View style={[styles.readyIndicator, {backgroundColor: ready ? "#3C3" : "#C33"}]}>
<Text style={{color: '#fff', fontSize: 16}}>{ready ? "Placé" : "Non placé"}</Text>
</View>
}
{ gameState == GameState.PLAYING && !captured && <Fragment>
{ 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={"Position envoyée dans"} seconds={!hasHandicap ? -timeLeftSendLocation: 0} />
</Fragment>}
@@ -99,12 +97,12 @@ const Interface = () => {
</View>
<View style={styles.bottomContainer} onLayout={(event) => setBottomContainerHeight(event.nativeEvent.layout.height)}>
<CustomMap/>
{ gameState == GameState.PLAYING && !captured && !hasHandicap &&
{ gameState == GAME_STATE.PLAYING && !captured && !hasHandicap &&
<TouchableOpacity style={styles.updatePosition} onPress={sendCurrentPosition}>
<Image source={require("../assets/images/update_position.png")} style={{width: 40, height: 40}} resizeMode="contain"></Image>
</TouchableOpacity>
}
{ gameState == GameState.PLAYING && !captured &&
{ gameState == GAME_STATE.PLAYING && !captured &&
<Drawer height={bottomContainerHeight}/>
}
</View>
@@ -116,7 +114,7 @@ export default Interface;
const styles = StyleSheet.create({
globalContainer: {
backgroundColor: Colors.background,
backgroundColor: COLORS.background,
flex: 1,
},
topContainer: {

View File

@@ -8,17 +8,20 @@ import { CustomImage } from './image';
import { CustomTextInput } from './input';
import { Stat } from './stat';
// Contexts
import { useTeamConnexion } from '../context/teamConnexionContext';
import { useTeamContext } from '../context/teamContext';
// Hooks
import { useTimeDifference } from '../hook/useTimeDifference';
import { useGame } from '../hook/useGame';
// Services
import { enemyImage } from '../services/imageService';
// Util
import { GameState } from '../util/gameState';
import { Colors } from '../util/colors';
import { secondsToHHMMSS } from '../util/functions';
import { useImageApi } from '../hook/useImageApi';
// Constants
import { GAME_STATE, COLORS } from '../constants';
export const Drawer = ({ height }) => {
const { teamId } = useTeamConnexion();
const [collapsibleState, setCollapsibleState] = useState(true);
const [enemyCaptureCode, setEnemyCaptureCode] = useState("");
const {teamInfos, gameState, startDate} = useTeamContext();
@@ -27,7 +30,6 @@ export const Drawer = ({ height }) => {
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"};
const { enemyImage } = useImageApi();
const avgSpeed = useMemo(() => {
const hours = (finishDate ? (finishDate - startDate) : timeSinceStart*1000) / 1000 / 3600;
@@ -76,13 +78,13 @@ export const Drawer = ({ height }) => {
</TouchableHighlight>
<Collapsible style={[styles.collapsibleWindow, {height: height - 44}]} title="Collapse" collapsed={collapsibleState}>
<ScrollView contentContainerStyle={styles.collapsibleContent}>
{ gameState == GameState.PLAYING &&
{ gameState == GAME_STATE.PLAYING &&
<Text style={{fontSize: 22, fontWeight: "bold", textAlign: "center"}}>Code de {(name ?? "Indisponible")} : {String(captureCode).padStart(4,"0")}</Text>
}
{ gameState == GameState.PLAYING && !hasHandicap && <Fragment>
{ gameState == GAME_STATE.PLAYING && !hasHandicap && <Fragment>
<View style={styles.imageContainer}>
<Text style={{fontSize: 15, margin: 5}}>{"Cible (" + (enemyName ?? "Indisponible") + ")"}</Text>
<CustomImage source={enemyImage} canZoom/>
<CustomImage source={enemyImage(teamId)} canZoom/>
</View>
<View style={styles.actionsContainer}>
<View style={styles.actionsLeftContainer}>
@@ -122,7 +124,7 @@ const styles = StyleSheet.create({
},
innerDrawerContainer: {
width: "100%",
backgroundColor: Colors.background,
backgroundColor: COLORS.background,
borderTopLeftRadius: 30,
borderTopRightRadius: 30,
overflow: 'hidden',
@@ -136,7 +138,7 @@ const styles = StyleSheet.create({
collapsibleWindow: {
width: "100%",
justifyContent: 'center',
backgroundColor: Colors.background,
backgroundColor: COLORS.background,
},
collapsibleContent: {
paddingHorizontal: 15,

View File

@@ -7,20 +7,22 @@ import LinearGradient from 'react-native-linear-gradient';
import { DashedCircle, InvertedCircle, InvertedPolygon } from './layer';
// Contexts
import { useTeamContext } from '../context/teamContext';
// Hook
import { useLocation } from '../hook/useLocation';
// Util
import { GameState } from '../util/gameState';
import { ZoneTypes, InitialRegions } from '../util/constants';
import { ZONE_TYPES, INITIAL_REGIONS, GAME_STATE } from '../constants';
export const CustomMap = () => {
const {teamInfos, zoneType, zoneExtremities, location, gameState} = useTeamContext();
const { location } = useLocation();
const {teamInfos, zoneType, zoneExtremities, gameState} = useTeamContext();
const {enemyLocation, startingArea, lastSentLocation, hasHandicap} = teamInfos;
const [centerMap, setCenterMap] = useState(true);
const mapRef = useRef(null);
// Center the map on user position
useEffect(() => {
if (centerMap && mapRef.current && location) {
mapRef.current.animateToRegion({latitude: location[0], longitude: location[1], latitudeDelta: 0, longitudeDelta: 0.02}, 1000);
if (centerMap && location && mapRef.current) {
mapRef.current.animateToRegion({...location, latitudeDelta: 0, longitudeDelta: 0.02}, 1000);
}
}, [centerMap, location]);
@@ -30,7 +32,7 @@ export const CustomMap = () => {
const latToLatitude = (pos) => ({latitude: pos.lat, longitude: pos.lng});
const startZone = useMemo(() => {
if (gameState != GameState.PLACEMENT || !startingArea) return null;
if (gameState != GAME_STATE.PLACEMENT || !startingArea) return null;
return (
<Circle key="start-zone" center={{ latitude: startingArea.center.lat, longitude: startingArea.center.lng }} radius={startingArea.radius} strokeWidth={2} strokeColor={`rgba(0, 0, 255, 1)`} fillColor={`rgba(0, 0, 255, 0.2)`}/>
@@ -38,7 +40,7 @@ export const CustomMap = () => {
}, [gameState, startingArea]);
const gameZone = useMemo(() => {
if (gameState !== GameState.PLAYING || !zoneExtremities) return null;
if (gameState !== GAME_STATE.PLAYING || !zoneExtremities) return null;
const items = [];
@@ -47,7 +49,7 @@ export const CustomMap = () => {
const strokeWidth = 3;
const lineDashPattern = [30, 10];
if (zoneType === ZoneTypes.circle) {
if (zoneType === ZONE_TYPES.CIRCLE) {
if (zoneExtremities.begin) items.push(
<InvertedCircle
key="game-zone-begin-circle"
@@ -68,7 +70,7 @@ export const CustomMap = () => {
lineDashPattern={lineDashPattern}
/>
);
} else if (zoneType === ZoneTypes.polygon) {
} else if (zoneType === ZONE_TYPES.POLYGON) {
if (zoneExtremities.begin) items.push(
<InvertedPolygon
key="game-zone-begin-poly"
@@ -95,14 +97,14 @@ export const CustomMap = () => {
if (!location) return null;
return (
<Marker key={"current-position-marker"} coordinate={{ latitude: location[0], longitude: location[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position actuelle", "Ceci est votre position")}>
<Marker key={"current-position-marker"} coordinate={location} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position actuelle", "Ceci est votre position")}>
<Image source={require("../assets/images/marker/blue.png")} style={styles.markerImage} resizeMode="contain"/>
</Marker>
);
}, [location]);
const lastPositionMarker = useMemo(() => {
if (gameState != GameState.PLAYING || !lastSentLocation || hasHandicap) return null;
if (gameState != GAME_STATE.PLAYING || !lastSentLocation || hasHandicap) return null;
return (
<Marker key={"last-position-marker"} coordinate={{ latitude: lastSentLocation[0], longitude: lastSentLocation[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position envoyée", "Ceci est votre dernière position connue par le serveur")}>
@@ -112,7 +114,7 @@ export const CustomMap = () => {
}, [gameState, hasHandicap, lastSentLocation]);
const enemyPositionMarker = useMemo(() => {
if (gameState != GameState.PLAYING || !enemyLocation || hasHandicap) return null;
if (gameState != GAME_STATE.PLAYING || !enemyLocation || hasHandicap) return null;
return (
<Marker key={"enemy-position-marker"} coordinate={{ latitude: enemyLocation[0], longitude: enemyLocation[1] }} anchor={{ x: 0.33, y: 0.33 }} onPress={() => Alert.alert("Position ennemie", "Ceci est la dernière position de vos ennemis connue")}>
@@ -124,7 +126,7 @@ export const CustomMap = () => {
return (
<View style={styles.container}>
<MapView ref={mapRef} style={{flex: 1}} initialRegion={InitialRegions.paris} mapType="standard" onTouchMove={() => setCenterMap(false)} toolbarEnabled={false}>
<MapView ref={mapRef} style={{flex: 1}} initialRegion={INITIAL_REGIONS.PARIS} mapType="standard" onTouchMove={() => setCenterMap(false)} toolbarEnabled={false}>
{startZone}
{gameZone}
{currentPositionMarker}

View File

@@ -1,3 +1,3 @@
export const Colors = {
export const COLORS = {
background: '#f5f5f5'
};

View File

@@ -0,0 +1,2 @@
export const SERVER_URL = process.env.EXPO_PUBLIC_SERVER_URL;
export const SOCKET_URL = process.env.EXPO_PUBLIC_SOCKET_URL;

View File

@@ -0,0 +1,11 @@
export const GAME_STATE = {
SETUP: "setup",
PLACEMENT: "placement",
PLAYING: "playing",
FINISHED: "finished"
};
export const ZONE_TYPES = {
CIRCLE: "circle",
POLYGON: "polygon"
};

View File

@@ -0,0 +1,4 @@
export * from './config';
export * from './game';
export * from './map';
export * from './colors';

View File

@@ -0,0 +1,32 @@
export const INITIAL_REGIONS = {
PARIS : {
latitude: 48.864,
longitude: 2.342,
latitudeDelta: 0,
longitudeDelta: 50
}
};
export const LOCATION_PARAMETERS = {
LOCAL: {
accuracy: 4, // High
distanceInterval: 3, // meters
timeInterval: 1000, // ms
},
SERVER: {
accuracy: 4, // High
distanceInterval: 5, // meters
timeInterval: 5000, // ms
showsBackgroundLocationIndicator: true, // iOS only
pausesUpdatesAutomatically: false, // (iOS) Prevents auto-pausing of location updates
foregroundService: {
notificationTitle: "Enregistrement de votre position.",
notificationBody: "L'application utilise votre position en arrière plan.",
notificationColor: "#FF0000", // (Android) Notification icon color
},
}
};
export const TASKS = {
BACKGROUND_LOCATION: "background-location-task"
};

View File

@@ -1,22 +0,0 @@
// React
import { createContext, useContext, useMemo } from "react";
// IO
import { io } from "socket.io-client";
// Util
import { SOCKET_URL } from "../util/constants";
const SocketContext = createContext();
const teamSocket = io(SOCKET_URL, {path: "/back/socket.io"});
export const SocketProvider = ({ children }) => {
const value = useMemo(() => ({ teamSocket }), []);
return (
<SocketContext.Provider value={value}>{children}</SocketContext.Provider>
);
};
export const useSocket = () => {
return useContext(SocketContext);
};

View File

@@ -1,24 +1,30 @@
// React
import { createContext, useContext, useMemo, useState } from "react";
import { createContext, useContext, useMemo, useState, useEffect } from "react";
// Context
import { useSocket } from "./socketContext";
import { useTeamConnexion } from "./teamConnexionContext";
// Hook
import { useSendDeviceInfo } from "../hook/useSendDeviceInfo";
import { useLocation } from "../hook/useLocation";
import { useSocketListener } from "../hook/useSocketListener";
// Util
import { GameState } from "../util/gameState";
// Services
import { socket } from "../services/socket";
// Constants
import { GAME_STATE } from "../constants";
const TeamContext = createContext();
const useSocketListener = (event, callback) => {
useEffect(() => {
socket.on(event, callback);
return () => {
socket.off(event, callback);
};
}, [callback, event]);
};
export const TeamProvider = ({children}) => {
const {teamSocket} = useSocket();
const [location, getLocationAuthorization, startLocationTracking, stopLocationTracking] = useLocation(5000, 10);
// update_team
const [teamInfos, setTeamInfos] = useState({});
// game_state
const [gameState, setGameState] = useState(GameState.SETUP);
const [gameState, setGAME_STATE] = useState(GAME_STATE.SETUP);
const [startDate, setStartDate] = useState(null);
// current_zone
const [zoneExtremities, setZoneExtremities] = useState(null);
@@ -31,16 +37,16 @@ export const TeamProvider = ({children}) => {
useSendDeviceInfo();
useSocketListener(teamSocket, "update_team", (data) => {
useSocketListener("update_team", (data) => {
setTeamInfos(teamInfos => ({...teamInfos, ...data}));
});
useSocketListener(teamSocket, "game_state", (data) => {
setGameState(data.state);
useSocketListener("game_state", (data) => {
setGAME_STATE(data.state);
setStartDate(data.date);
});
useSocketListener(teamSocket, "settings", (data) => {
useSocketListener("settings", (data) => {
setMessages(data.messages);
setZoneType(data.zone.type);
//TODO
@@ -48,16 +54,16 @@ export const TeamProvider = ({children}) => {
//setOutOfZoneDelay(data.outOfZoneDelay);
});
useSocketListener(teamSocket, "current_zone", (data) => {
useSocketListener("current_zone", (data) => {
setZoneExtremities({begin: data.begin, end: data.end});
setNextZoneDate(data.endDate);
});
useSocketListener(teamSocket, "logout", logout);
useSocketListener("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]);
{teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages}
), [teamInfos, gameState, startDate, zoneType, zoneExtremities, nextZoneDate, messages]);
return (
<TeamContext.Provider value={value}>

View File

@@ -1,14 +1,13 @@
// React
import { useCallback } from "react";
// Hook
import { useSocketCommands } from "./useSocketCommands";
// Services
import { emitSendPosition, emitCapture } from "../services/socketEmitter";
export const useGame = () => {
const { emitSendPosition, emitCapture } = useSocketCommands();
const sendCurrentPosition = useCallback(() => {
emitSendPosition();
}, [emitSendPosition]);
}, []);
const capture = useCallback((captureCode) => {
return new Promise((resolve, reject) => {
@@ -22,7 +21,7 @@ export const useGame = () => {
resolve(response);
});
});
}, [emitCapture]);
}, []);
return { sendCurrentPosition, capture };
};

View File

@@ -1,45 +0,0 @@
// 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,82 +1,33 @@
// React
import { useEffect, useState, useCallback, useMemo } from "react";
import { Alert } from "react-native";
import { useState, useEffect } from 'react';
// Expo
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
import * as Location from 'expo-location';
// Hook
import { useSocketCommands } from "./useSocketCommands";
// Constants
import { LOCATION_PARAMETERS } from '../constants';
export const useLocation = (timeInterval, distanceInterval) => {
const { emitUpdatePosition } = useSocketCommands();
const [location, setLocation] = useState(null); // [latitude, longitude]
const LOCATION_TASK_NAME = "background-location-task";
const locationUpdateParameters = useMemo(() => ({
accuracy: Location.Accuracy.High,
distanceInterval: distanceInterval, // Update every 10 meters
timeInterval: timeInterval, // Minimum interval in ms
showsBackgroundLocationIndicator: true, // iOS only
pausesUpdatesAutomatically: false, // (iOS) Prevents auto-pausing of location updates
foregroundService: {
notificationTitle: "Enregistrement de votre position.",
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) {
console.error(error);
return;
}
if (data) {
const { locations } = data;
if (locations.length > 0) {
const firstLocation = locations[0];
const { latitude, longitude } = firstLocation.coords;
const new_location = [latitude, longitude];
try {
setLocation(new_location);
} catch (e) {
console.warn("setLocation failed (probably in background):", e);
}
emitUpdatePosition(new_location);
} else {
console.log("No location measured.");
}
}
});
const getLocationAuthorization = useCallback(async () => {
const { status : statusForeground } = await Location.requestForegroundPermissionsAsync();
const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync();
if (statusForeground !== "granted" || statusBackground !== "granted") {
Alert.alert("Échec", "Activez la localisation en arrière plan dans les paramètres.");
return false;
} else {
return true;
}
}, []);
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]);
const stopLocationTracking = useCallback(async () => {
if (await isTaskRegisteredAsync(LOCATION_TASK_NAME)) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
console.log("Location tracking stopped.");
}
}, []);
export const useLocation = () => {
const [location, setLocation] = useState(null);
useEffect(() => {
getLocationAuthorization();
}, [getLocationAuthorization]);
let subscription;
return [location, getLocationAuthorization, startLocationTracking, stopLocationTracking];
const startWatching = async () => {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') return;
subscription = await Location.watchPositionAsync(
LOCATION_PARAMETERS,
(location) => setLocation(location.coords)
);
};
startWatching();
return () => {
if (subscription) subscription.remove();
};
}, []);
return { location };
};

View File

@@ -3,11 +3,10 @@ import { useEffect, useRef } from 'react';
import DeviceInfo from 'react-native-device-info';
// Context
import { useTeamConnexion } from "../context/teamConnexionContext";
// Hook
import { useSocketCommands } from "./useSocketCommands";
// Services
import { emitBattery, emitDeviceInfo } from "../services/socketEmitter";
export const useSendDeviceInfo = () => {
const { emitBattery, emitDeviceInfo } = useSocketCommands();
const { loggedIn } = useTeamConnexion();
const isMounted = useRef(true);
@@ -41,7 +40,7 @@ export const useSendDeviceInfo = () => {
isMounted.current = false;
clearInterval(batteryCheckInterval);
};
}, [emitBattery, emitDeviceInfo, loggedIn]);
}, [loggedIn]);
return null;
};

View File

@@ -2,10 +2,10 @@
import { useState, useEffect, useCallback, useRef } from 'react';
// Hook
import { useLocalStorage } from './useLocalStorage';
import { useSocketCommands } from "./useSocketCommands";
// Services
import { emitLogin, emitLogout } from "../services/socketEmitter";
export const useSocketAuth = () => {
const { emitLogin, emitLogout } = useSocketCommands();
const [loggedIn, setLoggedIn] = useState(false);
const [savedPassword, setSavedPassword] = useLocalStorage("team_password", null);
const isMounted = useRef(true);
@@ -42,7 +42,7 @@ export const useSocketAuth = () => {
}
});
});
}, [emitLogin, setSavedPassword]);
}, [setSavedPassword]);
useEffect(() => {
if (!loggedIn && savedPassword) {
@@ -54,7 +54,7 @@ export const useSocketAuth = () => {
setLoggedIn(false);
setSavedPassword(null);
emitLogout();
}, [emitLogout, setSavedPassword]);
}, [setSavedPassword]);
return {login, logout, password: savedPassword, loggedIn};
};

View File

@@ -1,52 +0,0 @@
// 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,11 +0,0 @@
// React
import { useEffect } from "react";
export const useSocketListener = (socket, event, callback) => {
useEffect(() => {
socket.on(event, callback);
return () => {
socket.off(event, callback);
};
}, [callback, event, socket]);
};

View File

@@ -0,0 +1,48 @@
// Expo
import { defineTask, isTaskRegisteredAsync } from "expo-task-manager";
import * as Location from 'expo-location';
// Services
import { emitUpdatePosition } from "./socketEmitter";
// Constants
import { TASKS, LOCATION_PARAMETERS } from "../constants";
// Task
defineTask(TASKS.BACKGROUND_LOCATION, async ({ data, error }) => {
if (error) {
console.error(error);
return;
}
if (data) {
const { locations } = data;
if (locations.length == 0) {
console.log("No location measured.");
return;
}
const { latitude, longitude } = locations[0].coords;
emitUpdatePosition([latitude, longitude]);
}
});
// Functions
export const getLocationAuthorization = async () => {
const { status : statusForeground } = await Location.requestForegroundPermissionsAsync();
const { status : statusBackground } = await Location.requestBackgroundPermissionsAsync();
return statusForeground == "granted" && statusBackground == "granted";
};
export const startLocationTracking = async () => {
if (!(await getLocationAuthorization())) return;
if (await isTaskRegisteredAsync(TASKS.BACKGROUND_LOCATION)) return;
console.log("Location tracking started.");
await Location.startLocationUpdatesAsync(TASKS.BACKGROUND_LOCATION, LOCATION_PARAMETERS.SERVER);
};
export const stopLocationTracking = async () => {
if (!await isTaskRegisteredAsync(TASKS.BACKGROUND_LOCATION)) return;
console.log("Location tracking stopped.");
await Location.stopLocationUpdatesAsync(TASKS.BACKGROUND_LOCATION);
};

View File

@@ -0,0 +1,32 @@
// Constants
import { SERVER_URL } from "../constants";
export const uploadTeamImage = async (id, imageUri) => {
if (!imageUri || !id) 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=${id}`, {
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;
}
};
export const enemyImage = (id) => {
if (!id) return require('../assets/images/missing_image.jpg');
return {uri: `${SERVER_URL}/photo/enemy?team=${id}`};
};

View File

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

View File

@@ -0,0 +1,44 @@
// 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

@@ -1,16 +0,0 @@
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,
longitude: 2.342,
latitudeDelta: 0,
longitudeDelta: 50
}
};
export const ZoneTypes = {
circle: "circle",
polygon: "polygon"
};

View File

@@ -1,6 +0,0 @@
export const GameState = {
SETUP: "setup",
PLACEMENT: "placement",
PLAYING: "playing",
FINISHED: "finished"
};