hooks/useGeolocation.ts
Annotation non disponible
Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.
2 exports
useGeolocationUseGeolocationReturn
Code source· typescript
/**
* Sprint International Foundation V1 — 2026-05-17
*
* Hook autour de `expo-location` + `useGeoStore`.
*
* Pattern :
* 1. Au mount : si la position en cache est encore fraîche (< 1h), on
* l'utilise sans demander la permission ni interroger le GPS.
* 2. Sinon, `requestPermission()` doit être appelé explicitement (geste UX
* — bouton "Activer la géoloc" dans le futur banner Home V1.8).
* 3. `getCurrentPosition()` interroge le GPS et alimente le store.
*
* V1 : pas de branchement UI. Hook prêt à l'emploi pour V1.8 quand on
* branchera la home sur `/api/mobile/vitrines?near=`.
*/
import { useCallback, useEffect, useState } from 'react';
import * as Location from 'expo-location';
import { useGeoStore } from '@/store/geoStore';
export type UseGeolocationReturn = {
lat: number | null;
lng: number | null;
accuracy: number | null;
loading: boolean;
error: string | null;
hasPermission: boolean | null; // null = pas encore vérifié
requestPermission: () => Promise<boolean>;
getCurrentPosition: () => Promise<void>;
clearPosition: () => void;
};
export function useGeolocation(): UseGeolocationReturn {
const position = useGeoStore((s) => s.position);
const setPosition = useGeoStore((s) => s.setPosition);
const clearPosition = useGeoStore((s) => s.clearPosition);
const isFresh = useGeoStore((s) => s.isFresh);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
// Vérifie le statut de permission au mount (sans rien demander à l'utilisateur)
useEffect(() => {
let alive = true;
Location.getForegroundPermissionsAsync()
.then((s) => {
if (alive) setHasPermission(s.granted);
})
.catch(() => {
if (alive) setHasPermission(false);
});
return () => {
alive = false;
};
}, []);
const requestPermission = useCallback(async (): Promise<boolean> => {
try {
const res = await Location.requestForegroundPermissionsAsync();
setHasPermission(res.granted);
if (!res.granted) {
setError('Permission de géolocalisation refusée.');
} else {
setError(null);
}
return res.granted;
} catch (e: any) {
setError(e?.message || 'Erreur permission géoloc');
setHasPermission(false);
return false;
}
}, []);
const getCurrentPosition = useCallback(async (): Promise<void> => {
setError(null);
// Si on a déjà un fix frais, on ne re-fetch pas le GPS
if (isFresh()) return;
setLoading(true);
try {
// S'assure d'avoir la permission (sans bloquer si déjà accordée)
const status = await Location.getForegroundPermissionsAsync();
if (!status.granted) {
const granted = await requestPermission();
if (!granted) {
setLoading(false);
return;
}
}
const loc = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
setPosition({
lat: loc.coords.latitude,
lng: loc.coords.longitude,
accuracy: loc.coords.accuracy ?? null,
timestamp: Date.now(),
});
} catch (e: any) {
setError(e?.message || 'Impossible de récupérer la position.');
} finally {
setLoading(false);
}
}, [isFresh, requestPermission, setPosition]);
return {
lat: position?.lat ?? null,
lng: position?.lng ?? null,
accuracy: position?.accuracy ?? null,
loading,
error,
hasPermission,
requestPermission,
getCurrentPosition,
clearPosition,
};
}
/**
* Sprint International Foundation V1 — 2026-05-17
*
* Hook autour de `expo-location` + `useGeoStore`.
*
* Pattern :
* 1. Au mount : si la position en cache est encore fraîche (< 1h), on
* l'utilise sans demander la permission ni interroger le GPS.
* 2. Sinon, `requestPermission()` doit être appelé explicitement (geste UX
* — bouton "Activer la géoloc" dans le futur banner Home V1.8).
* 3. `getCurrentPosition()` interroge le GPS et alimente le store.
*
* V1 : pas de branchement UI. Hook prêt à l'emploi pour V1.8 quand on
* branchera la home sur `/api/mobile/vitrines?near=`.
*/
import { useCallback, useEffect, useState } from 'react';
import * as Location from 'expo-location';
import { useGeoStore } from '@/store/geoStore';
export type UseGeolocationReturn = {
lat: number | null;
lng: number | null;
accuracy: number | null;
loading: boolean;
error: string | null;
hasPermission: boolean | null; // null = pas encore vérifié
requestPermission: () => Promise<boolean>;
getCurrentPosition: () => Promise<void>;
clearPosition: () => void;
};
export function useGeolocation(): UseGeolocationReturn {
const position = useGeoStore((s) => s.position);
const setPosition = useGeoStore((s) => s.setPosition);
const clearPosition = useGeoStore((s) => s.clearPosition);
const isFresh = useGeoStore((s) => s.isFresh);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [hasPermission, setHasPermission] = useState<boolean | null>(null);
// Vérifie le statut de permission au mount (sans rien demander à l'utilisateur)
useEffect(() => {
let alive = true;
Location.getForegroundPermissionsAsync()
.then((s) => {
if (alive) setHasPermission(s.granted);
})
.catch(() => {
if (alive) setHasPermission(false);
});
return () => {
alive = false;
};
}, []);
const requestPermission = useCallback(async (): Promise<boolean> => {
try {
const res = await Location.requestForegroundPermissionsAsync();
setHasPermission(res.granted);
if (!res.granted) {
setError('Permission de géolocalisation refusée.');
} else {
setError(null);
}
return res.granted;
} catch (e: any) {
setError(e?.message || 'Erreur permission géoloc');
setHasPermission(false);
return false;
}
}, []);
const getCurrentPosition = useCallback(async (): Promise<void> => {
setError(null);
// Si on a déjà un fix frais, on ne re-fetch pas le GPS
if (isFresh()) return;
setLoading(true);
try {
// S'assure d'avoir la permission (sans bloquer si déjà accordée)
const status = await Location.getForegroundPermissionsAsync();
if (!status.granted) {
const granted = await requestPermission();
if (!granted) {
setLoading(false);
return;
}
}
const loc = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
setPosition({
lat: loc.coords.latitude,
lng: loc.coords.longitude,
accuracy: loc.coords.accuracy ?? null,
timestamp: Date.now(),
});
} catch (e: any) {
setError(e?.message || 'Impossible de récupérer la position.');
} finally {
setLoading(false);
}
}, [isFresh, requestPermission, setPosition]);
return {
lat: position?.lat ?? null,
lng: position?.lng ?? null,
accuracy: position?.accuracy ?? null,
loading,
error,
hasPermission,
requestPermission,
getCurrentPosition,
clearPosition,
};
}