@authrobo/react 0.2.1 → 0.5.1

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, poweredBy: j.powered_by !== false },
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 = `
@@ -157,8 +227,39 @@ ${scoped('[data-theme="dark"] ', TOKENS_DARK)}
157
227
  .authrobo-title{font:700 16px system-ui,sans-serif;color:var(--authrobo-title-fg);}
158
228
  .authrobo-muted{color:var(--authrobo-muted);font:14px system-ui,sans-serif;margin:0;}
159
229
  .authrobo-error{color:var(--authrobo-error);font:14px system-ui,sans-serif;margin:0;}
230
+ .authrobo-powered{display:block;margin:2px 0 -2px;text-align:center;text-decoration:none;
231
+ font:500 11.5px system-ui,sans-serif;color:var(--authrobo-muted);opacity:.85;transition:opacity .15s ease;}
232
+ .authrobo-powered:hover{opacity:1;}
233
+ .authrobo-powered b{font-weight:700;color:var(--authrobo-title-fg);}
160
234
  .authrobo-divider{display:flex;align-items:center;gap:10px;color:var(--authrobo-divider-fg);font:500 12px system-ui,sans-serif;}
161
235
  .authrobo-divider::before,.authrobo-divider::after{content:"";height:1px;flex:1;background:var(--authrobo-divider-line);}
236
+ .authrobo-tabs{display:flex;gap:4px;padding:4px;border:1px solid var(--authrobo-btn-border);
237
+ background:var(--authrobo-tabs-bg);border-radius:10px;}
238
+ .authrobo-tab{flex:1;border:0;background:transparent;border-radius:7px;padding:8px 0;cursor:pointer;
239
+ font:600 13px system-ui,sans-serif;color:var(--authrobo-muted);transition:background .15s ease,color .15s ease;}
240
+ .authrobo-tab:hover{color:var(--authrobo-title-fg);}
241
+ .authrobo-tab[aria-selected="true"]{background:var(--authrobo-accent);color:var(--authrobo-accent-fg);}
242
+ .authrobo-tab:focus-visible{outline:2px solid var(--authrobo-focus);outline-offset:2px;}
243
+ .authrobo-form{display:flex;flex-direction:column;gap:10px;}
244
+ .authrobo-label{font:500 12.5px system-ui,sans-serif;color:var(--authrobo-muted);}
245
+ .authrobo-input{width:100%;box-sizing:border-box;margin-top:6px;border:1px solid var(--authrobo-btn-border);
246
+ background:var(--authrobo-input-bg);color:var(--authrobo-input-fg);border-radius:10px;padding:10px 12px;
247
+ font:14px system-ui,sans-serif;}
248
+ .authrobo-input:focus{outline:none;border-color:var(--authrobo-accent);}
249
+ .authrobo-primary{display:flex;width:100%;align-items:center;justify-content:center;gap:8px;border:0;
250
+ background:var(--authrobo-accent);color:var(--authrobo-accent-fg);border-radius:10px;padding:11px 14px;
251
+ font:700 14px system-ui,sans-serif;cursor:pointer;transition:filter .15s ease;}
252
+ .authrobo-primary:hover{filter:brightness(.96);}
253
+ .authrobo-primary:focus-visible{outline:2px solid var(--authrobo-focus);outline-offset:2px;}
254
+ .authrobo-primary:disabled{opacity:.7;cursor:default;}
255
+ .authrobo-btn-brand{transition:filter .15s ease,box-shadow .15s ease;}
256
+ .authrobo-btn-brand:hover{filter:brightness(.96);}
257
+ .authrobo-spinner{display:inline-block;width:1.15em;height:1.15em;vertical-align:-0.2em;border-radius:50%;
258
+ border:2px solid color-mix(in srgb, currentColor 24%, transparent);border-top-color:currentColor;
259
+ animation:authrobo-spin .7s linear infinite;}
260
+ .authrobo-loading{display:flex;align-items:center;justify-content:center;gap:10px;padding:20px 0;}
261
+ @keyframes authrobo-spin{to{transform:rotate(360deg)}}
262
+ @media (prefers-reduced-motion: reduce){.authrobo-spinner{animation-duration:1.4s}}
162
263
  .authrobo-overlay{position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;
163
264
  justify-content:center;padding:16px;background:var(--authrobo-overlay);backdrop-filter:blur(2px);
164
265
  animation:authrobo-fade .16s ease-out both;}
@@ -180,6 +281,17 @@ function useInjectStyles() {
180
281
  document.head.appendChild(el);
181
282
  }, []);
182
283
  }
284
+ function Spinner({ style, className }) {
285
+ return /* @__PURE__ */ jsx2(
286
+ "span",
287
+ {
288
+ className: className ? `authrobo-spinner ${className}` : "authrobo-spinner",
289
+ role: "status",
290
+ "aria-label": "Loading",
291
+ style
292
+ }
293
+ );
294
+ }
183
295
  function AuthRoboButton({
184
296
  provider,
185
297
  config,
@@ -187,36 +299,216 @@ function AuthRoboButton({
187
299
  className,
188
300
  style,
189
301
  onClick,
190
- lastUsed
302
+ lastUsed,
303
+ startPath,
304
+ buttonStyle = "neutral"
191
305
  }) {
192
306
  useInjectStyles();
193
307
  const c = useResolvedConfig(config);
194
308
  const meta = PROVIDER_META[provider];
309
+ const brand = buttonStyle === "branded" ? BRAND_STYLE[provider] : void 0;
310
+ const icon = brand?.whiteIcon ? WHITE_MARKS[provider] ?? meta.icon : meta.icon;
311
+ const classes = ["authrobo-btn", brand ? "authrobo-btn-brand" : "", className].filter(Boolean).join(" ");
312
+ const brandStyle = brand ? { background: brand.bg, color: brand.fg, borderColor: brand.border } : void 0;
195
313
  return /* @__PURE__ */ jsxs2(
196
314
  "button",
197
315
  {
198
316
  type: "button",
199
- className: className ? `authrobo-btn ${className}` : "authrobo-btn",
200
- style,
317
+ className: classes,
318
+ style: { ...brandStyle, ...style },
201
319
  onClick: () => {
202
320
  onClick?.();
203
- startSignIn(c, provider);
321
+ startSignIn(c, provider, { startPath });
204
322
  },
205
323
  children: [
206
- /* @__PURE__ */ jsx2("span", { style: { display: "inline-flex", alignItems: "center" }, children: meta.icon }),
324
+ /* @__PURE__ */ jsx2("span", { style: { display: "inline-flex", alignItems: "center" }, children: icon }),
207
325
  label ?? `Continue with ${meta.label}`,
208
326
  lastUsed && /* @__PURE__ */ jsx2("span", { className: "authrobo-last", children: "Last used" })
209
327
  ]
210
328
  }
211
329
  );
212
330
  }
331
+ var EMAIL_TABS = [
332
+ { key: "magic", method: "magic_link", label: "Magic Link", cta: "Email me a link", requires: "magic_link", fields: { name: false, password: false }, proxyPath: "magic" },
333
+ { key: "register", method: "password", label: "Register", cta: "Create account", requires: "password", fields: { name: true, password: true }, proxyPath: "register" },
334
+ { key: "login", method: "password", label: "Login", cta: "Sign in", requires: "password", fields: { name: false, password: true }, proxyPath: "login" }
335
+ ];
336
+ function EmailSwitcher({
337
+ config,
338
+ methods,
339
+ startPath,
340
+ onSuccess
341
+ }) {
342
+ const tabs = useMemo(() => EMAIL_TABS.filter((t) => methods.includes(t.requires)), [methods]);
343
+ const [active, setActive] = useState(tabs[0]?.key ?? "login");
344
+ const [name, setName] = useState("");
345
+ const [email, setEmail] = useState("");
346
+ const [password, setPassword] = useState("");
347
+ const [loading, setLoading] = useState(false);
348
+ const [error, setError] = useState(null);
349
+ const [sent, setSent] = useState(false);
350
+ const stateRef = useRef(null);
351
+ const proxy = !!startPath;
352
+ useEffect(() => {
353
+ if (tabs.length && !tabs.some((t) => t.key === active)) setActive(tabs[0].key);
354
+ }, [tabs, active]);
355
+ if (!tabs.length) return null;
356
+ const current = tabs.find((t) => t.key === active) ?? tabs[0];
357
+ const base = config.issuerUrl.replace(/\/$/, "");
358
+ const directAction = current.key === "magic" ? `${base}/api/v1/auth/magic/start` : `${base}/api/v1/auth/authorize`;
359
+ const selectTab = (key) => {
360
+ setActive(key);
361
+ setError(null);
362
+ setSent(false);
363
+ };
364
+ async function proxySubmit(e) {
365
+ e.preventDefault();
366
+ setError(null);
367
+ setLoading(true);
368
+ try {
369
+ const body = { email: email.trim() };
370
+ if (current.fields.password) body.password = password;
371
+ if (current.fields.name && name.trim()) body.name = name.trim();
372
+ const res = await fetch(`${startPath.replace(/\/$/, "")}/${current.proxyPath}`, {
373
+ method: "POST",
374
+ headers: { "content-type": "application/json" },
375
+ body: JSON.stringify(body)
376
+ });
377
+ if (!res.ok) {
378
+ const d = await res.json().catch(() => ({}));
379
+ throw new Error(d?.error || "Something went wrong. Please try again.");
380
+ }
381
+ try {
382
+ localStorage.setItem(lastMethodKey(config.clientId), current.method);
383
+ } catch {
384
+ }
385
+ if (current.key === "magic") setSent(true);
386
+ else onSuccess?.();
387
+ } catch (err) {
388
+ setError(err instanceof Error ? err.message : "Something went wrong.");
389
+ } finally {
390
+ setLoading(false);
391
+ }
392
+ }
393
+ function directSubmit() {
394
+ const s = randomState();
395
+ try {
396
+ sessionStorage.setItem("authrobo:state", s);
397
+ } catch {
398
+ }
399
+ if (stateRef.current) stateRef.current.value = s;
400
+ try {
401
+ localStorage.setItem(lastMethodKey(config.clientId), current.method);
402
+ } catch {
403
+ }
404
+ setLoading(true);
405
+ }
406
+ if (sent) {
407
+ return /* @__PURE__ */ jsxs2("div", { className: "authrobo-stack", role: "status", children: [
408
+ /* @__PURE__ */ jsx2("p", { className: "authrobo-title", children: "Check your email" }),
409
+ /* @__PURE__ */ jsxs2("p", { className: "authrobo-muted", children: [
410
+ "We sent a sign-in link to ",
411
+ /* @__PURE__ */ jsx2("b", { children: email.trim() }),
412
+ ". It works once and expires shortly."
413
+ ] })
414
+ ] });
415
+ }
416
+ return /* @__PURE__ */ jsxs2("div", { className: "authrobo-stack", children: [
417
+ tabs.length > 1 && /* @__PURE__ */ jsx2("div", { className: "authrobo-tabs", role: "tablist", "aria-label": "Sign-in method", children: tabs.map((t) => /* @__PURE__ */ jsx2(
418
+ "button",
419
+ {
420
+ type: "button",
421
+ role: "tab",
422
+ "aria-selected": current.key === t.key,
423
+ className: "authrobo-tab",
424
+ onClick: () => selectTab(t.key),
425
+ children: t.label
426
+ },
427
+ t.key
428
+ )) }),
429
+ /* @__PURE__ */ jsxs2(
430
+ "form",
431
+ {
432
+ className: "authrobo-form",
433
+ method: proxy ? void 0 : "post",
434
+ action: proxy ? void 0 : directAction,
435
+ onSubmit: proxy ? proxySubmit : directSubmit,
436
+ children: [
437
+ !proxy && /* @__PURE__ */ jsxs2(Fragment, { children: [
438
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "client_id", value: config.clientId }),
439
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "redirect_uri", value: config.redirectUri }),
440
+ /* @__PURE__ */ jsx2("input", { type: "hidden", name: "state", ref: stateRef }),
441
+ current.key !== "magic" && /* @__PURE__ */ jsx2("input", { type: "hidden", name: "mode", value: current.key === "register" ? "register" : "login" })
442
+ ] }),
443
+ current.fields.name && /* @__PURE__ */ jsxs2("label", { className: "authrobo-label", children: [
444
+ "Name",
445
+ /* @__PURE__ */ jsx2(
446
+ "input",
447
+ {
448
+ className: "authrobo-input",
449
+ name: "name",
450
+ type: "text",
451
+ autoComplete: "name",
452
+ placeholder: "Your name",
453
+ value: name,
454
+ onChange: (e) => setName(e.target.value)
455
+ }
456
+ )
457
+ ] }),
458
+ /* @__PURE__ */ jsxs2("label", { className: "authrobo-label", children: [
459
+ "Email",
460
+ /* @__PURE__ */ jsx2(
461
+ "input",
462
+ {
463
+ className: "authrobo-input",
464
+ name: "email",
465
+ type: "email",
466
+ required: true,
467
+ autoComplete: "email",
468
+ placeholder: "you@example.com",
469
+ value: email,
470
+ onChange: (e) => setEmail(e.target.value)
471
+ }
472
+ )
473
+ ] }),
474
+ current.fields.password && /* @__PURE__ */ jsxs2("label", { className: "authrobo-label", children: [
475
+ "Password",
476
+ /* @__PURE__ */ jsx2(
477
+ "input",
478
+ {
479
+ className: "authrobo-input",
480
+ name: "password",
481
+ type: "password",
482
+ required: true,
483
+ minLength: current.key === "register" ? 8 : void 0,
484
+ autoComplete: current.key === "register" ? "new-password" : "current-password",
485
+ placeholder: current.key === "register" ? "At least 8 characters" : "Your password",
486
+ value: password,
487
+ onChange: (e) => setPassword(e.target.value)
488
+ }
489
+ )
490
+ ] }),
491
+ error && /* @__PURE__ */ jsx2("p", { className: "authrobo-error", children: error }),
492
+ /* @__PURE__ */ jsx2("button", { type: "submit", className: "authrobo-primary", disabled: loading, children: loading ? /* @__PURE__ */ jsxs2(Fragment, { children: [
493
+ /* @__PURE__ */ jsx2(Spinner, {}),
494
+ " Please wait\u2026"
495
+ ] }) : current.cta })
496
+ ]
497
+ }
498
+ )
499
+ ] });
500
+ }
213
501
  function AuthRoboAuth({
214
502
  config,
215
503
  mode = "inline",
216
504
  open = true,
217
505
  onClose,
506
+ onSuccess,
218
507
  title,
219
- methods: only
508
+ showHeader = true,
509
+ methods: only,
510
+ startPath,
511
+ buttonStyle = "neutral"
220
512
  }) {
221
513
  useInjectStyles();
222
514
  const c = useResolvedConfig(config);
@@ -237,25 +529,33 @@ function AuthRoboAuth({
237
529
  const email = shown.filter((m) => EMAIL_METHODS.includes(m));
238
530
  const badge = (m) => shown.length > 1 && m === lastMethod;
239
531
  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: [
532
+ showHeader && (data?.logoUrl || data?.name) && /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", gap: 10, marginBottom: 4 }, children: [
241
533
  data?.logoUrl && /* @__PURE__ */ jsx2("img", { src: data.logoUrl, alt: "", width: 28, height: 28, style: { borderRadius: 6 } }),
242
534
  /* @__PURE__ */ jsx2("span", { className: "authrobo-title", children: title ?? `Sign in${data?.name ? ` to ${data.name}` : ""}` })
243
535
  ] }),
244
- loading && /* @__PURE__ */ jsx2("p", { className: "authrobo-muted", children: "Loading sign-in options\u2026" }),
536
+ loading && /* @__PURE__ */ jsxs2("div", { className: "authrobo-loading", children: [
537
+ /* @__PURE__ */ jsx2(Spinner, { style: { color: "var(--authrobo-accent)" } }),
538
+ /* @__PURE__ */ jsx2("span", { className: "authrobo-muted", children: "Loading sign-in options\u2026" })
539
+ ] }),
245
540
  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(
541
+ social.map((m) => /* @__PURE__ */ jsx2(
249
542
  AuthRoboButton,
250
543
  {
251
544
  provider: m,
252
545
  config: c,
253
- label: m === "magic_link" ? "Email me a sign-in link" : "Continue with email",
254
- lastUsed: badge(m)
546
+ lastUsed: badge(m),
547
+ startPath,
548
+ buttonStyle
255
549
  },
256
550
  m
257
551
  )),
258
- !loading && !error && shown.length === 0 && /* @__PURE__ */ jsx2("p", { className: "authrobo-muted", children: "No sign-in methods are enabled for this app." })
552
+ email.length > 0 && social.length > 0 && /* @__PURE__ */ jsx2("div", { className: "authrobo-divider", children: "or" }),
553
+ email.length > 0 && /* @__PURE__ */ jsx2(EmailSwitcher, { config: c, methods: email, startPath, onSuccess }),
554
+ !loading && !error && shown.length === 0 && /* @__PURE__ */ jsx2("p", { className: "authrobo-muted", children: "No sign-in methods are enabled for this app." }),
555
+ !loading && !error && data?.poweredBy && /* @__PURE__ */ jsxs2("a", { className: "authrobo-powered", href: "https://authrobo.com", target: "_blank", rel: "noreferrer", children: [
556
+ "Authentication provided by ",
557
+ /* @__PURE__ */ jsx2("b", { children: "AuthRobo" })
558
+ ] })
259
559
  ] });
260
560
  if (mode === "inline") return panel;
261
561
  return /* @__PURE__ */ jsx2("div", { className: "authrobo-overlay", onClick: () => onClose?.(), children: /* @__PURE__ */ jsxs2("div", { className: "authrobo-card", role: "dialog", "aria-modal": "true", onClick: (e) => e.stopPropagation(), children: [
@@ -267,7 +567,10 @@ export {
267
567
  AuthRoboAuth,
268
568
  AuthRoboButton,
269
569
  AuthRoboProvider,
570
+ BRAND_STYLE,
270
571
  PROVIDER_META,
572
+ Spinner,
573
+ prefetchAppConfig,
271
574
  startSignIn,
272
575
  useAppConfig,
273
576
  useLastMethod