From 8b39956ef69c1ee92b78d5e5a93514c02a2babbd Mon Sep 17 00:00:00 2001 From: Juan Mrad Date: Tue, 26 May 2026 09:06:03 -0500 Subject: [PATCH] [143] Disable Point of Interest when Google Maps API Key not enabled (#584) --- client/.env.example | 8 +++++ client/src/lib/config.ts | 25 +++++++++++++ client/src/utils/useMapsApis.ts | 35 ++++++++++--------- .../location/LocationInputModal.tsx | 12 ++++++- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/client/.env.example b/client/.env.example index 85f8bdd..5950ca1 100644 --- a/client/.env.example +++ b/client/.env.example @@ -25,6 +25,14 @@ VITE_UI_URL= # or mirror's docs. VITE_DOCS_URL=https://roostorg.github.io/coop/latest +# Google Maps Places API key used for the "Points of Interest" location search +# experience (Location Banks, rule location conditions, etc.). Leave unset to +# disable that tab in the UI; the rest of the location features (geohashes, +# location banks) keep working without it. The key is sent in client-side +# requests, so restrict it via HTTP referrer + API allowlists in the Google +# Cloud Console rather than relying on it being secret. +VITE_GOOGLE_PLACES_API_KEY= + # URL of the content proxy service to use for content display in iframes. # This is used to proxy the content URL to the content proxy service. VITE_CONTENT_PROXY_URL=http://localhost:4000 diff --git a/client/src/lib/config.ts b/client/src/lib/config.ts index f38aa1d..c21e77b 100644 --- a/client/src/lib/config.ts +++ b/client/src/lib/config.ts @@ -29,3 +29,28 @@ export const HOST_URL: string = */ export const DOCS_URL: string = import.meta.env.VITE_DOCS_URL ?? 'https://roostorg.github.io/coop/latest'; + +/** + * Google Maps Places API key used for the "Points of Interest" location + * search experience (Location Banks, rule location conditions, etc.). + * + * Configure with `VITE_GOOGLE_PLACES_API_KEY` at build time. The key is + * sent in client-side requests to Google's Places API; lock it down via + * HTTP referrer + API allowlists in the Google Cloud Console rather than + * relying on it being secret. Adopters who don't want to integrate + * Google Maps can leave this unset — the Points of Interest tab will be + * disabled in the UI and the rest of Location Banks (geohashes, banks) + * keeps working. + */ +export const GOOGLE_PLACES_API_KEY: string = + import.meta.env.VITE_GOOGLE_PLACES_API_KEY ?? ''; + +/** + * Whether this Coop build has a Google Maps Places API key configured. + * + * UI surfaces that depend on Google Places (e.g. the Points of Interest + * tab in `LocationInputModal`) should disable themselves when this is + * false rather than silently failing at request time. + */ +export const IS_GOOGLE_PLACES_API_CONFIGURED: boolean = + GOOGLE_PLACES_API_KEY.length > 0; diff --git a/client/src/utils/useMapsApis.ts b/client/src/utils/useMapsApis.ts index c807949..01c43dc 100644 --- a/client/src/utils/useMapsApis.ts +++ b/client/src/utils/useMapsApis.ts @@ -1,9 +1,7 @@ import { Loader } from '@googlemaps/js-api-loader'; import { useEffect, useState } from 'react'; -// The key should be available at build time in VITE_GOOGLE_PLACES_API_KEY env. -// It's not a secret key, though, so hardcoding it is fine. -const placesApiKey = import.meta.env.VITE_GOOGLE_PLACES_API_KEY ?? ''; +import { GOOGLE_PLACES_API_KEY } from '../lib/config'; // Use this variable to not load the Google APIs twice. let placesApiLoaded = false; @@ -27,19 +25,24 @@ export function useMapsApi() { geocoderService: new google.maps.Geocoder(), }); } else { - new Loader({ apiKey: placesApiKey, libraries: ['places'] }).load().then( - () => { - placesApiLoaded = true; - setMapsApi({ - type: 'LOADED', - autocompleteService: new google.maps.places.AutocompleteService(), - geocoderService: new google.maps.Geocoder(), - }); - }, - (e: Error) => { - setMapsApi({ type: 'ERROR', error: e }); - }, - ); + new Loader({ + apiKey: GOOGLE_PLACES_API_KEY, + libraries: ['places'], + }) + .load() + .then( + () => { + placesApiLoaded = true; + setMapsApi({ + type: 'LOADED', + autocompleteService: new google.maps.places.AutocompleteService(), + geocoderService: new google.maps.Geocoder(), + }); + }, + (e: Error) => { + setMapsApi({ type: 'ERROR', error: e }); + }, + ); } }, []); diff --git a/client/src/webpages/dashboard/components/location/LocationInputModal.tsx b/client/src/webpages/dashboard/components/location/LocationInputModal.tsx index bdc3d7c..869b427 100644 --- a/client/src/webpages/dashboard/components/location/LocationInputModal.tsx +++ b/client/src/webpages/dashboard/components/location/LocationInputModal.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; +import { IS_GOOGLE_PLACES_API_CONFIGURED } from '../../../../lib/config'; import { LocationFormLocation } from '../../../../models/locationBank'; import CoopModal from '../CoopModal'; import TabBar from '../TabBar'; @@ -13,6 +14,9 @@ enum LocationInputModalTab { LOCATION_BANK = 'LOCATION_BANK', } +const POI_DISABLED_TOOLTIP = + "Points of Interest search is unavailable because this Coop instance doesn't have a Google Maps Places API key configured."; + export function locationSectionHeader(header: string) { return
{header}
; } @@ -40,7 +44,9 @@ export default function LocationInputModal(props: { } = props; const [activeTab, setActiveTab] = useState( - LocationInputModalTab.GOOGLE_PLACE, + IS_GOOGLE_PLACES_API_CONFIGURED + ? LocationInputModalTab.GOOGLE_PLACE + : LocationInputModalTab.GEOHASH, ); return ( @@ -53,6 +59,10 @@ export default function LocationInputModal(props: { { label: 'Points of Interest', value: LocationInputModalTab.GOOGLE_PLACE, + disabled: !IS_GOOGLE_PLACES_API_CONFIGURED, + tooltip: !IS_GOOGLE_PLACES_API_CONFIGURED + ? POI_DISABLED_TOOLTIP + : undefined, }, { label: 'Geohashes', -- 2.51.2