'use client'; import React, { createContext, useContext, ReactNode, useState, useEffect, useRef } from 'react'; import { GOOGLE_MAPS_LIBRARIES, GOOGLE_MAPS_ID } from '../map-config'; interface GoogleMapsContextType { isLoaded: boolean; loadError: Error | undefined; load: (apiKey: string) => Promise; } const GoogleMapsContext = createContext({ isLoaded: false, loadError: undefined, load: () => Promise.resolve(), }); // Singleton global para prevenir múltiplos carregamentos declare global { interface Window { __XERTICA_GOOGLE_MAPS_LOADER__?: { isLoaded: boolean; loadError: Error | undefined; listeners: Set<(state: GoogleMapsContextType) => void>; scriptElement?: HTMLScriptElement; }; } } /** * Helper that synchronously reads the Google Maps API key from the environment or localStorage */ function getInitialApiKey(): string | undefined { // 1. Check Environment Variable (Vite) if ( typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_GOOGLE_MAPS_API_KEY ) { return import.meta.env.VITE_GOOGLE_MAPS_API_KEY as string; } if (typeof window === 'undefined') return undefined; const savedKey = window.localStorage?.getItem('xertica-googlemaps-api-key'); if (savedKey && savedKey.trim().length > 0) { return savedKey; } return undefined; } /** * Removes the existing Google Maps script tag and clears the global Maps object */ function removeExistingScript(): void { if (typeof window === 'undefined') return; // Remover callback global se existir if ((window as any).__googleMapsCallback) { delete (window as any).__googleMapsCallback; } // Remover script existente const existingScript = document.querySelector(`script[src*="maps.googleapis.com/maps/api/js"]`); if (existingScript) { existingScript.remove(); } // Limpar Google Maps do window if ((window as any).google?.maps) { delete (window as any).google.maps; } // Limpar singleton para permitir novo carregamento if (window.__XERTICA_GOOGLE_MAPS_LOADER__) { window.__XERTICA_GOOGLE_MAPS_LOADER__.isLoaded = false; window.__XERTICA_GOOGLE_MAPS_LOADER__.loadError = undefined; window.__XERTICA_GOOGLE_MAPS_LOADER__.scriptElement = undefined; } } /** * Returns true if the Google Maps JS API has already been loaded */ function isGoogleMapsAlreadyLoaded(): boolean { if (typeof window === 'undefined') return false; return window.google?.maps?.version !== undefined; } /** * Returns true if the AdvancedMarkerElement marker library is available */ function isMarkerLibraryLoaded(): boolean { if (typeof window === 'undefined') return false; return window.google?.maps?.marker?.AdvancedMarkerElement !== undefined; } /** * Dynamically injects the Google Maps JS script tag and resolves when all libraries are loaded */ function loadGoogleMapsScript(apiKey?: string): Promise { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { reject(new Error('Window is undefined')); return; } // Validar API key antes de carregar if (!apiKey || apiKey.length < 10) { reject( new Error( 'Invalid or missing Google Maps API key. Please configure your API key in Settings.' ) ); return; } // Se já está carregado com a mesma key, resolver imediatamente if (isGoogleMapsAlreadyLoaded() && isMarkerLibraryLoaded()) { // Verificar se a API key atual é a mesma const existingScript = document.querySelector( `script[src*="maps.googleapis.com/maps/api/js"]` ) as HTMLScriptElement; if (existingScript && existingScript.src.includes(`key=${apiKey}`)) { resolve(); return; } } // Verificar se o script já existe const existing = document.querySelector( `script[src*="maps.googleapis.com/maps/api/js"]` ) as HTMLScriptElement; if (existing) { // Se existe mas com chave diferente, logar aviso. Não podemos recarregar com SPA se componentes já foram definidos. if (!existing.src.includes(`key=${apiKey}`)) { console.warn( '[GoogleMapsLoader] API key changed, but Google Maps cannot be reloaded without a full page refresh because custom elements are already defined.' ); // Don't reject, just resolve to keep the app working with the old key until refresh resolve(); return; } // Aguardar o script carregar if (isGoogleMapsAlreadyLoaded() && isMarkerLibraryLoaded()) { resolve(); } else { existing.addEventListener('load', () => { setTimeout(() => { if (isMarkerLibraryLoaded()) { resolve(); } else { reject(new Error('Marker library failed to load')); } }, 100); }); existing.addEventListener('error', () => reject(new Error('Failed to load Google Maps'))); } return; } // Prevenir reinjeção se customElements já tem gmp-map (garantia final) if (typeof customElements !== 'undefined' && customElements.get('gmp-map')) { console.warn( '[GoogleMapsLoader] gmp-map is already defined in customElements. Skipping script injection.' ); resolve(); return; } // Criar novo script const script = document.createElement('script'); const params = new URLSearchParams({ key: apiKey, // SEMPRE incluir a API key callback: '__googleMapsCallback', libraries: GOOGLE_MAPS_LIBRARIES.join(','), v: 'quarterly', // Usar 'quarterly' para versão estável ao invés de 'weekly' loading: 'async', }); script.src = `https://maps.googleapis.com/maps/api/js?${params.toString()}`; script.async = true; script.defer = true; script.id = GOOGLE_MAPS_ID; // Callback global (window as any).__googleMapsCallback = () => { delete (window as any).__googleMapsCallback; // Aguardar um pouco para garantir que todas as bibliotecas foram carregadas setTimeout(() => { if (isMarkerLibraryLoaded()) { resolve(); } else { reject(new Error('Marker library failed to load')); } }, 100); }; script.addEventListener('error', () => { delete (window as any).__googleMapsCallback; reject( new Error( 'Failed to load Google Maps script. Please check your API key and ensure Maps JavaScript API is enabled.' ) ); }); document.head.appendChild(script); // Salvar referência ao script const singleton = getOrCreateSingleton(); if (singleton) { singleton.scriptElement = script; } }); } /** * Inicializa o singleton global */ function getOrCreateSingleton() { if (typeof window === 'undefined') return null; if (!window.__XERTICA_GOOGLE_MAPS_LOADER__) { const isPreloaded = isGoogleMapsAlreadyLoaded(); window.__XERTICA_GOOGLE_MAPS_LOADER__ = { isLoaded: isPreloaded, loadError: undefined, listeners: new Set(), scriptElement: undefined, }; } return window.__XERTICA_GOOGLE_MAPS_LOADER__; } /** * Atualiza o estado do singleton e notifica listeners */ function updateSingleton(state: Partial) { const singleton = getOrCreateSingleton(); if (!singleton) return; if (state.isLoaded !== undefined) singleton.isLoaded = state.isLoaded; if (state.loadError !== undefined) singleton.loadError = state.loadError; // Prepare safe state to notify const newState: GoogleMapsContextType = { isLoaded: singleton.isLoaded, loadError: singleton.loadError, load: async (apiKey: string) => { // Allow manual loading call via stored function in context if needed, // though typically we use the direct export or loadGoogleMapsScript if (singleton.isLoaded) return; try { await loadGoogleMapsScript(apiKey); updateSingleton({ isLoaded: true, loadError: undefined }); } catch (error: any) { updateSingleton({ isLoaded: false, loadError: error }); throw error; } }, }; singleton.listeners.forEach(listener => listener(newState)); } /** * Internal component that subscribes to the singleton loader state */ const SingletonLoaderWrapper = ({ children }: { children: ReactNode }) => { const [state, setState] = useState(() => { const singleton = getOrCreateSingleton(); // Default load function that triggers the script load const loadFn = async (apiKey: string) => { if (singleton?.isLoaded) return; try { await loadGoogleMapsScript(apiKey); updateSingleton({ isLoaded: true, loadError: undefined }); } catch (error: any) { updateSingleton({ isLoaded: false, loadError: error }); throw error; } }; if (!singleton) return { isLoaded: false, loadError: undefined, load: loadFn }; return { isLoaded: singleton.isLoaded, loadError: singleton.loadError, load: loadFn, }; }); useEffect(() => { const singleton = getOrCreateSingleton(); if (!singleton) return; const listener = (newState: GoogleMapsContextType) => { setState(newState); }; singleton.listeners.add(listener); // Sincronizar estado inicial e função de load listener({ isLoaded: singleton.isLoaded, loadError: singleton.loadError, load: state.load, }); return () => { singleton.listeners.delete(listener); }; }, []); return {children}; }; /** * Internal component that manually triggers the Google Maps script load */ const LoaderInitializer = ({ apiKey }: { apiKey?: string }) => { const hasInitializedRef = useRef(false); useEffect(() => { // Prevenir múltiplas execuções if (hasInitializedRef.current) { return; } const singleton = getOrCreateSingleton(); if (!singleton) return; // Se já está carregado, atualizar estado if (isGoogleMapsAlreadyLoaded() && isMarkerLibraryLoaded()) { updateSingleton({ isLoaded: true, loadError: undefined }); hasInitializedRef.current = true; return; } hasInitializedRef.current = true; // Use prop key OR get from storage const keyToUse = apiKey || getInitialApiKey(); // Se não houver API key, apenas marcar como não carregado (sem erro) if (!keyToUse) { updateSingleton({ isLoaded: false, loadError: undefined, // Não definir erro quando não há API key }); return; } loadGoogleMapsScript(keyToUse) .then(() => { updateSingleton({ isLoaded: true, loadError: undefined }); }) .catch(error => { updateSingleton({ isLoaded: false, loadError: error }); }); }, [apiKey]); return null; }; interface GoogleMapsLoaderProviderProps { children: ReactNode; apiKey?: string; } /** * GoogleMapsLoaderProvider * * Provider global que gerencia o carregamento da API do Google Maps. * Usa carregamento manual do script para evitar conflitos com custom elements. */ export const GoogleMapsLoaderProvider = ({ children, apiKey }: GoogleMapsLoaderProviderProps) => { const [shouldInitialize] = useState(() => { const singleton = getOrCreateSingleton(); if (!singleton) return false; // Se já está carregado, não inicializar if (singleton.isLoaded || isGoogleMapsAlreadyLoaded() || isMarkerLibraryLoaded()) { singleton.isLoaded = true; return false; } return true; }); return ( <> {shouldInitialize && } {children} ); }; export const useGoogleMapsLoader = () => useContext(GoogleMapsContext); /** * Recarrega o Google Maps com uma nova API key */ export function reloadGoogleMaps(newApiKey: string): Promise { return new Promise((resolve, reject) => { if (typeof window === 'undefined') { reject(new Error('Window is undefined')); return; } // Validar API key if (!newApiKey || newApiKey.length < 10) { reject(new Error('Invalid or missing Google Maps API key')); return; } // Verificar se a key atual é a mesma const existingScript = document.querySelector( `script[src*="maps.googleapis.com/maps/api/js"]` ) as HTMLScriptElement; if (existingScript && existingScript.src.includes(`key=${newApiKey}`)) { // Mesma key, apenas resolver (ou esperar carregar) resolve(); return; } // Se a chave for diferente, NÃO PODEMOS remover o script e adicionar outro // se o Google Maps já definiu custom elements. Isso causa o erro: // "Element with name 'gmp-...' already defined" if (typeof customElements !== 'undefined' && customElements.get('gmp-map')) { console.warn( '[GoogleMapsLoader] Cannot reload map API dynamically because custom elements (gmp-map) are already registered. A full page reload is required to apply the new API key.' ); resolve(); // Resolver para não quebrar a UI return; } // Se chegou aqui e não tem custom elements, podemos tentar recarregar removeExistingScript(); // Atualizar singleton updateSingleton({ isLoaded: false, loadError: undefined }); // Carregar novo script loadGoogleMapsScript(newApiKey) .then(() => { updateSingleton({ isLoaded: true, loadError: undefined }); resolve(); }) .catch(error => { updateSingleton({ isLoaded: false, loadError: error }); reject(error); }); }); } // Alias for backwards compatibility export const GoogleMapsProvider = GoogleMapsLoaderProvider;