"use client"; import React, { useEffect, useState, useMemo } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Circle, CheckCircle, Loader2 } from "lucide-react"; import { PhoneNumber } from "@/components/types"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; export default function ChecklistAndConfig({ ready, setReady, selectedPhoneNumber, setSelectedPhoneNumber, }: { ready: boolean; setReady: (val: boolean) => void; selectedPhoneNumber: string; setSelectedPhoneNumber: (val: string) => void; }) { const [hasCredentials, setHasCredentials] = useState(false); const [phoneNumbers, setPhoneNumbers] = useState([]); const [currentNumberSid, setCurrentNumberSid] = useState(""); const [currentVoiceUrl, setCurrentVoiceUrl] = useState(""); const [publicUrl, setPublicUrl] = useState(""); const [localServerUp, setLocalServerUp] = useState(false); const [publicUrlAccessible, setPublicUrlAccessible] = useState(false); const [allChecksPassed, setAllChecksPassed] = useState(false); const [webhookLoading, setWebhookLoading] = useState(false); const [ngrokLoading, setNgrokLoading] = useState(false); const appendedTwimlUrl = publicUrl ? `${publicUrl}/twiml` : ""; const isWebhookMismatch = appendedTwimlUrl && currentVoiceUrl && appendedTwimlUrl !== currentVoiceUrl; useEffect(() => { let polling = true; const pollChecks = async () => { try { // 1. Check credentials let res = await fetch("/api/twilio"); if (!res.ok) throw new Error("Failed credentials check"); const credData = await res.json(); setHasCredentials(!!credData?.credentialsSet); // 2. Fetch numbers res = await fetch("/api/twilio/numbers"); if (!res.ok) throw new Error("Failed to fetch phone numbers"); const numbersData = await res.json(); if (Array.isArray(numbersData) && numbersData.length > 0) { setPhoneNumbers(numbersData); // If currentNumberSid not set or not in the list, use first const selected = numbersData.find((p: PhoneNumber) => p.sid === currentNumberSid) || numbersData[0]; setCurrentNumberSid(selected.sid); setCurrentVoiceUrl(selected.voiceUrl || ""); setSelectedPhoneNumber(selected.friendlyName || ""); } // 3. Check local server & public URL let foundPublicUrl = ""; try { const resLocal = await fetch("http://localhost:8081/public-url"); if (resLocal.ok) { const pubData = await resLocal.json(); foundPublicUrl = pubData?.publicUrl || ""; setLocalServerUp(true); setPublicUrl(foundPublicUrl); } else { throw new Error("Local server not responding"); } } catch { setLocalServerUp(false); setPublicUrl(""); } } catch (err) { console.error(err); } }; pollChecks(); const intervalId = setInterval(() => polling && pollChecks(), 1000); return () => { polling = false; clearInterval(intervalId); }; }, [currentNumberSid, setSelectedPhoneNumber]); const updateWebhook = async () => { if (!currentNumberSid || !appendedTwimlUrl) return; try { setWebhookLoading(true); const res = await fetch("/api/twilio/numbers", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phoneNumberSid: currentNumberSid, voiceUrl: appendedTwimlUrl, }), }); if (!res.ok) throw new Error("Failed to update webhook"); setCurrentVoiceUrl(appendedTwimlUrl); } catch (err) { console.error(err); } finally { setWebhookLoading(false); } }; const checkNgrok = async () => { if (!localServerUp || !publicUrl) return; setNgrokLoading(true); let success = false; for (let i = 0; i < 5; i++) { try { const resTest = await fetch(publicUrl + "/public-url"); if (resTest.ok) { setPublicUrlAccessible(true); success = true; break; } } catch { // retry } if (i < 4) { await new Promise((r) => setTimeout(r, 3000)); } } if (!success) { setPublicUrlAccessible(false); } setNgrokLoading(false); }; const checklist = useMemo(() => { return [ { label: "Set up Twilio account", done: hasCredentials, description: "Then update account details in webapp/.env", field: ( ), }, { label: "Set up Twilio phone number", done: phoneNumbers.length > 0, description: "Costs around $1.15/month", field: phoneNumbers.length > 0 ? ( phoneNumbers.length === 1 ? ( ) : ( ) ) : ( ), }, { label: "Start local WebSocket server", done: localServerUp, description: "cd websocket-server && npm run dev", field: null, }, { label: "Start ngrok", done: publicUrlAccessible, description: "Then set ngrok URL in websocket-server/.env", field: (
), }, { label: "Update Twilio webhook URL", done: !!publicUrl && !isWebhookMismatch, description: "Can also be done manually in Twilio console", field: (
), }, ]; }, [ hasCredentials, phoneNumbers, currentNumberSid, localServerUp, publicUrl, publicUrlAccessible, currentVoiceUrl, isWebhookMismatch, appendedTwimlUrl, webhookLoading, ngrokLoading, setSelectedPhoneNumber, ]); useEffect(() => { setAllChecksPassed(checklist.every((item) => item.done)); }, [checklist]); useEffect(() => { if (!ready) { checkNgrok(); } }, [localServerUp, ready]); useEffect(() => { if (!allChecksPassed) { setReady(false); } }, [allChecksPassed, setReady]); const handleDone = () => setReady(true); return ( Setup Checklist This sample app requires a few steps before you get started
{checklist.map((item, i) => (
{item.done ? ( ) : ( )} {item.label}
{item.description && (

{item.description}

)}
{item.field}
))}
); }