|
| 1 | +import { useMemo, useState } from "react"; |
| 2 | +import { |
| 3 | + KernelManagedAuth, |
| 4 | + type AuthErrorPayload, |
| 5 | + type AuthSuccessPayload, |
| 6 | + type ManagedAuthResponse, |
| 7 | +} from "@onkernel/managed-auth-react"; |
| 8 | +import "@onkernel/managed-auth-react/styles.css"; |
| 9 | +import { createMockFetch } from "./mockFetch"; |
| 10 | + |
| 11 | +type Mode = "mock" | "live"; |
| 12 | + |
| 13 | +export function App() { |
| 14 | + const params = useMemo(() => new URLSearchParams(window.location.search), []); |
| 15 | + const initialMode: Mode = params.get("session") ? "live" : "mock"; |
| 16 | + |
| 17 | + const [mode, setMode] = useState<Mode>(initialMode); |
| 18 | + const [theme, setTheme] = useState<"light" | "dark" | "auto">("auto"); |
| 19 | + const [domain, setDomain] = useState(params.get("domain") ?? "github.com"); |
| 20 | + const [sessionId, setSessionId] = useState( |
| 21 | + params.get("session") ?? "demo-session-id", |
| 22 | + ); |
| 23 | + const [handoffCode, setHandoffCode] = useState( |
| 24 | + params.get("code") ?? "demo-handoff-code", |
| 25 | + ); |
| 26 | + const [event, setEvent] = useState<string | null>(null); |
| 27 | + |
| 28 | + const mockFetch = useMemo( |
| 29 | + () => (mode === "mock" ? createMockFetch({ domain }) : undefined), |
| 30 | + [mode, domain], |
| 31 | + ); |
| 32 | + |
| 33 | + const handleSuccess = (p: AuthSuccessPayload) => { |
| 34 | + setEvent(`onSuccess → ${JSON.stringify(p)}`); |
| 35 | + }; |
| 36 | + const handleError = (p: AuthErrorPayload) => { |
| 37 | + setEvent(`onError → ${JSON.stringify(p)}`); |
| 38 | + }; |
| 39 | + |
| 40 | + return ( |
| 41 | + <div style={styles.page}> |
| 42 | + <aside style={styles.sidebar}> |
| 43 | + <h1 style={styles.h1}>@onkernel/managed-auth-react</h1> |
| 44 | + <p style={styles.muted}> |
| 45 | + A live render of <code><KernelManagedAuth /></code> exactly as a |
| 46 | + user would see it. |
| 47 | + </p> |
| 48 | + |
| 49 | + <section style={styles.section}> |
| 50 | + <label style={styles.label}>Mode</label> |
| 51 | + <div style={styles.row}> |
| 52 | + <button |
| 53 | + style={mode === "mock" ? styles.btnActive : styles.btn} |
| 54 | + onClick={() => setMode("mock")} |
| 55 | + > |
| 56 | + Mock backend |
| 57 | + </button> |
| 58 | + <button |
| 59 | + style={mode === "live" ? styles.btnActive : styles.btn} |
| 60 | + onClick={() => setMode("live")} |
| 61 | + > |
| 62 | + Live (api.onkernel.com) |
| 63 | + </button> |
| 64 | + </div> |
| 65 | + <p style={styles.hint}> |
| 66 | + {mode === "mock" |
| 67 | + ? "Uses an in-browser mock fetch so you can click through the entire flow without real Kernel credentials." |
| 68 | + : "Calls the real Kernel API. Provide a valid sessionId + handoffCode below."} |
| 69 | + </p> |
| 70 | + </section> |
| 71 | + |
| 72 | + {mode === "mock" ? ( |
| 73 | + <section style={styles.section}> |
| 74 | + <label style={styles.label}>Target domain</label> |
| 75 | + <input |
| 76 | + style={styles.input} |
| 77 | + value={domain} |
| 78 | + onChange={(e) => setDomain(e.target.value)} |
| 79 | + placeholder="github.com" |
| 80 | + /> |
| 81 | + <p style={styles.hint}> |
| 82 | + Drives the mocked discovery response (site name, icon, MFA |
| 83 | + fan-out). |
| 84 | + </p> |
| 85 | + </section> |
| 86 | + ) : ( |
| 87 | + <section style={styles.section}> |
| 88 | + <label style={styles.label}>Session ID</label> |
| 89 | + <input |
| 90 | + style={styles.input} |
| 91 | + value={sessionId} |
| 92 | + onChange={(e) => setSessionId(e.target.value)} |
| 93 | + /> |
| 94 | + <label style={{ ...styles.label, marginTop: 12 }}> |
| 95 | + Handoff code |
| 96 | + </label> |
| 97 | + <input |
| 98 | + style={styles.input} |
| 99 | + value={handoffCode} |
| 100 | + onChange={(e) => setHandoffCode(e.target.value)} |
| 101 | + /> |
| 102 | + </section> |
| 103 | + )} |
| 104 | + |
| 105 | + <section style={styles.section}> |
| 106 | + <label style={styles.label}>Theme</label> |
| 107 | + <div style={styles.row}> |
| 108 | + {(["light", "dark", "auto"] as const).map((t) => ( |
| 109 | + <button |
| 110 | + key={t} |
| 111 | + style={theme === t ? styles.btnActive : styles.btn} |
| 112 | + onClick={() => setTheme(t)} |
| 113 | + > |
| 114 | + {t} |
| 115 | + </button> |
| 116 | + ))} |
| 117 | + </div> |
| 118 | + </section> |
| 119 | + |
| 120 | + <section style={styles.section}> |
| 121 | + <label style={styles.label}>Last event</label> |
| 122 | + <pre style={styles.event}>{event ?? "(none yet)"}</pre> |
| 123 | + </section> |
| 124 | + </aside> |
| 125 | + |
| 126 | + <main style={styles.stage}> |
| 127 | + <div style={styles.surface} data-theme={theme}> |
| 128 | + <KernelManagedAuth |
| 129 | + key={`${mode}-${domain}-${sessionId}-${handoffCode}`} |
| 130 | + sessionId={sessionId} |
| 131 | + handoffCode={handoffCode} |
| 132 | + appearance={{ theme }} |
| 133 | + onSuccess={handleSuccess} |
| 134 | + onError={handleError} |
| 135 | + fetch={mockFetch as typeof fetch | undefined} |
| 136 | + /> |
| 137 | + </div> |
| 138 | + </main> |
| 139 | + </div> |
| 140 | + ); |
| 141 | +} |
| 142 | + |
| 143 | +const styles: Record<string, React.CSSProperties> = { |
| 144 | + page: { |
| 145 | + display: "grid", |
| 146 | + gridTemplateColumns: "360px 1fr", |
| 147 | + minHeight: "100vh", |
| 148 | + fontFamily: |
| 149 | + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif', |
| 150 | + background: "#0b0d12", |
| 151 | + color: "#e6e8ee", |
| 152 | + }, |
| 153 | + sidebar: { |
| 154 | + padding: 24, |
| 155 | + borderRight: "1px solid #1d2230", |
| 156 | + background: "#0f121a", |
| 157 | + overflowY: "auto", |
| 158 | + }, |
| 159 | + stage: { |
| 160 | + display: "flex", |
| 161 | + alignItems: "center", |
| 162 | + justifyContent: "center", |
| 163 | + padding: 32, |
| 164 | + background: |
| 165 | + "radial-gradient(1200px 600px at 50% -10%, rgba(99,102,241,0.18), transparent 60%), #0b0d12", |
| 166 | + }, |
| 167 | + surface: { |
| 168 | + width: "100%", |
| 169 | + maxWidth: 460, |
| 170 | + }, |
| 171 | + h1: { fontSize: 16, margin: "0 0 8px", fontWeight: 600 }, |
| 172 | + muted: { |
| 173 | + fontSize: 13, |
| 174 | + color: "#8a93a6", |
| 175 | + margin: "0 0 20px", |
| 176 | + lineHeight: 1.5, |
| 177 | + }, |
| 178 | + section: { marginBottom: 20 }, |
| 179 | + label: { |
| 180 | + display: "block", |
| 181 | + fontSize: 11, |
| 182 | + textTransform: "uppercase", |
| 183 | + letterSpacing: "0.08em", |
| 184 | + color: "#8a93a6", |
| 185 | + marginBottom: 8, |
| 186 | + }, |
| 187 | + hint: { fontSize: 12, color: "#6b7387", marginTop: 8, lineHeight: 1.5 }, |
| 188 | + row: { display: "flex", gap: 6, flexWrap: "wrap" }, |
| 189 | + btn: { |
| 190 | + padding: "6px 10px", |
| 191 | + fontSize: 12, |
| 192 | + background: "#161b27", |
| 193 | + color: "#cfd4e1", |
| 194 | + border: "1px solid #232a3a", |
| 195 | + borderRadius: 6, |
| 196 | + cursor: "pointer", |
| 197 | + }, |
| 198 | + btnActive: { |
| 199 | + padding: "6px 10px", |
| 200 | + fontSize: 12, |
| 201 | + background: "#3b82f6", |
| 202 | + color: "white", |
| 203 | + border: "1px solid #3b82f6", |
| 204 | + borderRadius: 6, |
| 205 | + cursor: "pointer", |
| 206 | + }, |
| 207 | + input: { |
| 208 | + width: "100%", |
| 209 | + padding: "8px 10px", |
| 210 | + fontSize: 13, |
| 211 | + background: "#161b27", |
| 212 | + color: "#e6e8ee", |
| 213 | + border: "1px solid #232a3a", |
| 214 | + borderRadius: 6, |
| 215 | + boxSizing: "border-box", |
| 216 | + }, |
| 217 | + event: { |
| 218 | + margin: 0, |
| 219 | + padding: 10, |
| 220 | + background: "#0a0d14", |
| 221 | + border: "1px solid #1d2230", |
| 222 | + borderRadius: 6, |
| 223 | + fontSize: 12, |
| 224 | + color: "#cfd4e1", |
| 225 | + whiteSpace: "pre-wrap", |
| 226 | + wordBreak: "break-all", |
| 227 | + }, |
| 228 | +}; |
| 229 | + |
| 230 | +// Re-export the type for explicit typing in styles map. |
| 231 | +export type { ManagedAuthResponse }; |
0 commit comments