import React, {useState, useEffect} from 'react'; import {Rondevu} from '@xtr-dev/rondevu-client'; import toast, {Toaster} from 'react-hot-toast'; const API_URL = 'https://api.ronde.vu'; const RTC_CONFIG = { iceServers: [ {urls: 'stun:stun.l.google.com:19302'}, {urls: 'stun:stun1.l.google.com:19302'}, { urls: 'turn:relay1.expressturn.com:3480', username: 'ef13B1E5PH265HK1N2', credential: 'TTcTPEy3ndxsS0Gp' } ] }; export default function App() { const [client, setClient] = useState(null); const [credentials, setCredentials] = useState(null); const [activeTab, setActiveTab] = useState('setup'); const [status, setStatus] = useState('Not registered'); // Offer state const [offerTopics, setOfferTopics] = useState('demo-chat'); const [myConnections, setMyConnections] = useState([]); // Discovery state const [searchTopic, setSearchTopic] = useState('demo-chat'); const [discoveredOffers, setDiscoveredOffers] = useState([]); // Messages const [messages, setMessages] = useState([]); const [messageInput, setMessageInput] = useState(''); // Load credentials useEffect(() => { const saved = localStorage.getItem('rondevu-credentials'); if (saved) { try { const creds = JSON.parse(saved); // Validate credentials have required fields if (creds && creds.peerId && creds.secret) { setCredentials(creds); setClient(new Rondevu({baseUrl: API_URL, credentials: creds})); setStatus('Registered (from storage)'); } else { // Invalid credentials, remove them localStorage.removeItem('rondevu-credentials'); setClient(new Rondevu({baseUrl: API_URL})); setStatus('Not registered'); } } catch (err) { // Corrupted credentials, remove them console.error('Failed to load credentials:', err); localStorage.removeItem('rondevu-credentials'); setClient(new Rondevu({baseUrl: API_URL})); setStatus('Not registered'); } } else { setClient(new Rondevu({baseUrl: API_URL})); } }, []); // Cleanup on unmount useEffect(() => { return () => { // Close all connections when component unmounts myConnections.forEach(c => c.conn?.close()); }; }, [myConnections]); // Register const handleRegister = async () => { if (!client) return; try { setStatus('Registering...'); const creds = await client.register(); setCredentials(creds); localStorage.setItem('rondevu-credentials', JSON.stringify(creds)); setClient(new Rondevu({baseUrl: API_URL, credentials: creds})); setStatus('Registered!'); setActiveTab('offer'); } catch (err) { setStatus(`Error: ${err.message}`); } }; // Create offer with connection manager const handleCreateOffer = async () => { if (!client || !credentials) { toast.error('Please register first!'); return; } try { const topics = offerTopics.split(',').map(t => t.trim()).filter(Boolean); // Create connection using the manager const conn = client.createConnection(RTC_CONFIG); // Add debugging addApiLogging(client); addIceLogging(conn); // Setup event listeners conn.on('connecting', () => { updateConnectionStatus(conn.id, 'connecting'); }); conn.on('connected', () => { updateConnectionStatus(conn.id, 'connected'); }); conn.on('disconnected', () => { updateConnectionStatus(conn.id, 'disconnected'); }); conn.on('datachannel', (channel) => { // Handle data channel channel.onmessage = (event) => { setMessages(prev => [...prev, { from: 'peer', text: event.data, timestamp: Date.now(), connId: conn.id }]); }; updateConnectionChannel(conn.id, channel); }); // Create offer const offerId = await conn.createOffer({ topics, ttl: 300000 }); // Add to connections list setMyConnections(prev => [...prev, { id: offerId, topics, status: 'waiting', role: 'offerer', conn, channel: conn.channel }]); setOfferTopics(''); toast.success(`Created offer! Share topic "${topics[0]}" with peers.`); } catch (err) { toast.error(`Error: ${err.message}`); } }; // Discover peers const handleDiscoverPeers = async () => { if (!client) return; if (!client.isAuthenticated()) { toast.error('Please register first!'); return; } try { const offers = await client.offers.findByTopic(searchTopic.trim(), {limit: 50}); setDiscoveredOffers(offers); if (offers.length === 0) { toast.error('No peers found!'); } else { toast.success(`Found ${offers.length} peer(s)`); } } catch (err) { toast.error(`Error: ${err.message}`); } }; // Answer an offer const handleAnswerOffer = async (offer) => { if (!client || !credentials) { toast.error('Please register first!'); return; } try { // Create connection using the manager const conn = client.createConnection(RTC_CONFIG); // Add debugging addApiLogging(client); addIceLogging(conn); // Setup event listeners conn.on('connecting', () => { updateConnectionStatus(conn.id, 'connecting'); }); conn.on('connected', () => { updateConnectionStatus(conn.id, 'connected'); }); conn.on('disconnected', () => { updateConnectionStatus(conn.id, 'disconnected'); }); conn.on('datachannel', (channel) => { // Handle data channel channel.onmessage = (event) => { setMessages(prev => [...prev, { from: 'peer', text: event.data, timestamp: Date.now(), connId: conn.id }]); }; updateConnectionChannel(conn.id, channel); }); // Answer the offer await conn.answer(offer.id, offer.sdp); // Add to connections list setMyConnections(prev => [...prev, { id: offer.id, topics: offer.topics, status: 'connecting', role: 'answerer', conn, channel: null }]); setActiveTab('connections'); toast.success('Connecting...'); } catch (err) { toast.error(`Error: ${err.message}`); } }; // Helper functions const updateConnectionStatus = (connId, status) => { setMyConnections(prev => prev.map(c => c.id === connId ? {...c, status} : c )); }; const updateConnectionChannel = (connId, channel) => { setMyConnections(prev => prev.map(c => c.id === connId ? {...c, channel} : c )); }; // Add API-level ICE candidate logging const addApiLogging = (client) => { const originalAddIceCandidates = client.offers.addIceCandidates.bind(client.offers); const originalGetIceCandidates = client.offers.getIceCandidates.bind(client.offers); client.offers.addIceCandidates = async (offerId, candidates) => { console.log(`📤 Sending ${candidates.length} ICE candidate(s) to server for offer ${offerId}`); return originalAddIceCandidates(offerId, candidates); }; client.offers.getIceCandidates = async (offerId, since) => { const result = await originalGetIceCandidates(offerId, since); console.log(`📥 Received ${result.length} ICE candidate(s) from server for offer ${offerId}, since=${since}`); if (result.length > 0) { console.log(`📥 First candidate:`, result[0]); } return result; }; }; // Add ICE debugging to a connection const addIceLogging = (conn) => { const pc = conn['pc']; // Access underlying peer connection for debugging if (pc) { // Add new handlers that don't override existing ones pc.addEventListener('icecandidate', (event) => { if (event.candidate) { console.log('🧊 ICE candidate gathered:', { type: event.candidate.type, protocol: event.candidate.protocol, address: event.candidate.address, port: event.candidate.port, candidate: event.candidate.candidate }); } else { console.log('🧊 ICE gathering complete'); } }); pc.addEventListener('icegatheringstatechange', () => { console.log('🧊 ICE gathering state:', pc.iceGatheringState); }); pc.addEventListener('iceconnectionstatechange', () => { console.log('🧊 ICE connection state:', pc.iceConnectionState); }); pc.addEventListener('connectionstatechange', () => { console.log('🔌 Connection state:', pc.connectionState); }); } }; // Send message const handleSendMessage = (connection) => { if (!messageInput.trim() || !connection.channel) return; if (connection.channel.readyState !== 'open') { toast.error('Channel not open yet!'); return; } connection.channel.send(messageInput); setMessages(prev => [...prev, { from: 'me', text: messageInput, timestamp: Date.now(), connId: connection.id }]); setMessageInput(''); }; // Clear credentials const handleClearCredentials = () => { localStorage.removeItem('rondevu-credentials'); setCredentials(null); setStatus('Not registered'); myConnections.forEach(c => c.conn?.close()); setMyConnections([]); setMessages([]); setClient(new Rondevu({baseUrl: API_URL})); }; return (
Topic-Based Peer Discovery & WebRTC
v0.4.0 - With Connection Manager
Get your credentials to start connecting
{credentials.peerId.substring(0, 20)}...Create a WebRTC offer that peers can discover
{!credentials ? (Search for peers by topic
Chat with connected peers
{myConnections.length === 0 ? (Server: {API_URL}
Open in multiple tabs to test peer-to-peer connections