@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
JavaScript
/**
* @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
};