diff --git a/main/docs/quickstart/webapp/rails/interactive.mdx b/main/docs/quickstart/webapp/rails/interactive.mdx index 3281b86ee..a597e8573 100644 --- a/main/docs/quickstart/webapp/rails/interactive.mdx +++ b/main/docs/quickstart/webapp/rails/interactive.mdx @@ -26,7 +26,9 @@ import {QuickstartButtons} from "/snippets/QuickstartButtons.jsx"; import {AuthCodeGroup} from "/snippets/AuthCodeGroup.jsx"; - +export const sampleAppUrl = "https://github.com/auth0-samples/auth0-rubyonrails-sample/tree/master/sample"; + + export const sections = [ { id: "configure-auth0", title: "Configure Auth0" }, @@ -243,11 +245,11 @@ export const sections = [ - + - + @@ -291,11 +293,11 @@ export const sections = [ - + - + diff --git a/main/snippets/recipe.jsx b/main/snippets/recipe.jsx index 2429fd925..90d78b1b9 100644 --- a/main/snippets/recipe.jsx +++ b/main/snippets/recipe.jsx @@ -19,9 +19,18 @@ export const Content = ({ title, children }) => { ); }; -export const Section = ({ id, title, stepNumber, children, isSingleColumn = false }) => { +export const Section = ({ + id, + title, + stepNumber, + children, + isSingleColumn = false, +}) => { return ( -
+
{/* OPTION WITH OPACITY
{ const sectionBottom = sectionTop + rect.height; const multiplier = viewportHeight > 1600 ? 0.34 : 0.22; - if (scrollY + viewportHeight * multiplier >= sectionTop && scrollY <= sectionBottom) { + if ( + scrollY + viewportHeight * multiplier >= sectionTop && + scrollY <= sectionBottom + ) { currentVisible = id; } } @@ -120,85 +132,333 @@ export const SideMenu = ({ sections, children }) => { export const SideMenuSectionItem = ({ id, children }) => { return ( -
+
{children}
); }; -export const SignUpForm = () => { - const [isAuthenticated, setIsAuthenticated] = useState(false); +export const SignUpForm = ({ + appType, // 'regular_web' | 'spa' | 'native' | 'non_interactive', + sampleAppUrl, +}) => { + const TOAST_DISPLAY_DURATION = 2200; + const [storeReady, setStoreReady] = useState(false); + const [toasts, setToasts] = useState([]); + const [route, setRoute] = useState("menu"); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [selectedClientId, setSelectedClientIdState] = useState(null); + const [selectedApiId, setSelectedApiIdState] = useState(null); + + // Side effects useEffect(() => { - let unsubscribe = null; - - function init() { - setStoreReady(true); - // Set up reactive subscription to auth state - unsubscribe = window.autorun(() => { - const authenticated = window.rootStore?.sessionStore?.isAuthenticated || false; - setIsAuthenticated(authenticated); - }); - } + const init = () => setStoreReady(true); if (window.rootStore) { init(); - } else { + } else if (typeof window !== "undefined") { window.addEventListener("adu:storeReady", init); } return () => { - window.removeEventListener("adu:storeReady", init); - unsubscribe?.(); + if (typeof window !== "undefined") { + window.removeEventListener("adu:storeReady", init); + } }; }, []); - /** Start LoggedInForm */ - /** - * Auth Flows Demo (no external UI libs) - * - Tailwind-only, production-ready structure - * - LocalStorage-backed app list + per-app URL config - * - 3 flows: - * 1) Menu: Create | Integrate | Sample - * 2) Create Application -> adds to list then routes to Integrate flow preselected - * 3) Integrate with Existing: select app + save URLs -> toast - */ - function LoggedInForm({ sampleApp }) { - /** Utilities */ - const LS_APPS_KEY = "auth_demo_apps"; // [{id, name}] - const LS_APP_CFG_KEY = "auth_demo_app_cfg"; // { [id]: { callbacks, logouts, origins } } - const CHANNEL = "auth_flows_sync_v1"; - const mkChannel = () => new BroadcastChannel(CHANNEL); - - function uid() { - return Math.random().toString(36).slice(2) + Date.now().toString(36); - } + useEffect(() => { + if (!storeReady) return; - function loadApps() { - const raw = localStorage.getItem(LS_APPS_KEY); - if (raw) return JSON.parse(raw); - // seed with the 3 shown in screenshots - const seeded = [{ id: "{yourClientId}", name: "Default App" }]; - localStorage.setItem(LS_APPS_KEY, JSON.stringify(seeded)); - return seeded; - } + const disposer = autorun(() => { + const rootStore = window.rootStore; + setIsAuthenticated(rootStore.sessionStore.isAuthenticated); + setSelectedClientIdState(rootStore.clientStore.selectedClientId ?? null); + setSelectedApiIdState( + rootStore.resourceServerStore.selectedApiId ?? null + ); + }); - function saveApps(apps) { - localStorage.setItem(LS_APPS_KEY, JSON.stringify(apps)); - } + return () => { + disposer(); + }; + }, [storeReady]); - function loadCfg() { - const raw = localStorage.getItem(LS_APP_CFG_KEY); - return raw ? JSON.parse(raw) : {}; - } + if (!storeReady || typeof window === "undefined") { + return <>; + } + + // Utility functions - function saveCfg(cfg) { - localStorage.setItem(LS_APP_CFG_KEY, JSON.stringify(cfg)); + const setSelectedClientId = (clientId) => { + window.rootStore.clientStore.setSelectedClient(clientId); + }; + + const setSelectedApiId = (apiId) => { + window.rootStore.resourceServerStore.setSelectedApi(apiId); + }; + + const showToast = (message, type = "success") => { + const id = `toast-${Date.now()}-${Math.random()}`; + const toast = { id, message, type }; + setToasts((prev) => [...prev, toast]); + + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== id)); + }, TOAST_DISPLAY_DURATION); + }; + + const dismissToast = (id) => { + setToasts((prev) => prev.filter((toast) => toast.id !== id)); + }; + + const cn = (...classes) => { + return classes.filter(Boolean).join(" "); + }; + + // Components + + function Toast({ open, onClose, children, type = "success" }) { + if (open) { + setTimeout(onClose, TOAST_DISPLAY_DURATION); } - /** Icons */ - const RightChevron = ({ className = "w-5 h-5", ...props }) => ( + const isError = type === "error"; + + const bgColorClass = isError ? "bg-red-500" : "bg-emerald-500"; + const ringColorClass = isError ? "ring-red-200" : "ring-emerald-200"; + const textColorClass = isError ? "text-red-600" : "text-emerald-600"; + + return ( +
+
+ + + {isError ? ( + // Error icon (X) + <> + + + ) : ( + // Success icon (checkmark) + + )} + + + {children} + +
+
+ ); + } + + function Wrapper({ children }) { + return ( + <> + {children} +
+ {toasts.map((toast) => ( + dismissToast(toast.id)} + type={toast.type} + > + {toast.message} + + ))} +
+ + ); + } + + function Card({ className = "", children }) { + return ( +
+ {children} +
+ ); + } + + function Button({ variant, children, className, ...props }) { + const commonClasses = + "inline-flex items-center justify-center gap-2 h-10 px-4 rounded-xl font-medium transition disabled:opacity-50 disabled:cursor-not-allowed"; + const variants = { + primary: + "mint-bg-indigo-600 text-white hover:mint-bg-indigo-700 disabled:hover:mint-bg-indigo-600", + outline: + "border border-zinc-300 dark:border-zinc-700 mint-bg-transparent hover:mint-bg-zinc-50 dark:hover:mint-bg-zinc-800 disabled:hover:mint-bg-transparent", + ghost: + "hover:mint-bg-zinc-100 dark:hover:mint-bg-zinc-800 disabled:hover:mint-bg-transparent", + }; + + return ( + + ); + } + + function Input({ id, label, value, onChange, placeholder, name }) { + return ( + + ); + } + + function Select({ label, value, onChange, options }) { + return ( + + ); + } + + function IconTile({ children }) { + return ( +
+ {children} +
+ ); + } + + function SignUpFormInternal() { + return ( +
+ Sign up for an Auth0 account + + Sign up for an{" "} + + Auth0 account + {" "} + or{" "} + console.log("log in")} + > + log in + {" "} + to your existing account to integrate directly with your own tenant. + + +
+ ); + } + + function RightChevron({ className = "w-5 h-5", ...props }) { + return ( { ); + } - const LightningIcon = () => ( - + function LightningIcon() { + return ( + { /> ); + } - const LayersIcon = () => ( - + function LayersIcon() { + return ( + { /> ); + } - const GithubIcon = () => ( + function GithubIcon() { + return ( { ); + } - function IconTile({ children }) { - return ( -
- {children} -
- ); - } + function Menu() { + const showBackendFlow = appType === "non_interactive"; - /** Basic UI atoms */ - function Card({ className = "", children }) { - return ( -
{children}
- ); - } + const createLabel = showBackendFlow + ? "Create a new API" + : "Create a new application"; - function Button({ variant = "primary", type = "button", onClick, children }) { - const base = "inline-flex items-center justify-center gap-2 h-10 px-4 rounded-xl font-medium transition"; - - let styles = ""; - if (variant === "primary") { - styles = "mint-bg-indigo-600 text-white hover:mint-bg-indigo-700"; - } else if (variant === "outline") { - styles = - "border border-zinc-300 dark:border-zinc-700 mint-bg-transparent hover:mint-bg-zinc-50 dark:hover:mint-bg-zinc-800"; - } else if (variant === "ghost") { - styles = "hover:mint-bg-zinc-100 dark:hover:mint-bg-zinc-800"; - } + const integrateLabel = showBackendFlow + ? "Integrate with an existing API" + : "Integrate with an existing application"; - return ( - - ); - } + const handleCreate = () => { + setRoute(showBackendFlow ? "createApi" : "create"); + }; - function Input({ id, label, value, onChange, placeholder, name }) { - return ( - - ); - } + const handleIntegrate = () => { + setRoute(showBackendFlow ? "integrateApi" : "integrate"); + }; - function Select({ label, value, onChange, options }) { - return ( - - ); - } + return ( + + ); + } - /** Toast */ - function Toast({ open, onClose, children }) { - useEffect(() => { - if (!open) return; - const t = setTimeout(onClose, 2200); - return () => clearTimeout(t); - }, [open, onClose]); - return ( -
-
- - - - - {children} -
-
- ); - } + function CreateClientForm({ onCancel }) { + const [name, setName] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); - function Flows() { - const [route, setRoute] = useState("menu"); // menu | create | integrate - const [apps, setApps] = useState(loadApps()); - const [cfg, setCfg] = useState(loadCfg()); - const [selected, setSelected] = useState(apps[0]?.id || ""); - const [toast, setToast] = useState(false); + const reset = () => { + setName(""); + setError(null); + if (onCancel) { + onCancel(); + } else { + setRoute("menu"); + } + }; - // one channel per instance - const [bc] = useState(() => mkChannel()); + const handleSubmit = async () => { + if (!name.trim()) { + setError("Application name is required"); + return; + } - // keep selected in sync if apps change - useEffect(() => { - if (!apps.find((a) => a.id === selected)) { - setSelected(apps[0]?.id || ""); - } - }, [apps, selected]); - - // listen to messages from other instances - useEffect(() => { - const onMsg = (e) => { - const { type, payload } = e.data || {}; - switch (type) { - case "NAV": - setRoute(payload.route); - break; - case "SELECT": - setSelected(payload.appId); - break; - case "APPS_UPDATED": - setApps(loadApps()); // pull latest from localStorage - break; - case "CFG_UPDATED": - setCfg(loadCfg()); // pull latest from localStorage - setToast(true); - break; - default: - break; - } - }; - bc.addEventListener("message", onMsg); - return () => bc.removeEventListener("message", onMsg); - }, [bc]); - - // helpers that also broadcast - const nav = (nextRoute) => { - setRoute(nextRoute); - bc.postMessage({ type: "NAV", payload: { route: nextRoute } }); - }; + setIsLoading(true); + setError(null); - const selectApp = (appId) => { - setSelected(appId); - bc.postMessage({ type: "SELECT", payload: { appId } }); - }; + try { + // Create client using the clientStore method + await window.rootStore.clientStore.createClient({ + name: name.trim(), + }); - const onCreate = (name) => { - const id = uid(); - const next = [...apps, { id, name: name || "Untitled" }]; - setApps(next); - saveApps(next); - bc.postMessage({ type: "APPS_UPDATED" }); + setName(""); + showToast("Application created successfully!", "success"); + reset(); + } catch (err) { + console.error("Error creating client:", err); + const errorMessage = + err instanceof Error ? err.message : "Failed to create application"; + setError(errorMessage); + showToast(errorMessage, "error"); + } finally { + setIsLoading(false); + } + }; - selectApp(id); - nav("integrate"); - }; + return ( +
+ +

+ You can change this later in the application settings. +

+ {error &&

{error}

} +
+ + +
+
+ ); + } - const onSaveCfg = (appId, data) => { - const next = { ...cfg, [appId]: data }; - setCfg(next); - saveCfg(next); - setToast(true); - bc.postMessage({ type: "CFG_UPDATED" }); - }; + function IntegrateClientForm({ clients, onCancel }) { + const [callbacks, setCallbacks] = useState(""); + const [logouts, setLogouts] = useState(""); + const [origins, setOrigins] = useState(""); + const [isLoading, setIsLoading] = useState(false); - return ( -
- {route === "menu" && nav("create")} onIntegrate={() => nav("integrate")} />} - - {route === "create" && nav("menu")} onSave={onCreate} />} - - {route === "integrate" && ( - onSaveCfg(selected, data)} - onCancel={() => nav("menu")} - /> - )} + const reset = () => { + if (onCancel) { + onCancel(); + } else { + setRoute("menu"); + } + }; - setToast(false)}> - Successfully saved your changes. - -
- ); - } + // Load stored config when selected client changes + useEffect(() => { + if (selectedClientId) { + const selectedClient = clients.find( + (c) => c.client_id === selectedClientId + ); + if (selectedClient) { + setCallbacks(selectedClient.callbacks?.join(", ") || ""); + setLogouts(""); + setOrigins(""); + } + } + }, [selectedClientId, clients]); - /** Views */ - function Menu({ onCreate, onIntegrate }) { + const handleSubmit = async (e) => { + e.preventDefault(); + setIsLoading(true); + try { + // TODO: Update client using the clientStore method + // await window.rootStore.clientStore.updateClient(selectedClientId, { + // callbacks: callbacks + // .split(",") + // .map((url) => url.trim()) + // .filter((url) => url), + // allowed_logout_urls: logouts + // .split(",") + // .map((url) => url.trim()) + // .filter((url) => url), + // web_origins: origins + // .split(",") + // .map((url) => url.trim()) + // .filter((url) => url), + // }); + showToast("Configuration saved successfully!", "success"); + reset(); + } catch (err) { + console.error("Failed to save configuration:", err); + showToast("Failed to save configuration", "error"); + } finally { + setIsLoading(false); + } + }; + + if (clients.length === 0) { return ( - +
+

+ No applications found. Please create one first. +

+ +
); } - function CreateForm({ onSave, onCancel }) { - const [name, setName] = useState(""); - return ( -
- -

You can change this later in the application settings.

-
- - +
+ +
+ ); + } + + function CreateApiForm({ onCancel }) { + const [name, setName] = useState(""); + const [identifier, setIdentifier] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const reset = () => { + setName(""); + setIdentifier(""); + setError(null); + if (onCancel) { + onCancel(); + } else { + setRoute("menu"); + } + }; + + const handleSubmit = async () => { + if (!name.trim()) { + setError("API name is required"); + return; + } + if (!identifier.trim()) { + setError("API identifier is required"); + return; + } + + setIsLoading(true); + setError(null); + + try { + // Create API using the resourceServerStore method + await rootStore.resourceServerStore.createApi({ + name: name.trim(), + identifier: identifier.trim(), + }); + + setName(""); + setIdentifier(""); + showToast("API created successfully!", "success"); + reset(); + } catch (err) { + console.error("Error creating API:", err); + const errorMessage = + err instanceof Error ? err.message : "Failed to create API"; + setError(errorMessage); + showToast(errorMessage, "error"); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + +

+ The identifier is used to uniquely identify your API. +

+ {error &&

{error}

} +
+ +
- ); - } +
+ ); + } - function IntegrateForm({ apps, selected, onSelect, saved, onSave, onCancel }) { - const [callbacks, setCallbacks] = useState(saved?.callbacks ?? ""); - const [logouts, setLogouts] = useState(saved?.logouts ?? ""); - const [origins, setOrigins] = useState(saved?.origins ?? ""); + function IntegrateApiForm({ apis, onCancel }) { + const [isLoading, setIsLoading] = useState(false); - useEffect(() => { - // when changing selection, load stored values if available - setCallbacks(loadCfg()[selected]?.callbacks ?? ""); - setLogouts(loadCfg()[selected]?.logouts ?? ""); - setOrigins(loadCfg()[selected]?.origins ?? ""); - }, [selected]); + const reset = () => { + if (onCancel) { + onCancel(); + } else { + setRoute("menu"); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setIsLoading(true); + try { + // TODO: Update selected API using the resourceServerStore method + // await rootStore.resourceServerStore.updateApi(selectedApiId, { + // // Include any necessary data for the update + // }); + showToast("API configuration saved successfully!", "success"); + reset(); + } catch (err) { + console.error("Failed to save API configuration:", err); + showToast("Failed to save API configuration", "error"); + } finally { + setIsLoading(false); + } + }; + if (apis.length === 0) { return (
-
- Select your Application - - - - -
- - -
- +

+ No APIs found. Please create one first. +

+
); } return ( -
- +
+
+ + Select your API + +