@authrobo/react 0.2.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,11 +2,27 @@
2
2
  "use client";
3
3
 
4
4
  // src/index.tsx
5
- import { createContext, useContext, useEffect, useState } from "react";
5
+ import {
6
+ createContext,
7
+ useCallback,
8
+ useContext,
9
+ useEffect,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ useSyncExternalStore
14
+ } from "react";
6
15
 
7
16
  // src/icons.tsx
8
17
  import { jsx, jsxs } from "react/jsx-runtime";
9
18
  var sz = { width: 18, height: 18, "aria-hidden": true };
19
+ var LINKEDIN_PATH = "M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.45v6.29zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0z";
20
+ var GITHUB_PATH = "M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-6a4.7 4.7 0 0 1 1.2-3.2c-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 2.9.1 3.2a4.7 4.7 0 0 1 1.2 3.2c0 4.7-2.8 5.7-5.5 6 .4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3";
21
+ var APPLE_PATH = "M17.05 12.04c-.03-2.7 2.2-4 2.3-4.06-1.25-1.84-3.2-2.09-3.9-2.12-1.66-.17-3.24.98-4.08.98-.84 0-2.14-.96-3.52-.93-1.81.03-3.48 1.05-4.41 2.67-1.88 3.27-.48 8.1 1.35 10.76.9 1.3 1.97 2.76 3.38 2.71 1.36-.05 1.87-.88 3.51-.88 1.64 0 2.1.88 3.53.85 1.46-.03 2.38-1.33 3.27-2.63 1.03-1.51 1.46-2.97 1.48-3.05-.03-.01-2.84-1.09-2.87-4.32M14.4 4.2c.74-.9 1.24-2.15 1.1-3.4-1.07.04-2.36.71-3.13 1.61-.69.8-1.29 2.07-1.13 3.29 1.19.09 2.41-.61 3.16-1.5";
22
+ var FACEBOOK_PATH = "M24 12.07C24 5.4 18.63 0 12 0S0 5.4 0 12.07C0 18.1 4.39 23.1 10.13 24v-8.44H7.08v-3.49h3.05V9.41c0-3.02 1.79-4.69 4.53-4.69 1.31 0 2.68.24 2.68.24v2.97h-1.51c-1.49 0-1.95.93-1.95 1.88v2.26h3.32l-.53 3.49h-2.79V24C19.61 23.1 24 18.1 24 12.07";
23
+ function Glyph({ d, fill }) {
24
+ return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", ...sz, fill, children: /* @__PURE__ */ jsx("path", { d }) });
25
+ }
10
26
  function GoogleIcon() {
11
27
  return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 48 48", ...sz, children: [
12
28
  /* @__PURE__ */ jsx("path", { fill: "#FFC107", d: "M43.6 20.5H42V20H24v8h11.3C33.7 32.9 29.3 36 24 36c-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.9 1.2 8 3.1l5.7-5.7C34.6 6.1 29.6 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.3-.1-2.3-.4-3.5z" }),
@@ -15,15 +31,6 @@ function GoogleIcon() {
15
31
  /* @__PURE__ */ jsx("path", { fill: "#1976D2", d: "M43.6 20.5H42V20H24v8h11.3c-.8 2.2-2.2 4.1-4.1 5.6l6.3 5.2C41.9 36 44 30.5 44 24c0-1.3-.1-2.3-.4-3.5z" })
16
32
  ] });
17
33
  }
18
- function LinkedInIcon() {
19
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", ...sz, fill: "#0A66C2", children: /* @__PURE__ */ jsx("path", { d: "M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.41v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.45v6.29zM5.34 7.43a2.06 2.06 0 1 1 0-4.13 2.06 2.06 0 0 1 0 4.13zM7.12 20.45H3.56V9h3.56v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.56C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.72V1.72C24 .77 23.2 0 22.22 0z" }) });
20
- }
21
- function GitHubIcon() {
22
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", ...sz, fill: "#181717", children: /* @__PURE__ */ jsx("path", { d: "M12 .3a12 12 0 0 0-3.8 23.4c.6.1.8-.3.8-.6v-2c-3.3.7-4-1.6-4-1.6-.6-1.4-1.3-1.8-1.3-1.8-1.1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1.1 1.8 2.8 1.3 3.5 1 .1-.8.4-1.3.8-1.6-2.7-.3-5.5-1.3-5.5-6a4.7 4.7 0 0 1 1.2-3.2c-.1-.3-.5-1.5.1-3.2 0 0 1-.3 3.3 1.2a11.5 11.5 0 0 1 6 0c2.3-1.5 3.3-1.2 3.3-1.2.6 1.7.2 2.9.1 3.2a4.7 4.7 0 0 1 1.2 3.2c0 4.7-2.8 5.7-5.5 6 .4.4.8 1.1.8 2.2v3.3c0 .3.2.7.8.6A12 12 0 0 0 12 .3" }) });
23
- }
24
- function AppleIcon() {
25
- return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", ...sz, fill: "#000", children: /* @__PURE__ */ jsx("path", { d: "M17.05 12.04c-.03-2.7 2.2-4 2.3-4.06-1.25-1.84-3.2-2.09-3.9-2.12-1.66-.17-3.24.98-4.08.98-.84 0-2.14-.96-3.52-.93-1.81.03-3.48 1.05-4.41 2.67-1.88 3.27-.48 8.1 1.35 10.76.9 1.3 1.97 2.76 3.38 2.71 1.36-.05 1.87-.88 3.51-.88 1.64 0 2.1.88 3.53.85 1.46-.03 2.38-1.33 3.27-2.63 1.03-1.51 1.46-2.97 1.48-3.05-.03-.01-2.84-1.09-2.87-4.32M14.4 4.2c.74-.9 1.24-2.15 1.1-3.4-1.07.04-2.36.71-3.13 1.61-.69.8-1.29 2.07-1.13 3.29 1.19.09 2.41-.61 3.16-1.5" }) });
26
- }
27
34
  function MailIcon() {
28
35
  return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", ...sz, fill: "none", stroke: "#334155", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", children: [
29
36
  /* @__PURE__ */ jsx("rect", { x: "3", y: "5", width: "18", height: "14", rx: "2" }),
@@ -32,20 +39,37 @@ function MailIcon() {
32
39
  }
33
40
  var PROVIDER_META = {
34
41
  google: { label: "Google", icon: /* @__PURE__ */ jsx(GoogleIcon, {}) },
35
- linkedin: { label: "LinkedIn", icon: /* @__PURE__ */ jsx(LinkedInIcon, {}) },
36
- github: { label: "GitHub", icon: /* @__PURE__ */ jsx(GitHubIcon, {}) },
37
- apple: { label: "Apple", icon: /* @__PURE__ */ jsx(AppleIcon, {}) },
42
+ linkedin: { label: "LinkedIn", icon: /* @__PURE__ */ jsx(Glyph, { d: LINKEDIN_PATH, fill: "#0A66C2" }) },
43
+ github: { label: "GitHub", icon: /* @__PURE__ */ jsx(Glyph, { d: GITHUB_PATH, fill: "#181717" }) },
44
+ apple: { label: "Apple", icon: /* @__PURE__ */ jsx(Glyph, { d: APPLE_PATH, fill: "#000" }) },
45
+ facebook: { label: "Facebook", icon: /* @__PURE__ */ jsx(Glyph, { d: FACEBOOK_PATH, fill: "#1877F2" }) },
38
46
  password: { label: "email", icon: /* @__PURE__ */ jsx(MailIcon, {}) },
39
47
  magic_link: { label: "magic link", icon: /* @__PURE__ */ jsx(MailIcon, {}) }
40
48
  };
49
+ var WHITE_MARKS = {
50
+ linkedin: /* @__PURE__ */ jsx(Glyph, { d: LINKEDIN_PATH, fill: "#fff" }),
51
+ github: /* @__PURE__ */ jsx(Glyph, { d: GITHUB_PATH, fill: "#fff" }),
52
+ apple: /* @__PURE__ */ jsx(Glyph, { d: APPLE_PATH, fill: "#fff" }),
53
+ facebook: /* @__PURE__ */ jsx(Glyph, { d: FACEBOOK_PATH, fill: "#fff" })
54
+ };
55
+ var BRAND_STYLE = {
56
+ google: { bg: "#ffffff", fg: "#1a73e8", border: "#dadce0" },
57
+ linkedin: { bg: "#0a66c2", fg: "#ffffff", border: "#0a66c2", whiteIcon: true },
58
+ github: { bg: "#181717", fg: "#ffffff", border: "#181717", whiteIcon: true },
59
+ apple: { bg: "#000000", fg: "#ffffff", border: "#000000", whiteIcon: true },
60
+ facebook: { bg: "#1877f2", fg: "#ffffff", border: "#1877f2", whiteIcon: true }
61
+ };
41
62
 
42
63
  // src/index.tsx
43
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
64
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
44
65
  var ConfigContext = createContext(null);
45
66
  function AuthRoboProvider({
46
67
  config,
47
68
  children
48
69
  }) {
70
+ useEffect(() => {
71
+ prefetchAppConfig(config);
72
+ }, [config.issuerUrl, config.clientId]);
49
73
  return /* @__PURE__ */ jsx2(ConfigContext.Provider, { value: config, children });
50
74
  }
51
75
  function useResolvedConfig(override) {
@@ -65,6 +89,18 @@ function randomState() {
65
89
  }
66
90
  var EMAIL_METHODS = ["password", "magic_link"];
67
91
  function startSignIn(config, method, opts) {
92
+ try {
93
+ localStorage.setItem(lastMethodKey(config.clientId), method);
94
+ } catch {
95
+ }
96
+ if (opts?.startPath) {
97
+ const p = new URLSearchParams();
98
+ if (opts.loginHint) p.set("login_hint", opts.loginHint);
99
+ if (opts.screen) p.set("screen", opts.screen);
100
+ const qs = p.toString();
101
+ window.location.assign(`${opts.startPath.replace(/\/$/, "")}/${method}/start${qs ? `?${qs}` : ""}`);
102
+ return;
103
+ }
68
104
  const base = config.issuerUrl.replace(/\/$/, "");
69
105
  const state = opts?.state ?? randomState();
70
106
  try {
@@ -77,12 +113,16 @@ function startSignIn(config, method, opts) {
77
113
  state
78
114
  });
79
115
  if (opts?.loginHint) params.set("login_hint", opts.loginHint);
80
- try {
81
- localStorage.setItem(lastMethodKey(config.clientId), method);
82
- } catch {
116
+ let path;
117
+ if (method === "magic_link") {
118
+ params.set("screen", "magic");
119
+ path = "/login";
120
+ } else if (method === "password") {
121
+ path = opts?.screen === "register" ? "/register" : "/login";
122
+ } else {
123
+ path = `/api/v1/auth/${method}/start`;
83
124
  }
84
- const url = EMAIL_METHODS.includes(method) ? `${base}/login?${params.toString()}` : `${base}/api/v1/auth/${method}/start?${params.toString()}`;
85
- window.location.assign(url);
125
+ window.location.assign(`${base}${path}?${params.toString()}`);
86
126
  }
87
127
  var lastMethodKey = (clientId) => `authrobo:last-method:${clientId}`;
88
128
  function useLastMethod(clientId) {
@@ -95,28 +135,54 @@ function useLastMethod(clientId) {
95
135
  }, [clientId]);
96
136
  return last;
97
137
  }
138
+ var LOADING_STATE = { data: null, loading: true, error: null };
139
+ var configStore = /* @__PURE__ */ new Map();
140
+ var configKey = (c) => `${c.issuerUrl.replace(/\/$/, "")}|${c.clientId}`;
141
+ function configEntry(key) {
142
+ let e = configStore.get(key);
143
+ if (!e) {
144
+ e = { state: LOADING_STATE, listeners: /* @__PURE__ */ new Set() };
145
+ configStore.set(key, e);
146
+ }
147
+ return e;
148
+ }
149
+ function prefetchAppConfig(config) {
150
+ if (typeof window === "undefined") return Promise.resolve();
151
+ const key = configKey(config);
152
+ const e = configEntry(key);
153
+ if (e.state.data || e.promise) return e.promise ?? Promise.resolve();
154
+ const base = config.issuerUrl.replace(/\/$/, "");
155
+ e.promise = fetch(`${base}/api/v1/config?client_id=${encodeURIComponent(config.clientId)}`).then((r) => r.ok ? r.json() : Promise.reject(new Error(`config ${r.status}`))).then((j) => {
156
+ e.state = {
157
+ data: { name: j.name, brandColor: j.brand_color, logoUrl: j.logo_url, methods: j.methods },
158
+ loading: false,
159
+ error: null
160
+ };
161
+ }).catch((err) => {
162
+ e.state = { data: null, loading: false, error: String(err?.message ?? err) };
163
+ }).finally(() => {
164
+ e.promise = void 0;
165
+ e.listeners.forEach((l) => l());
166
+ });
167
+ return e.promise;
168
+ }
98
169
  function useAppConfig(config) {
99
170
  const c = useResolvedConfig(config);
100
- const [state, setState] = useState({
101
- data: null,
102
- loading: true,
103
- error: null
104
- });
171
+ const key = configKey(c);
172
+ const subscribe = useCallback(
173
+ (cb) => {
174
+ const e = configEntry(key);
175
+ e.listeners.add(cb);
176
+ return () => {
177
+ e.listeners.delete(cb);
178
+ };
179
+ },
180
+ [key]
181
+ );
182
+ const getSnapshot = useCallback(() => configEntry(key).state, [key]);
183
+ const state = useSyncExternalStore(subscribe, getSnapshot, () => LOADING_STATE);
105
184
  useEffect(() => {
106
- let active = true;
107
- setState({ data: null, loading: true, error: null });
108
- const base = c.issuerUrl.replace(/\/$/, "");
109
- fetch(`${base}/api/v1/config?client_id=${encodeURIComponent(c.clientId)}`).then((r) => r.ok ? r.json() : Promise.reject(new Error(`config ${r.status}`))).then((j) => {
110
- if (!active) return;
111
- setState({
112
- data: { name: j.name, brandColor: j.brand_color, logoUrl: j.logo_url, methods: j.methods },
113
- loading: false,
114
- error: null
115
- });
116
- }).catch((e) => active && setState({ data: null, loading: false, error: String(e?.message ?? e) }));
117
- return () => {
118
- active = false;
119
- };
185
+ prefetchAppConfig(c);
120
186
  }, [c.issuerUrl, c.clientId]);
121
187
  return state;
122
188
  }
@@ -128,7 +194,9 @@ var TOKENS_LIGHT = `
128
194
  --authrobo-error:#b91c1c;--authrobo-divider-fg:#9ca3af;--authrobo-divider-line:#e5e7eb;
129
195
  --authrobo-overlay:rgba(2,6,23,.55);--authrobo-close:#94a3b8;--authrobo-close-hover:#334155;
130
196
  --authrobo-focus:#2b8aff;--authrobo-card-shadow:rgba(2,6,23,.4);
131
- --authrobo-last-bg:#e7efff;--authrobo-last-fg:#2563eb;`;
197
+ --authrobo-last-bg:#e7efff;--authrobo-last-fg:#2563eb;
198
+ --authrobo-input-bg:#fff;--authrobo-input-fg:#111827;
199
+ --authrobo-tabs-bg:#f1f5f9;--authrobo-accent:#4338ca;--authrobo-accent-fg:#fff;`;
132
200
  var TOKENS_DARK = `
133
201
  --authrobo-btn-bg:#0e1626;--authrobo-btn-fg:#e6edf8;--authrobo-btn-border:#28374f;
134
202
  --authrobo-btn-hover-bg:#16233a;--authrobo-btn-hover-border:#3a4d6b;
@@ -136,7 +204,9 @@ var TOKENS_DARK = `
136
204
  --authrobo-error:#f87171;--authrobo-divider-fg:#64748b;--authrobo-divider-line:#223049;
137
205
  --authrobo-overlay:rgba(2,6,23,.72);--authrobo-close:#64748b;--authrobo-close-hover:#cbd5e1;
138
206
  --authrobo-focus:#46c8ff;--authrobo-card-shadow:rgba(0,0,0,.6);
139
- --authrobo-last-bg:rgba(70,160,255,.18);--authrobo-last-fg:#8fc4ff;`;
207
+ --authrobo-last-bg:rgba(70,160,255,.18);--authrobo-last-fg:#8fc4ff;
208
+ --authrobo-input-bg:#0e1626;--authrobo-input-fg:#e6edf8;
209
+ --authrobo-tabs-bg:#0e1626;--authrobo-accent:#4338ca;--authrobo-accent-fg:#fff;`;
140
210
  var TOKEN_ROOTS = ".authrobo-btn,.authrobo-stack,.authrobo-card,.authrobo-overlay,.authrobo-divider,.authrobo-title";
141
211
  var scoped = (prefix, tokens) => TOKEN_ROOTS.split(",").map((s) => `${prefix}${s.trim()}`).join(",") + `{${tokens}}`;
142
212
  var CSS = `
@@ -159,6 +229,33 @@ ${scoped('[data-theme="dark"] ', TOKENS_DARK)}
159
229
  .authrobo-error{color:var(--authrobo-error);font:14px system-ui,sans-serif;margin:0;}
160
230
  .authrobo-divider{display:flex;align-items:center;gap:10px;color:var(--authrobo-divider-fg);font:500 12px system-ui,sans-serif;}
161
231
  .authrobo-divider::before,.authrobo-divider::after{content:"";height:1px;flex:1;background:var(--authrobo-divider-line);}
232
+ .authrobo-tabs{display:flex;gap:4px;padding:4px;border:1px solid var(--authrobo-btn-border);
233
+ background:var(--authrobo-tabs-bg);border-radius:10px;}
234
+ .authrobo-tab{flex:1;border:0;background:transparent;border-radius:7px;padding:8px 0;cursor:pointer;
235
+ font:600 13px system-ui,sans-serif;color:var(--authrobo-muted);transition:background .15s ease,color .15s ease;}
236
+ .authrobo-tab:hover{color:var(--authrobo-title-fg);}
237
+ .authrobo-tab[aria-selected="true"]{background:var(--authrobo-accent);color:var(--authrobo-accent-fg);}
238
+ .authrobo-tab:focus-visible{outline:2px solid var(--authrobo-focus);outline-offset:2px;}
239
+ .authrobo-form{display:flex;flex-direction:column;gap:10px;}
240
+ .authrobo-label{font:500 12.5px system-ui,sans-serif;color:var(--authrobo-muted);}
241
+ .authrobo-input{width:100%;box-sizing:border-box;margin-top:6px;border:1px solid var(--authrobo-btn-border);
242
+ background:var(--authrobo-input-bg);color:var(--authrobo-input-fg);border-radius:10px;padding:10px 12px;
243
+ font:14px system-ui,sans-serif;}
244
+ .authrobo-input:focus{outline:none;border-color:var(--authrobo-accent);}
245
+ .authrobo-primary{display:flex;width:100%;align-items:center;justify-content:center;gap:8px;border:0;
246
+ background:var(--authrobo-accent);color:var(--authrobo-accent-fg);border-radius:10px;padding:11px 14px;
247
+ font:700 14px system-ui,sans-serif;cursor:pointer;transition:filter .15s ease;}
248
+ .authrobo-primary:hover{filter:brightness(.96);}
249
+ .authrobo-primary:focus-visible{outline:2px solid var(--authrobo-focus);outline-offset:2px;}
250
+ .authrobo-primary:disabled{opacity:.7;cursor:default;}
251
+ .authrobo-btn-brand{transition:filter .15s ease,box-shadow .15s ease;}
252
+ .authrobo-btn-brand:hover{filter:brightness(.96);}
253
+ .authrobo-spinner{display:inline-block;width:1.15em;height:1.15em;vertical-align:-0.2em;border-radius:50%;
254
+ border:2px solid color-mix(in srgb, currentColor 24%, transparent);border-top-color:currentColor;
255
+ animation:authrobo-spin .7s linear infinite;}
256
+ .authrobo-loading{display:flex;align-items:center;gap:10px;padding:4px 0;}
257
+ @keyframes authrobo-spin{to{transform:rotate(360deg)}}
258
+ @media (prefers-reduced-motion: reduce){.authrobo-spinner{animation-duration:1.4s}}
162
259
  .authrobo-overlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;
163
260
  justify-content:center;padding:16px;background:var(--authrobo-overlay);backdrop-filter:blur(2px);
164
261
  animation:authrobo-fade .16s ease-out both;}
@@ -180,6 +277,17 @@ function useInjectStyles() {
180
277
  document.head.appendChild(el);
181
278
  }, []);
182
279
  }
280
+ function Spinner({ style, className }) {
281
+ return /* @__PURE__ */ jsx2(
282
+ "span",
283
+ {
284
+ className: className ? `authrobo-spinner ${className}` : "authrobo-spinner",
285
+ role: "status",
286
+ "aria-label": "Loading",
287
+ style
288
+ }
289
+ );
290
+ }
183
291
  function AuthRoboButton({
184
292
  provider,
185
293
  config,
@@ -187,36 +295,216 @@ function AuthRoboButton({
187
295
  className,
188
296
  style,
189
297
  onClick,
190
- lastUsed
298
+ lastUsed,
299
+ startPath,
300
+ buttonStyle = "neutral"
191
301
  }) {
192
302
  useInjectStyles();
193
303
  const c = useResolvedConfig(config);
194
304
  const meta = PROVIDER_META[provider];
305
+ const brand = buttonStyle === "branded" ? BRAND_STYLE[provider] : void 0;
306
+ const icon = brand?.whiteIcon ? WHITE_MARKS[provider] ?? meta.icon : meta.icon;
307
+ const classes = ["authrobo-btn", brand ? "authrobo-btn-brand" : "", className].filter(Boolean).join(" ");
308
+ const brandStyle = brand ? { background: brand.bg, color: brand.fg, borderColor: brand.border } : void 0;
195
309
  return /* @__PURE__ */ jsxs2(
196
310
  "button",
197
311
  {
198
312
  type: "button",
199
- className: className ? `authrobo-btn ${className}` : "authrobo-btn",
200
- style,
313
+ className: classes,
314
+ style: { ...brandStyle, ...style },
201
315
  onClick: () => {
202
316
  onClick?.();
203
- startSignIn(c, provider);
317
+ startSignIn(c, provider, { startPath });
204
318
  },
205
319
  children: [
206
- /* @__PURE__ */ jsx2("span", { style: { display: "inline-flex", alignItems: "center" }, children: meta.icon }),
320
+ /* @__PURE__ */ jsx2("span", { style: { display: "inline-flex", alignItems: "center" }, children: icon }),
207
321
  label ?? `Continue with ${meta.label}`,
208
322
  lastUsed && /* @__PURE__ */ jsx2("span", { className: "authrobo-last", children: "Last used" })
209
323
  ]
210
324
  }
211
325
  );
212
326
  }
327
+ var EMAIL_TABS = [
328
+ { key: "magic", method: "magic_link", label: "Magic Link", cta: "Email me a link", requires: "magic_link", fields: { name: false, password: false }, proxyPath: "magic" },
329
+ { key: "register", method: "password", label: "Register", cta: "Create account", requires: "password", fields: { name: true, password: true }, proxyPath: "register" },
330
+ { key: "login", method: "password", label: "Login", cta: "Sign in", requires: "password", fields: { name: false, password: true }, proxyPath: "login" }
331
+ ];
332
+ function EmailSwitcher({
333
+ config,
334
+ methods,
335
+ startPath,
336
+ onSuccess
337
+ }) {
338
+ const tabs = useMemo(() => EMAIL_TABS.filter((t) => methods.includes(t.requires)), [methods]);
339
+ const [active, setActive] = useState(tabs[0]?.key ?? "login");
340
+ const [name, setName] = useState("");
341
+ const [email, setEmail] = useState("");
342
+ const [password, setPassword] = useState("");
343
+ const [loading, setLoading] = useState(false);
344
+ const [error, setError] = useState(null);
345
+ const [sent, setSent] = useState(false);
346
+ const stateRef = useRef(null);
347
+ const proxy = !!startPath;
348
+ useEffect(() => {
349
+ if (tabs.length && !tabs.some((t) => t.key === active)) setActive(tabs[0].key);
350
+ }, [tabs, active]);
351
+ if (!tabs.length) return null;
352
+ const current = tabs.find((t) => t.key === active) ?? tabs[0];
353
+ const base = config.issuerUrl.replace(/\/$/, "");
354
+ const directAction = current.key === "magic" ? `${base}/api/v1/auth/magic/start` : `${base}/api/v1/auth/authorize`;
355
+ const selectTab = (key) => {
356
+ setActive(key);
357
+ setError(null);
358
+ setSent(false);
359
+ };
360
+ async function proxySubmit(e) {
361
+ e.preventDefault();
362
+ setError(null);
363
+ setLoading(true);
364
+ try {
365
+ const body = { email: email.trim() };
366
+ if (current.fields.password) body.password = password;
367
+ if (current.fields.name && name.trim()) body.name = name.trim();
368
+ const res = await fetch(`${startPath.replace(/\/$/, "")}/${current.proxyPath}`, {
369
+ method: "POST",
370
+ headers: { "content-type": "application/json" },
371
+ body: JSON.stringify(body)
372
+ });
373
+ if (!res.ok) {
374
+ const d = await res.json().catch(() => ({}));
375
+ throw new Error(d?.error || "Something went wrong. Please try again.");
376
+ }
377
+ try {
378
+ localStorage.setItem(lastMethodKey(config.clientId), current.method);
379
+ } catch {
380
+ }
381
+ if (current.key === "magic") setSent(true);
382
+ else onSuccess?.();
383
+ } catch (err) {
384
+ setError(err instanceof Error ? err.message : "Something went wrong.");
385
+ } finally {
386
+ setLoading(false);
387
+ }
388
+ }
389
+ function directSubmit() {
390
+ const s = randomState();
391
+ try {
392
+ sessionStorage.setItem("authrobo:state", s);
393
+ } catch {
394
+ }
395
+ if (stateRef.current) stateRef.current.value = s;
396
+ try {
397
+ localStorage.setItem(lastMethodKey(config.clientId), current.method);
398
+ } catch {
399
+ }
400
+ setLoading(true);
401
+ }
402
+ if (sent) {
403
+ return /* @__PURE__ */ jsxs2("div", { className: "authrobo-stack", role: "status", children: [
404
+ /* @__PURE__ */ jsx2("p", { className: "authrobo-title", children: "Check your email" }),
405
+ /* @__PURE__ */ jsxs2("p", { className: "authrobo-muted", children: [
406
+ "We sent a sign-in link to ",
407
+ /* @__PURE__ */ jsx2("b", { children: email.trim() }),
408
+ ". It works once and expires shortly."
409
+ ] })
410
+ ] });
411
+ }
412
+ return /* @__PURE__ */ jsxs2("div", { className: "authrobo-stack", children: [
413
+ tabs.length > 1 && /* @__PURE__ */ jsx2("div", { className: "authrobo-tabs", role: "tablist", "aria-label": "Sign-in method", children: tabs.map((t) => /* @__PURE__ */ jsx2(
414
+ "button",
415
+ {
416
+ type: "button",
417
+ role: "tab",
418
+ "aria-selected": current.key === t.key,
419
+ className: "authrobo-tab",
420
+ onClick: () => selectTab(t.key),
421
+ children: t.label
422
+ },
423
+ t.key
424
+ )) }),
425
+ /* @__PURE__ */ jsxs2(
426
+ "form",
427
+ {
428
+ className: "authrobo-form",
429
+ method: proxy ? void 0 : "post",
430
+ action: proxy ? void 0 : directAction,
431
+ onSubmit: proxy ? proxySubmit : directSubmit,
432
+ children: [
433
+ !proxy && /* @__PURE__ */ jsxs2(Fragment, { children: [
434
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "client_id", value: config.clientId }),
435
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "redirect_uri", value: config.redirectUri }),
436
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "state", ref: stateRef }),
437
+ current.key !== "magic" && /* @__PURE__ */ jsx2("input", { type: "hidden", name: "mode", value: current.key === "register" ? "register" : "login" })
438
+ ] }),
439
+ current.fields.name && /* @__PURE__ */ jsxs2("label", { className: "authrobo-label", children: [
440
+ "Name",
441
+ /* @__PURE__ */ jsx2(
442
+ "input",
443
+ {
444
+ className: "authrobo-input",
445
+ name: "name",
446
+ type: "text",
447
+ autoComplete: "name",
448
+ placeholder: "Your name",
449
+ value: name,
450
+ onChange: (e) => setName(e.target.value)
451
+ }
452
+ )
453
+ ] }),
454
+ /* @__PURE__ */ jsxs2("label", { className: "authrobo-label", children: [
455
+ "Email",
456
+ /* @__PURE__ */ jsx2(
457
+ "input",
458
+ {
459
+ className: "authrobo-input",
460
+ name: "email",
461
+ type: "email",
462
+ required: true,
463
+ autoComplete: "email",
464
+ placeholder: "you@example.com",
465
+ value: email,
466
+ onChange: (e) => setEmail(e.target.value)
467
+ }
468
+ )
469
+ ] }),
470
+ current.fields.password && /* @__PURE__ */ jsxs2("label", { className: "authrobo-label", children: [
471
+ "Password",
472
+ /* @__PURE__ */ jsx2(
473
+ "input",
474
+ {
475
+ className: "authrobo-input",
476
+ name: "password",
477
+ type: "password",
478
+ required: true,
479
+ minLength: current.key === "register" ? 8 : void 0,
480
+ autoComplete: current.key === "register" ? "new-password" : "current-password",
481
+ placeholder: current.key === "register" ? "At least 8 characters" : "Your password",
482
+ value: password,
483
+ onChange: (e) => setPassword(e.target.value)
484
+ }
485
+ )
486
+ ] }),
487
+ error && /* @__PURE__ */ jsx2("p", { className: "authrobo-error", children: error }),
488
+ /* @__PURE__ */ jsx2("button", { type: "submit", className: "authrobo-primary", disabled: loading, children: loading ? /* @__PURE__ */ jsxs2(Fragment, { children: [
489
+ /* @__PURE__ */ jsx2(Spinner, {}),
490
+ " Please wait\u2026"
491
+ ] }) : current.cta })
492
+ ]
493
+ }
494
+ )
495
+ ] });
496
+ }
213
497
  function AuthRoboAuth({
214
498
  config,
215
499
  mode = "inline",
216
500
  open = true,
217
501
  onClose,
502
+ onSuccess,
218
503
  title,
219
- methods: only
504
+ showHeader = true,
505
+ methods: only,
506
+ startPath,
507
+ buttonStyle = "neutral"
220
508
  }) {
221
509
  useInjectStyles();
222
510
  const c = useResolvedConfig(config);
@@ -237,24 +525,28 @@ function AuthRoboAuth({
237
525
  const email = shown.filter((m) => EMAIL_METHODS.includes(m));
238
526
  const badge = (m) => shown.length > 1 && m === lastMethod;
239
527
  const panel = /* @__PURE__ */ jsxs2("div", { className: "authrobo-stack", children: [
240
- (data?.logoUrl || data?.name) && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 }, children: [
528
+ showHeader && (data?.logoUrl || data?.name) && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 }, children: [
241
529
  data?.logoUrl && /* @__PURE__ */ jsx2("img", { src: data.logoUrl, alt: "", width: 28, height: 28, style: { borderRadius: 6 } }),
242
530
  /* @__PURE__ */ jsx2("span", { className: "authrobo-title", children: title ?? `Sign in${data?.name ? ` to ${data.name}` : ""}` })
243
531
  ] }),
244
- loading && /* @__PURE__ */ jsx2("p", { className: "authrobo-muted", children: "Loading sign-in options\u2026" }),
532
+ loading && /* @__PURE__ */ jsxs2("div", { className: "authrobo-loading", children: [
533
+ /* @__PURE__ */ jsx2(Spinner, { style: { color: "var(--authrobo-accent)" } }),
534
+ /* @__PURE__ */ jsx2("span", { className: "authrobo-muted", children: "Loading sign-in options\u2026" })
535
+ ] }),
245
536
  error && /* @__PURE__ */ jsx2("p", { className: "authrobo-error", children: "Couldn't load sign-in options." }),
246
- social.map((m) => /* @__PURE__ */ jsx2(AuthRoboButton, { provider: m, config: c, lastUsed: badge(m) }, m)),
247
- email.length > 0 && social.length > 0 && /* @__PURE__ */ jsx2("div", { className: "authrobo-divider", children: "or" }),
248
- email.map((m) => /* @__PURE__ */ jsx2(
537
+ social.map((m) => /* @__PURE__ */ jsx2(
249
538
  AuthRoboButton,
250
539
  {
251
540
  provider: m,
252
541
  config: c,
253
- label: m === "magic_link" ? "Email me a sign-in link" : "Continue with email",
254
- lastUsed: badge(m)
542
+ lastUsed: badge(m),
543
+ startPath,
544
+ buttonStyle
255
545
  },
256
546
  m
257
547
  )),
548
+ email.length > 0 && social.length > 0 && /* @__PURE__ */ jsx2("div", { className: "authrobo-divider", children: "or" }),
549
+ email.length > 0 && /* @__PURE__ */ jsx2(EmailSwitcher, { config: c, methods: email, startPath, onSuccess }),
258
550
  !loading && !error && shown.length === 0 && /* @__PURE__ */ jsx2("p", { className: "authrobo-muted", children: "No sign-in methods are enabled for this app." })
259
551
  ] });
260
552
  if (mode === "inline") return panel;
@@ -267,7 +559,10 @@ export {
267
559
  AuthRoboAuth,
268
560
  AuthRoboButton,
269
561
  AuthRoboProvider,
562
+ BRAND_STYLE,
270
563
  PROVIDER_META,
564
+ Spinner,
565
+ prefetchAppConfig,
271
566
  startSignIn,
272
567
  useAppConfig,
273
568
  useLastMethod