import { useState } from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ds/ui/dialog"; import { Button } from "@/components/ds/ui/button"; import { Input } from "@/components/ds/ui/input"; import { Label } from "@/components/ds/ui/label"; import { Alert, AlertDescription } from "@/components/ds/ui/alert"; import { Loader2, Database, CheckCircle, AlertCircle, ExternalLink, Check, } from "lucide-react"; import { useTranslate } from "ra-core"; import { saveSupabaseConfig, validateSupabaseConnection, } from "@/lib/supabase-config"; type WizardStep = "welcome" | "credentials" | "validating" | "success"; interface SupabaseSetupWizardProps { open: boolean; onComplete: () => void; canClose?: boolean; } /** * Normalizes Supabase URL input - accepts either full URL or just project ID */ function normalizeSupabaseUrl(input: string): string { const trimmed = input.trim(); if (!trimmed) return ""; // If it starts with http:// or https://, treat as full URL if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { return trimmed; } // Otherwise, treat as project ID and construct full URL return `https://${trimmed}.supabase.co`; } /** * Validates if input looks like a valid Supabase URL or project ID */ function validateUrlFormat(input: string): { valid: boolean; messageKey?: string; } { const trimmed = input.trim(); if (!trimmed) return { valid: false }; // Check if it's a full URL if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) { try { const url = new URL(trimmed); if (url.hostname.endsWith(".supabase.co")) { return { valid: true, messageKey: "crm.setup_wizard.credentials.url_valid", }; } return { valid: false, messageKey: "crm.setup_wizard.credentials.url_must_be_supabase", }; } catch { return { valid: false, messageKey: "crm.setup_wizard.credentials.url_invalid_format", }; } } // Check if it's a project ID (alphanumeric, typically 20 chars) if (/^[a-z0-9]+$/.test(trimmed)) { return { valid: true, messageKey: "crm.setup_wizard.credentials.url_project_id", }; } return { valid: false, messageKey: "crm.setup_wizard.credentials.url_hint" }; } /** * Validates if input looks like a valid Supabase API key */ function validateKeyFormat(input: string): { valid: boolean; messageKey?: string; } { const trimmed = input.trim(); if (!trimmed) return { valid: false }; // New publishable keys start with "sb_publishable_" followed by key content if (trimmed.startsWith("sb_publishable_")) { // Check that there's actual key content after the prefix (at least 20 chars) if (trimmed.length > "sb_publishable_".length + 20) { return { valid: true, messageKey: "crm.setup_wizard.credentials.key_valid_publishable", }; } return { valid: false, messageKey: "crm.setup_wizard.credentials.key_incomplete_publishable", }; } // Legacy anon keys are JWT tokens starting with "eyJ" if (trimmed.startsWith("eyJ")) { if (trimmed.length > 100) { return { valid: true, messageKey: "crm.setup_wizard.credentials.key_valid_anon", }; } return { valid: false, messageKey: "crm.setup_wizard.credentials.key_incomplete_anon", }; } return { valid: false, messageKey: "crm.setup_wizard.credentials.key_invalid", }; } export function SupabaseSetupWizard({ open, onComplete, canClose = false, }: SupabaseSetupWizardProps) { const [step, setStep] = useState("welcome"); const [url, setUrl] = useState(""); const [anonKey, setAnonKey] = useState(""); const [error, setError] = useState(null); const [urlTouched, setUrlTouched] = useState(false); const [keyTouched, setKeyTouched] = useState(false); const translate = useTranslate(); const handleValidateAndSave = async () => { setError(null); setStep("validating"); // Normalize the URL before validation const normalizedUrl = normalizeSupabaseUrl(url); const trimmedKey = anonKey.trim(); const result = await validateSupabaseConnection(normalizedUrl, trimmedKey); if (result.valid) { saveSupabaseConfig({ url: normalizedUrl, anonKey: trimmedKey }); setStep("success"); // Reload after short delay to apply new config setTimeout(() => { // Force reload to ensure new config is loaded window.location.href = window.location.origin; }, 1500); } else { setError( result.error || translate("crm.setup_wizard.credentials.error_failed"), ); setStep("credentials"); } }; // Get validation states const urlValidation = url ? validateUrlFormat(url) : { valid: false }; const keyValidation = anonKey ? validateKeyFormat(anonKey) : { valid: false }; const normalizedUrl = url ? normalizeSupabaseUrl(url) : ""; const showUrlExpansion = url && !url.startsWith("http") && urlValidation.valid; const handleClose = () => { if (canClose) { onComplete(); } }; return ( !canClose && e.preventDefault()} onEscapeKeyDown={(e) => !canClose && e.preventDefault()} > {step === "welcome" && ( <>
{translate("crm.setup_wizard.welcome.title", { title: "CRM", })}
{translate("crm.setup_wizard.welcome.description")}
{translate("crm.setup_wizard.welcome.no_project")}
{translate("crm.setup_wizard.welcome.create_free")}{" "} supabase.com

{translate("crm.setup_wizard.welcome.need_title")}

  • {translate("crm.setup_wizard.welcome.need_url")}
  • {translate("crm.setup_wizard.welcome.need_key")}
)} {step === "credentials" && ( <> {translate("crm.setup_wizard.credentials.title")} {translate("crm.setup_wizard.credentials.description")}
{error && ( {error} )}
{ setUrl(e.target.value); setUrlTouched(true); }} onBlur={() => setUrlTouched(true)} className={ urlTouched && url ? urlValidation.valid ? "pr-8 border-success" : "pr-8 border-destructive" : "" } /> {urlTouched && url && urlValidation.valid && ( )}
{showUrlExpansion && (
{translate("crm.setup_wizard.credentials.url_expansion", { url: normalizedUrl, })}
)} {urlTouched && url && urlValidation.messageKey && !urlValidation.valid && (

{translate(urlValidation.messageKey)}

)} {(!urlTouched || !url) && (

{translate("crm.setup_wizard.credentials.url_default_hint")}

)}
{ setAnonKey(e.target.value); setKeyTouched(true); }} onBlur={() => setKeyTouched(true)} className={ keyTouched && anonKey ? keyValidation.valid ? "pr-8 border-success" : "pr-8 border-destructive" : "" } /> {keyTouched && anonKey && keyValidation.valid && ( )}
{keyTouched && anonKey && keyValidation.messageKey && (

{translate(keyValidation.messageKey)}

)} {(!keyTouched || !anonKey) && (

{translate("crm.setup_wizard.credentials.key_default_hint")}

)}
)} {step === "validating" && ( <> {translate("crm.setup_wizard.validating.title")} {translate("crm.setup_wizard.validating.description")}

{translate("crm.setup_wizard.validating.wait")}

)} {step === "success" && ( <> {translate("crm.setup_wizard.success.title")} {translate("crm.setup_wizard.success.description")}

{translate("crm.setup_wizard.success.reloading")}

)}
); }