UNPKG

@pkme/widget-bridge

Version:

TypeScript React bridge client for Secret City Games widget integration across Framer, Figma, and web platforms

355 lines (286 loc) 11.6 kB
/** * @pkme/widget-bridge/react - CommonJS Version * * React hooks for Secret City Games widget integration */ const React = require('react'); const { useState, useEffect, useCallback, useRef } = React; const { bridge } = require('./index.js'); // ═══════════════════════════════════════════════════════════════════════════════ // CORE BRIDGE HOOK // ═══════════════════════════════════════════════════════════════════════════════ function useBridge() { const [isReady, setIsReady] = useState(bridge.isReady); const [context, setContext] = useState(bridge.context); const [environment, setEnvironment] = useState(bridge.environment); useEffect(() => { // If already ready, update state if (bridge.isReady) { setIsReady(true); setContext(bridge.context); setEnvironment(bridge.environment); return; } // Wait for bridge to be ready let mounted = true; bridge.ready().then(() => { if (mounted) { setIsReady(true); setContext(bridge.context); setEnvironment(bridge.environment); } }); return () => { mounted = false; }; }, []); return { bridge, isReady, context, environment }; } // ═══════════════════════════════════════════════════════════════════════════════ // COMBINED DATA HOOK // ═══════════════════════════════════════════════════════════════════════════════ function useBridgeData() { const { bridge: bridgeInstance, isReady, context, environment } = useBridge(); // State const [globalData, setGlobalData] = useState(null); const [itemData, setItemData] = useState(null); // Refs to prevent loops during optimistic updates const isUpdatingGlobal = useRef(false); const isUpdatingItem = useRef(false); // Subscribe to global data useEffect(() => { if (!bridgeInstance || !isReady) return; console.log('[useBridgeData] Setting up global subscription'); const unsubscribe = bridgeInstance.subscribeGlobal((data) => { if (!isUpdatingGlobal.current) { console.log('[useBridgeData] Received global data:', data); setGlobalData(data); } }); return unsubscribe; }, [bridgeInstance, isReady]); // Subscribe to item data useEffect(() => { if (!bridgeInstance || !isReady) return; console.log('[useBridgeData] Setting up item subscription'); const unsubscribe = bridgeInstance.subscribeItem((data) => { if (!isUpdatingItem.current) { console.log('[useBridgeData] Received item data:', data); setItemData(data); } }); return unsubscribe; }, [bridgeInstance, isReady]); // Update global data with optimistic updates const setGlobal = useCallback(async (data) => { if (!bridgeInstance || !isReady) return; console.log('[useBridgeData] Setting global data:', data); // Optimistic update isUpdatingGlobal.current = true; setGlobalData(data); try { await bridgeInstance.setGlobal(data); } catch (error) { console.error('[useBridgeData] Failed to set global data:', error); } finally { // Reset flag after a short delay to allow for any incoming updates setTimeout(() => { isUpdatingGlobal.current = false; }, 100); } }, [bridgeInstance, isReady]); // Update item data with optimistic updates const setItem = useCallback(async (data) => { if (!bridgeInstance || !isReady) return; console.log('[useBridgeData] Setting item data:', data); // Optimistic update isUpdatingItem.current = true; setItemData(data); try { await bridgeInstance.setItem(data); } catch (error) { console.error('[useBridgeData] Failed to set item data:', error); } finally { // Reset flag after a short delay to allow for any incoming updates setTimeout(() => { isUpdatingItem.current = false; }, 100); } }, [bridgeInstance, isReady]); // Navigation methods const navigate = useCallback(async (to, params, method) => { if (!bridgeInstance || !isReady) return; return bridgeInstance.navigate(to, params, method); }, [bridgeInstance, isReady]); const back = useCallback(async (steps) => { if (!bridgeInstance || !isReady) return; return bridgeInstance.back(steps); }, [bridgeInstance, isReady]); const complete = useCallback(async (data) => { if (!bridgeInstance || !isReady) return; return bridgeInstance.complete(data); }, [bridgeInstance, isReady]); return { globalData, itemData, setGlobal, setItem, navigate, back, complete, isReady, context, environment }; } // ═══════════════════════════════════════════════════════════════════════════════ // INDIVIDUAL DATA HOOKS // ═══════════════════════════════════════════════════════════════════════════════ function useGlobalData(initialData) { const { bridge: bridgeInstance, isReady } = useBridge(); const [data, setData] = useState(initialData || null); const isUpdating = useRef(false); useEffect(() => { if (!bridgeInstance || !isReady) return; console.log('[useGlobalData] Setting up subscription'); const unsubscribe = bridgeInstance.subscribeGlobal((receivedData) => { if (!isUpdating.current) { console.log('[useGlobalData] Received data:', receivedData); setData(receivedData); } }); return unsubscribe; }, [bridgeInstance, isReady]); const updateData = useCallback(async (newData) => { if (!bridgeInstance || !isReady) return; console.log('[useGlobalData] Updating data:', newData); // Optimistic update isUpdating.current = true; setData(newData); try { await bridgeInstance.setGlobal(newData); } catch (error) { console.error('[useGlobalData] Failed to update data:', error); } finally { setTimeout(() => { isUpdating.current = false; }, 100); } }, [bridgeInstance, isReady]); return [data, updateData]; } function useItemData(initialData) { const { bridge: bridgeInstance, isReady } = useBridge(); const [data, setData] = useState(initialData || null); const isUpdating = useRef(false); useEffect(() => { if (!bridgeInstance || !isReady) return; console.log('[useItemData] Setting up subscription'); const unsubscribe = bridgeInstance.subscribeItem((receivedData) => { if (!isUpdating.current) { console.log('[useItemData] Received data:', receivedData); setData(receivedData); } }); return unsubscribe; }, [bridgeInstance, isReady]); const updateData = useCallback(async (newData) => { if (!bridgeInstance || !isReady) return; console.log('[useItemData] Updating data:', newData); // Optimistic update isUpdating.current = true; setData(newData); try { await bridgeInstance.setItem(newData); } catch (error) { console.error('[useItemData] Failed to update data:', error); } finally { setTimeout(() => { isUpdating.current = false; }, 100); } }, [bridgeInstance, isReady]); return [data, updateData]; } // ═══════════════════════════════════════════════════════════════════════════════ // NAVIGATION HOOKS // ═══════════════════════════════════════════════════════════════════════════════ function useNavigation() { const { bridge: bridgeInstance, isReady } = useBridge(); const navigate = useCallback(async (to, params, method) => { if (!bridgeInstance || !isReady) return; return bridgeInstance.navigate(to, params, method); }, [bridgeInstance, isReady]); const back = useCallback(async (steps) => { if (!bridgeInstance || !isReady) return; return bridgeInstance.back(steps); }, [bridgeInstance, isReady]); const complete = useCallback(async (data) => { if (!bridgeInstance || !isReady) return; return bridgeInstance.complete(data); }, [bridgeInstance, isReady]); return { navigate, back, complete, isReady }; } // ═══════════════════════════════════════════════════════════════════════════════ // UTILITY FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════════ async function navigateToNextCard(params) { await bridge.complete({ completed: true, ...params }); if (params && params.nextCardId) { await bridge.navigate('/games/' + bridge.context?.gameId + '/cards/' + params.nextCardId, params); } } async function completeCard(completionData) { const dataToSave = { completed: true, completedAt: Date.now(), ...completionData }; await bridge.setItem(dataToSave); await bridge.setGlobal({ cardCompletion: dataToSave }); await bridge.send('app/cardCompleted', dataToSave); if (completionData && completionData.nextCardId) { await navigateToNextCard({ completed: true, ...completionData }); } } async function navigateToCard(cardId, cardData) { await bridge.navigate('/games/' + bridge.context?.gameId + '/cards/' + cardId, { nextCardId: cardId, ...cardData }); } async function savePuzzleProgress(progress, additionalData) { await bridge.setItem({ progress, timestamp: Date.now(), ...additionalData }); } async function updateGlobalGameState(updates) { await bridge.setGlobal(updates); } // ═══════════════════════════════════════════════════════════════════════════════ // MODULE EXPORTS // ═══════════════════════════════════════════════════════════════════════════════ module.exports = { useBridge, useBridgeData, useGlobalData, useItemData, useNavigation, navigateToNextCard, completeCard, navigateToCard, savePuzzleProgress, updateGlobalGameState };