// Login / Register page const { useState: useStateLogin, useEffect: useEffectLogin, useRef: useRefLogin } = React; // Sconto annuale — deve corrispondere a ANNUAL_DISCOUNT in stripe_service.py const ANNUAL_DISCOUNT = 0.90; const PLANS_META = [ { id: "starter", name: "Starter", monthlyPrice: 19, properties: "1 struttura", features: ["Chatbot WhatsApp/Telegram", "100 messaggi per sessione", "Check-in PDF", "QR Code"], }, { id: "pro", name: "Pro", monthlyPrice: 39, properties: "fino a 10 strutture", highlight: true, features: ["Tutto di Starter", "500 messaggi per sessione", "Check-in digitale OCR", "Supporto prioritario"], }, { id: "business", name: "Business", monthlyPrice: 79, properties: "strutture illimitate", features: ["Tutto di Pro", "1.000 messaggi per sessione", "Account manager dedicato", "API access"], }, ]; function fmtPrice(monthly, interval) { if (interval === "monthly") return `€${monthly}`; return `€${(monthly * ANNUAL_DISCOUNT).toFixed(2).replace(".00", "")}`; } function yearlySaving(monthly) { return Math.round(monthly * 12 * (1 - ANNUAL_DISCOUNT)); } // Monta il form Stripe Embedded Checkout in un div interno dedicato. // stripeCustomerId: se presente usa /auth/checkout-anon (registrazione), altrimenti /billing/checkout (host autenticato). function EmbeddedCheckout({ planId, interval, stripeCustomerId }) { const mountRef = useRefLogin(null); const [error, setError] = useStateLogin(null); useEffectLogin(() => { if (!planId) return; let checkout = null; (async () => { try { const stripe = await window.API.getStripe(); if (!stripe) { setError("Pagamenti non disponibili al momento."); return; } checkout = await stripe.initEmbeddedCheckout({ fetchClientSecret: async () => { const data = stripeCustomerId ? await window.API.checkoutAnon(stripeCustomerId, planId, interval) : await window.API.createCheckout(planId, interval); return data.client_secret; }, }); if (mountRef.current) checkout.mount(mountRef.current); } catch (e) { setError(e.message || "Errore durante il caricamento del pagamento."); } })(); return () => { if (checkout) checkout.destroy(); }; }, [planId, interval, stripeCustomerId]); if (error) return (
{error}
); return
; } function PlanSelectPage({ onLogin, heading, subtitle, stripeCustomerId, billingStatus }) { const [interval, setInterval] = useStateLogin("monthly"); const [selectedPlan, setSelectedPlan] = useStateLogin(null); const selectPlan = (planId) => setSelectedPlan(planId); // Piano custom preassegnato dall'admin: mostra solo il CTA di attivazione const isCustomPlan = billingStatus?.plan_type === "custom"; const assignedPlanId = billingStatus?.plan; const assignedPlanName = billingStatus?.plan_display_name; if (isCustomPlan && assignedPlanId) { const hasStripePrices = billingStatus?.has_stripe_prices === true; return (

Il tuo account è pronto

Hai il piano {assignedPlanName} preconfigurato per te.
{hasStripePrices ? "Clicca qui sotto per completare l'attivazione e iniziare a usare Argo." : "Entra nel dashboard per iniziare a gestire le tue strutture."}

{hasStripePrices ? : ( ) }
); } return (

{heading || "Scegli il tuo piano"}

{subtitle || "Inizia subito — puoi cambiare piano in qualsiasi momento."}

{/* Toggle mensile / annuale */}
{["monthly", "yearly"].map(iv => ( ))}
{/* Layout: cards a sinistra, checkout a destra */}
{/* Colonna sinistra: plan cards */}
{PLANS_META.map(p => { const isSelected = selectedPlan === p.id; return (
selectPlan(p.id)} style={{ background: "white", borderRadius: 12, padding: "20px", width: 280, boxShadow: isSelected ? "0 0 0 2px var(--green-600), 0 8px 24px rgba(0,0,0,.08)" : p.highlight && !selectedPlan ? "0 0 0 2px var(--green-600), 0 8px 24px rgba(0,0,0,.08)" : "0 2px 12px rgba(0,0,0,.06)", display: "flex", gap: 14, cursor: "pointer", position: "relative", transition: "box-shadow .15s", }}> {p.highlight && !selectedPlan && (
Più popolare
)} {/* Radio */}
{isSelected &&
}
{p.name} {fmtPrice(p.monthlyPrice, interval)} /mese
{p.properties} {interval === "yearly" && ( Risparmi €{yearlySaving(p.monthlyPrice)}/anno )}
    {p.features.map(f =>
  • {f}
  • )}
); })}
{/* Colonna destra: Stripe Embedded Checkout */}
{selectedPlan && ( <>
Piano {PLANS_META.find(p => p.id === selectedPlan)?.name} · {interval === "monthly" ? "fatturazione mensile" : "fatturazione annuale"}
)}
); } function LoginPage({ onLogin, resetToken }) { const registrationSuccess = new URLSearchParams(window.location.search).get("registration_success") === "true"; const initialMode = resetToken ? "reset" : "signin"; const [mode, setMode] = useStateLogin(initialMode); const [email, setEmail] = useStateLogin(""); const [password, setPassword] = useStateLogin(""); const [name, setName] = useStateLogin(""); const [loading, setLoading] = useStateLogin(false); const [error, setError] = useStateLogin(null); const [success, setSuccess] = useStateLogin(registrationSuccess ? "Account attivato! Accedi con le tue credenziali per iniziare." : null ); // stripeCustomerId è impostato dopo la prima fase della registrazione const [pendingStripeCustomerId, setPendingStripeCustomerId] = useStateLogin(null); const switchMode = (m) => { setMode(m); setError(null); setSuccess(null); }; if (pendingStripeCustomerId) { return ( ); } const submit = async (e) => { e.preventDefault(); setError(null); setSuccess(null); setLoading(true); try { if (mode === "signin") { await window.API.login(email, password); onLogin(); } else if (mode === "signup") { // Fase 1: valida + crea customer Stripe + salva in Redis const res = await window.API.register(email, password, name); // Fase 2: mostra selezione piano con checkout anonimo setPendingStripeCustomerId(res.stripe_customer_id); } else if (mode === "forgot") { const res = await window.API.forgotPassword(email); setSuccess(res.detail); } else if (mode === "reset") { await window.API.resetPassword(resetToken, password); setSuccess("Password aggiornata! Puoi accedere con le nuove credenziali."); setTimeout(() => switchMode("signin"), 2000); } } catch (err) { setError(err.message); } finally { setLoading(false); } }; const isSignIn = mode === "signin"; return (
{/* Tabs signin/signup — nascosti in forgot e reset */} {(mode === "signin" || mode === "signup") && (
)}

{mode === "signin" && "Bentornato"} {mode === "signup" && "Inizia con Argo"} {mode === "forgot" && "Password dimenticata"} {mode === "reset" && "Scegli una nuova password"}

{mode === "signin" && "Accedi per gestire le tue strutture e il chatbot."} {mode === "signup" && "Crea il tuo account e imposta il chatbot in pochi minuti."} {mode === "forgot" && "Inserisci la tua email e ti invieremo un link per reimpostare la password."} {mode === "reset" && "Scegli una password sicura di almeno 8 caratteri."}

{mode === "signup" && (
setName(e.target.value)} placeholder="Marco Rossi" required />
)} {(mode === "signin" || mode === "signup" || mode === "forgot") && (
setEmail(e.target.value)} placeholder="tu@struttura.it" required />
)} {(mode === "signin" || mode === "signup" || mode === "reset") && (
{mode === "signin" && ( )}
setPassword(e.target.value)} placeholder={mode === "signin" ? "••••••••" : "Almeno 8 caratteri"} required />
)} {error && (
{error}
)} {success && (
{success}
)} {(!success || mode === "signin") && ( )}
{(mode === "forgot" || mode === "reset") && !success && (

)} {mode === "signup" && (

Creando un account accetti i Termini e la{" "} Privacy policy.

)}
); } window.LoginPage = LoginPage; // --------------------------------------------------------------------------- // ForceChangePasswordPage — mostrata all'host dopo il primo login con password temporanea // --------------------------------------------------------------------------- function ForceChangePasswordPage({ onDone, onLogout }) { const [currentPw, setCurrentPw] = useStateLogin(""); const [newPw, setNewPw] = useStateLogin(""); const [confirmPw, setConfirmPw] = useStateLogin(""); const [loading, setLoading] = useStateLogin(false); const [error, setError] = useStateLogin(null); const submit = async (e) => { e.preventDefault(); setError(null); if (newPw.length < 8) { setError("La nuova password deve essere di almeno 8 caratteri."); return; } if (newPw !== confirmPw) { setError("Le due password non coincidono."); return; } setLoading(true); try { await window.API.changePassword(currentPw, newPw); onDone(); } catch (err) { setError(err.message || "Errore durante il cambio password. Riprova."); } finally { setLoading(false); } }; return (

Imposta la tua password

Il tuo account è stato creato dall'amministratore con una password temporanea.
Scegli una password personale per continuare.

setCurrentPw(e.target.value)} placeholder="••••••••" required autoComplete="current-password" />
setNewPw(e.target.value)} placeholder="Almeno 8 caratteri" required autoComplete="new-password" />
setConfirmPw(e.target.value)} placeholder="Ripeti la password" required autoComplete="new-password" />
{error && (
{error}
)}
); } window.ForceChangePasswordPage = ForceChangePasswordPage;