import React, { useState, useEffect, useRef } from 'react'; import { Button } from 'avanza-ui'; import './App.css'; import StudioPortal from './components/Portal/StudioPortal'; import { isAllowedOrigin } from './utils/postMessage'; import { Room } from 'livekit-client'; function App() { const [isConnected, setIsConnected] = useState(false); const [credentials, setCredentials] = useState({ serverUrl: import.meta.env.VITE_LIVEKIT_URL || 'ws://localhost:7880', token: '', roomName: 'Studio Demo', }); const [tempToken, setTempToken] = useState(''); const autoAttemptRef = useRef(false); // external LiveKit Room managed by App when token is received const roomRef = useRef(null); const messageSourceRef = useRef(null); // store the last validated origin that sent a token so we can ACK securely const lastValidatedOrigin = useRef(null); // Called when the LiveKit room reports connected const handleRoomConnected = () => { setIsConnected(true); // send connected ACK to the validated origin if available try { const payload = { type: 'LIVEKIT_ACK', status: 'connected' }; const targetOrigin = lastValidatedOrigin.current || '*'; // Prefer replying directly to the message source window if available if (messageSourceRef.current && typeof (messageSourceRef.current as any).postMessage === 'function') { try { (messageSourceRef.current as any).postMessage(payload, targetOrigin); } catch (e) { /* ignore */ } } if (window.opener && !window.opener.closed) { try { window.opener.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ } } if (window.parent && window.parent !== window) { try { window.parent.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ } } } catch (e) { /* ignore */ } }; const handleRoomDisconnected = () => { setIsConnected(false); try { const payload = { type: 'LIVEKIT_ACK', status: 'disconnected' }; const targetOrigin = lastValidatedOrigin.current || '*'; if (window.opener && !window.opener.closed) { try { window.opener.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ } } if (window.parent && window.parent !== window) { try { window.parent.postMessage(payload, targetOrigin); } catch (e) { /* ignore */ } } } catch (e) {} // disconnect and clear app-managed room try { if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; } } catch(e){} }; // Cleanup app-managed room on unmount useEffect(() => { return () => { try { if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; } } catch(e){} }; }, []); // Listen for LIVEKIT_TOKEN posted via postMessage (handshake flow) useEffect(() => { // central token handler used by both message events and custom events const handleIncomingToken = async (payload: any, origin?: string | null, source?: any) => { try { const originToUse = origin || null; if (!isAllowedOrigin(originToUse)) { return; } // store validated origin and message source for ACKs if (originToUse) lastValidatedOrigin.current = originToUse; if (source) messageSourceRef.current = source; if (payload?.url) setCredentials(prev => ({ ...prev, serverUrl: String(payload.url) })); const receivedToken = String(payload.token || payload?.token); setTempToken(receivedToken); // cleanup previous room if exists try { if (roomRef.current) { roomRef.current.disconnect(); roomRef.current = null; } } catch(e){} const newRoom = new Room(); roomRef.current = newRoom; try { const sUrl = payload?.url || credentials.serverUrl; await newRoom.connect(sUrl, receivedToken); setCredentials(prev => ({ ...prev, token: receivedToken })); handleRoomConnected(); } catch (err) { console.error('App-managed room connect failed', err); } if (!autoAttemptRef.current) { autoAttemptRef.current = true; setTimeout(() => { if (receivedToken) handleConnectWithToken(receivedToken); }, 60); } } catch (err) { console.debug('handleIncomingToken error', err); } }; function onMessage(e: MessageEvent) { try { const d = e.data || {}; if (d?.type === 'LIVEKIT_TOKEN' && d.token) { // call central handler and pass origin/source handleIncomingToken(d, e.origin || null, e.source); } } catch (err) { console.debug('postMessage in App error', err) } } window.addEventListener('message', onMessage); // Also listen for the custom event dispatched by main.tsx function onCustomToken(e: any) { try { const detail = e?.detail || (window as any).__AVANZACAST_PENDING_TOKEN || null; // attempt to recover origin/source from globals set by main.tsx if present const lastMsg = (window as any).__AVZ_LAST_MSG_SOURCE || null; const origin = lastMsg?.origin || null; const source = lastMsg?.source || null; if (detail && detail.token) handleIncomingToken(detail, origin, source); } catch(err) { console.debug('custom token handler error', err); } } window.addEventListener('avz:livekit:token', onCustomToken as EventListener); return () => { window.removeEventListener('message', onMessage); window.removeEventListener('avz:livekit:token', onCustomToken as EventListener); }; }, []); function handleConnectWithToken(tokenVal: string) { if (tokenVal && tokenVal.trim()) { setCredentials(prev => ({ ...prev, token: tokenVal })); setIsConnected(true); } } const handleConnect = () => { if (tempToken.trim()) { setCredentials(prev => ({ ...prev, token: tempToken })); setIsConnected(true); } }; // Update a global #status element and notify opener/parent when connected — helps E2E tests detect ACK/state useEffect(() => { try { const setStatus = (txt: string) => { try { let el = document.getElementById('status'); if (!el) { el = document.createElement('div'); el.id = 'status'; el.style.position = 'fixed'; el.style.right = '12px'; el.style.top = '12px'; el.style.padding = '8px 12px'; el.style.background = 'rgba(16,185,129,0.12)'; el.style.color = '#10B981'; el.style.borderRadius = '6px'; el.style.zIndex = '9999'; document.body.appendChild(el); } el.textContent = txt; } catch (e) { /* ignore */ } }; if (isConnected) { setStatus('Conectado'); } else { setStatus('Esperando token...'); } } catch (e) {} }, [isConnected]); if (!isConnected) { return (

Studio Panel - AvanzaCast

Ingresa tus credenciales de LiveKit para comenzar

setCredentials(prev => ({ ...prev, serverUrl: e.target.value }))} style={{ width: '100%', padding: '10px 12px', background: 'var(--studio-bg-primary)', border: '1px solid var(--studio-border)', borderRadius: 'var(--studio-radius-md)', color: 'var(--studio-text-primary)', fontSize: 'var(--studio-text-base)', fontFamily: 'var(--studio-font-family)' }} placeholder="ws://localhost:7880" />
setCredentials(prev => ({ ...prev, roomName: e.target.value }))} style={{ width: '100%', padding: '10px 12px', background: 'var(--studio-bg-primary)', border: '1px solid var(--studio-border)', borderRadius: 'var(--studio-radius-md)', color: 'var(--studio-text-primary)', fontSize: 'var(--studio-text-base)', fontFamily: 'var(--studio-font-family)' }} placeholder="Studio Demo" />