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.ronde.vu:3478"] }, { urls: ["turn:turn.ronde.vu:3478"], username: "webrtcuser", credential: "supersecretpassword" } ] }; 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); // 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); // 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 )); }; // 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