|
| 1 | +/** |
| 2 | + * useGeofencing — React hook for background geofencing integration |
| 3 | + * |
| 4 | + * Issue #589: [Mobile] Implement Background Location Geofencing for IRL Events |
| 5 | + * |
| 6 | + * Usage: |
| 7 | + * const { status, transitions, start, stop } = useGeofencing(regions); |
| 8 | + * |
| 9 | + * - Manages the full lifecycle: permissions → start → monitor → stop |
| 10 | + * - Exposes a live list of geofence transitions for in-app UI |
| 11 | + * - Cleans up the background task on unmount |
| 12 | + */ |
| 13 | + |
| 14 | +import { useCallback, useEffect, useRef, useState } from "react"; |
| 15 | +import { |
| 16 | + GeofenceRegion, |
| 17 | + GeofenceTransition, |
| 18 | + GeofencingStatus, |
| 19 | + getGeofencingStatus, |
| 20 | + startGeofencing, |
| 21 | + stopGeofencing, |
| 22 | +} from "../services/GeofencingService"; |
| 23 | + |
| 24 | +// ─── Types ──────────────────────────────────────────────────────────────────── |
| 25 | + |
| 26 | +export type GeofencingHookState = |
| 27 | + | "idle" |
| 28 | + | "requesting_permissions" |
| 29 | + | "active" |
| 30 | + | "stopped" |
| 31 | + | "error"; |
| 32 | + |
| 33 | +export interface UseGeofencingReturn { |
| 34 | + /** Current hook lifecycle state */ |
| 35 | + hookState: GeofencingHookState; |
| 36 | + /** Detailed status from the native service */ |
| 37 | + serviceStatus: GeofencingStatus | null; |
| 38 | + /** Ordered list of transitions (newest first, capped at 50) */ |
| 39 | + transitions: GeofenceTransition[]; |
| 40 | + /** Most recent transition, or null */ |
| 41 | + latestTransition: GeofenceTransition | null; |
| 42 | + /** Start monitoring the supplied regions */ |
| 43 | + start: () => Promise<void>; |
| 44 | + /** Stop monitoring and clean up */ |
| 45 | + stop: () => Promise<void>; |
| 46 | + /** Clear the transitions history */ |
| 47 | + clearTransitions: () => void; |
| 48 | + /** Human-readable error message, if any */ |
| 49 | + error: string | null; |
| 50 | +} |
| 51 | + |
| 52 | +const MAX_TRANSITIONS = 50; |
| 53 | + |
| 54 | +// ─── Hook ───────────────────────────────────────────────────────────────────── |
| 55 | + |
| 56 | +/** |
| 57 | + * @param regions Array of geofence regions to monitor. |
| 58 | + * Changing this array after mount does NOT restart monitoring |
| 59 | + * automatically — call stop() then start() if regions change. |
| 60 | + */ |
| 61 | +export function useGeofencing(regions: GeofenceRegion[]): UseGeofencingReturn { |
| 62 | + const [hookState, setHookState] = useState<GeofencingHookState>("idle"); |
| 63 | + const [serviceStatus, setServiceStatus] = useState<GeofencingStatus | null>(null); |
| 64 | + const [transitions, setTransitions] = useState<GeofenceTransition[]>([]); |
| 65 | + const [error, setError] = useState<string | null>(null); |
| 66 | + |
| 67 | + // Keep a stable ref to regions so the transition callback doesn't close over |
| 68 | + // a stale value |
| 69 | + const regionsRef = useRef(regions); |
| 70 | + useEffect(() => { regionsRef.current = regions; }, [regions]); |
| 71 | + |
| 72 | + // ── Transition handler ────────────────────────────────────────────────────── |
| 73 | + |
| 74 | + const handleTransition = useCallback((t: GeofenceTransition) => { |
| 75 | + setTransitions((prev) => [t, ...prev].slice(0, MAX_TRANSITIONS)); |
| 76 | + }, []); |
| 77 | + |
| 78 | + // ── Start ─────────────────────────────────────────────────────────────────── |
| 79 | + |
| 80 | + const start = useCallback(async () => { |
| 81 | + setHookState("requesting_permissions"); |
| 82 | + setError(null); |
| 83 | + |
| 84 | + const status = await startGeofencing(regionsRef.current, handleTransition); |
| 85 | + setServiceStatus(status); |
| 86 | + |
| 87 | + if (status.active) { |
| 88 | + setHookState("active"); |
| 89 | + } else { |
| 90 | + setHookState("error"); |
| 91 | + setError(status.reason ?? "Failed to start geofencing"); |
| 92 | + } |
| 93 | + }, [handleTransition]); |
| 94 | + |
| 95 | + // ── Stop ──────────────────────────────────────────────────────────────────── |
| 96 | + |
| 97 | + const stop = useCallback(async () => { |
| 98 | + await stopGeofencing(); |
| 99 | + setHookState("stopped"); |
| 100 | + const status = await getGeofencingStatus(); |
| 101 | + setServiceStatus(status); |
| 102 | + }, []); |
| 103 | + |
| 104 | + // ── Clear transitions ─────────────────────────────────────────────────────── |
| 105 | + |
| 106 | + const clearTransitions = useCallback(() => { |
| 107 | + setTransitions([]); |
| 108 | + }, []); |
| 109 | + |
| 110 | + // ── Sync status on mount ──────────────────────────────────────────────────── |
| 111 | + |
| 112 | + useEffect(() => { |
| 113 | + let cancelled = false; |
| 114 | + |
| 115 | + getGeofencingStatus().then((status) => { |
| 116 | + if (cancelled) return; |
| 117 | + setServiceStatus(status); |
| 118 | + if (status.active) setHookState("active"); |
| 119 | + }); |
| 120 | + |
| 121 | + return () => { cancelled = true; }; |
| 122 | + }, []); |
| 123 | + |
| 124 | + // ── Cleanup on unmount ────────────────────────────────────────────────────── |
| 125 | + |
| 126 | + useEffect(() => { |
| 127 | + return () => { |
| 128 | + // Do not stop the background task on unmount — it should keep running |
| 129 | + // even when the component is unmounted. Call stop() explicitly to halt. |
| 130 | + }; |
| 131 | + }, []); |
| 132 | + |
| 133 | + return { |
| 134 | + hookState, |
| 135 | + serviceStatus, |
| 136 | + transitions, |
| 137 | + latestTransition: transitions[0] ?? null, |
| 138 | + start, |
| 139 | + stop, |
| 140 | + clearTransitions, |
| 141 | + error, |
| 142 | + }; |
| 143 | +} |
0 commit comments