From 606b0fb649d5c3e3bd06b2fdcea8d8d1f20b1e31 Mon Sep 17 00:00:00 2001 From: Anton Tranelis <31516529+antontranelis@users.noreply.github.com> Date: Fri, 13 Mar 2026 10:24:22 +0100 Subject: [PATCH] feat(app): auto-start geolocation after login (#756) Co-authored-by: Claude Opus 4.6 --- app/src/App.tsx | 1 + .../fields/maps/auto_locate_on_login.json | 45 +++++++++++++++++++ lib/src/Components/AppShell/AppShell.tsx | 3 ++ lib/src/Components/AppShell/SetAppState.tsx | 6 +++ .../Components/AppShell/hooks/useAppState.tsx | 2 + .../Subcomponents/Controls/LocateControl.tsx | 30 ++++++++++++- 6 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 backend/directus-config/development/snapshot/fields/maps/auto_locate_on_login.json diff --git a/app/src/App.tsx b/app/src/App.tsx index e21ef608..e02220d2 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -207,6 +207,7 @@ function App() { embedded={embedded} openCollectiveApiKey={config.openCollectiveApiKey} hideSignup={map.hide_signup} + autoLocateOnLogin={map.auto_locate_on_login} > {tagsApi && } diff --git a/backend/directus-config/development/snapshot/fields/maps/auto_locate_on_login.json b/backend/directus-config/development/snapshot/fields/maps/auto_locate_on_login.json new file mode 100644 index 00000000..61d29d88 --- /dev/null +++ b/backend/directus-config/development/snapshot/fields/maps/auto_locate_on_login.json @@ -0,0 +1,45 @@ +{ + "collection": "maps", + "field": "auto_locate_on_login", + "type": "boolean", + "meta": { + "collection": "maps", + "conditions": null, + "display": null, + "display_options": null, + "field": "auto_locate_on_login", + "group": "Presets", + "hidden": false, + "interface": "boolean", + "note": "Automatically start geolocation after user login", + "options": null, + "readonly": false, + "required": false, + "sort": 3, + "special": [ + "cast-boolean" + ], + "translations": null, + "validation": null, + "validation_message": null, + "width": "half" + }, + "schema": { + "name": "auto_locate_on_login", + "table": "maps", + "data_type": "boolean", + "default_value": false, + "max_length": null, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/lib/src/Components/AppShell/AppShell.tsx b/lib/src/Components/AppShell/AppShell.tsx index 4e2a9c34..c0a6d58c 100644 --- a/lib/src/Components/AppShell/AppShell.tsx +++ b/lib/src/Components/AppShell/AppShell.tsx @@ -16,6 +16,7 @@ export function AppShell({ embedded, openCollectiveApiKey, hideSignup, + autoLocateOnLogin, }: { appName: string children: React.ReactNode @@ -23,6 +24,7 @@ export function AppShell({ embedded?: boolean openCollectiveApiKey?: string hideSignup?: boolean + autoLocateOnLogin?: boolean }) { return ( @@ -32,6 +34,7 @@ export function AppShell({ embedded={embedded} openCollectiveApiKey={openCollectiveApiKey} hideSignup={hideSignup} + autoLocateOnLogin={autoLocateOnLogin} />
diff --git a/lib/src/Components/AppShell/SetAppState.tsx b/lib/src/Components/AppShell/SetAppState.tsx index e27bf4c5..cf525a4c 100644 --- a/lib/src/Components/AppShell/SetAppState.tsx +++ b/lib/src/Components/AppShell/SetAppState.tsx @@ -9,11 +9,13 @@ export const SetAppState = ({ embedded, openCollectiveApiKey, hideSignup, + autoLocateOnLogin, }: { assetsApi: AssetsApi embedded?: boolean openCollectiveApiKey?: string hideSignup?: boolean + autoLocateOnLogin?: boolean }) => { const setAppState = useSetAppState() @@ -33,5 +35,9 @@ export const SetAppState = ({ setAppState({ hideSignup: hideSignup ?? false }) }, [hideSignup, setAppState]) + useEffect(() => { + setAppState({ autoLocateOnLogin: autoLocateOnLogin ?? false }) + }, [autoLocateOnLogin, setAppState]) + return <> } diff --git a/lib/src/Components/AppShell/hooks/useAppState.tsx b/lib/src/Components/AppShell/hooks/useAppState.tsx index c31086ed..593446eb 100644 --- a/lib/src/Components/AppShell/hooks/useAppState.tsx +++ b/lib/src/Components/AppShell/hooks/useAppState.tsx @@ -11,6 +11,7 @@ interface AppState { embedded: boolean openCollectiveApiKey: string hideSignup: boolean + autoLocateOnLogin: boolean } type UseAppManagerResult = ReturnType @@ -23,6 +24,7 @@ const initialAppState: AppState = { embedded: false, openCollectiveApiKey: '', hideSignup: false, + autoLocateOnLogin: false, } const AppContext = createContext({ diff --git a/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx b/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx index 194b4b89..6360abab 100644 --- a/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx +++ b/lib/src/Components/Map/Subcomponents/Controls/LocateControl.tsx @@ -8,6 +8,7 @@ import { useNavigate } from 'react-router-dom' import { toast } from 'react-toastify' import TargetSVG from '#assets/target.svg' +import { useAppState } from '#components/AppShell/hooks/useAppState' import { useAuth } from '#components/Auth/useAuth' import { useAddItem, useUpdateItem } from '#components/Map/hooks/useItems' import { useLayers } from '#components/Map/hooks/useLayers' @@ -39,11 +40,16 @@ export const LocateControl = (): React.JSX.Element => { const updateItem = useUpdateItem() const addItem = useAddItem() const layers = useLayers() - const { user } = useAuth() + const { user, isInitialized } = useAuth() + const { autoLocateOnLogin } = useAppState() const navigate = useNavigate() // Prevent React 18 StrictMode from calling useEffect twice const init = useRef(false) + // Track whether auto-locate has already fired (one-shot per session) + const hasAutoLocatedRef = useRef(false) + // Snapshot of user after initial auth completes — changes after this are real logins + const initialUserRef = useRef(undefined) // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment const [lc, setLc] = useState(null) @@ -89,6 +95,28 @@ export const LocateControl = (): React.JSX.Element => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + // Auto-start location tracking after a real login (not page reload) + useEffect(() => { + if (!isInitialized || !autoLocateOnLogin || !lc) return + + // First time isInitialized is true: snapshot the current user as baseline + if (initialUserRef.current === undefined) { + initialUserRef.current = user + return + } + + // If user changed from null to a value after the baseline was set, it's a real login + if (!hasAutoLocatedRef.current && user && initialUserRef.current === null) { + hasAutoLocatedRef.current = true + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + lc.start() + setLoading(true) + setHasDeclinedModal(false) + } + + initialUserRef.current = user + }, [isInitialized, user, lc, autoLocateOnLogin]) + // Check if user logged in while location is active and found useEffect(() => { if (