@optare/react 0.1.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/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/index.cjs +1510 -0
- package/dist/index.d.cts +396 -0
- package/dist/index.d.ts +396 -0
- package/dist/index.js +1477 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1477 @@
|
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
createElement,
|
|
5
|
+
useContext,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState
|
|
10
|
+
} from "react";
|
|
11
|
+
import {
|
|
12
|
+
resolveOptareConfig,
|
|
13
|
+
OptareConfigError,
|
|
14
|
+
DEFAULT_AUTH_METHODS
|
|
15
|
+
} from "@optare/client";
|
|
16
|
+
|
|
17
|
+
// src/auth-client.ts
|
|
18
|
+
import { createAuthClient } from "better-auth/react";
|
|
19
|
+
import {
|
|
20
|
+
organizationClient,
|
|
21
|
+
twoFactorClient,
|
|
22
|
+
emailOTPClient,
|
|
23
|
+
magicLinkClient,
|
|
24
|
+
multiSessionClient,
|
|
25
|
+
jwtClient
|
|
26
|
+
} from "better-auth/client/plugins";
|
|
27
|
+
function optarePlugins() {
|
|
28
|
+
return [
|
|
29
|
+
organizationClient(),
|
|
30
|
+
twoFactorClient(),
|
|
31
|
+
emailOTPClient(),
|
|
32
|
+
magicLinkClient(),
|
|
33
|
+
multiSessionClient(),
|
|
34
|
+
jwtClient()
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function createReactAuthClient(options) {
|
|
38
|
+
return createAuthClient({
|
|
39
|
+
baseURL: options.bootstrap.baseURL,
|
|
40
|
+
fetchOptions: {
|
|
41
|
+
...options.fetch ? { customFetchImpl: options.fetch } : {},
|
|
42
|
+
headers: {
|
|
43
|
+
"x-optare-publishable-key": options.publishableKey,
|
|
44
|
+
...options.headers
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
plugins: [...optarePlugins()]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/theme.ts
|
|
52
|
+
var DEFAULT_PRIMARY = "#4f46e5";
|
|
53
|
+
var DEFAULT_RADIUS = "10px";
|
|
54
|
+
var CSS_COLOR = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
55
|
+
var CSS_LENGTH = /^-?\d+(?:\.\d+)?(?:px|rem|em|%)$/;
|
|
56
|
+
function safeColor(value, fallback) {
|
|
57
|
+
return value && CSS_COLOR.test(value.trim()) ? value.trim() : fallback;
|
|
58
|
+
}
|
|
59
|
+
function safeLength(value, fallback) {
|
|
60
|
+
const v = (value ?? "").trim();
|
|
61
|
+
if (CSS_LENGTH.test(v)) return v;
|
|
62
|
+
if (/^\d+$/.test(v)) return `${v}px`;
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
function normalizeHex(hex) {
|
|
66
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim());
|
|
67
|
+
if (!m) return null;
|
|
68
|
+
let h = m[1];
|
|
69
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
70
|
+
return `#${h.slice(0, 6)}`;
|
|
71
|
+
}
|
|
72
|
+
function relativeLuminance(hex) {
|
|
73
|
+
const n = normalizeHex(hex);
|
|
74
|
+
if (!n) return 0;
|
|
75
|
+
const int = parseInt(n.slice(1), 16);
|
|
76
|
+
const chan = [int >> 16 & 255, int >> 8 & 255, int & 255].map((v) => {
|
|
77
|
+
const s2 = v / 255;
|
|
78
|
+
return s2 <= 0.03928 ? s2 / 12.92 : ((s2 + 0.055) / 1.055) ** 2.4;
|
|
79
|
+
});
|
|
80
|
+
return 0.2126 * chan[0] + 0.7152 * chan[1] + 0.0722 * chan[2];
|
|
81
|
+
}
|
|
82
|
+
function contrastText(hex) {
|
|
83
|
+
const bg = relativeLuminance(hex);
|
|
84
|
+
const whiteTextRatio = (1 + 0.05) / (bg + 0.05);
|
|
85
|
+
const darkTextRatio = (bg + 0.05) / (relativeLuminance("#111827") + 0.05);
|
|
86
|
+
return darkTextRatio >= whiteTextRatio ? "#111827" : "#ffffff";
|
|
87
|
+
}
|
|
88
|
+
function brandingToCssVars(branding) {
|
|
89
|
+
const primary = safeColor(branding?.primaryColor, DEFAULT_PRIMARY);
|
|
90
|
+
const radius = safeLength(branding?.radius, DEFAULT_RADIUS);
|
|
91
|
+
return {
|
|
92
|
+
"--optare-primary": primary,
|
|
93
|
+
"--optare-primary-text": contrastText(primary),
|
|
94
|
+
// A translucent wash of the primary for hover/focus rings — layered over
|
|
95
|
+
// white it reads as a tint without needing a second colour from branding.
|
|
96
|
+
"--optare-primary-soft": "color-mix(in srgb, var(--optare-primary) 12%, #ffffff)",
|
|
97
|
+
"--optare-ring": "color-mix(in srgb, var(--optare-primary) 35%, transparent)",
|
|
98
|
+
"--optare-radius": radius,
|
|
99
|
+
"--optare-bg": "#ffffff",
|
|
100
|
+
"--optare-bg-subtle": "#f9fafb",
|
|
101
|
+
"--optare-fg": "#111827",
|
|
102
|
+
"--optare-fg-muted": "#4b5563",
|
|
103
|
+
"--optare-muted": "#6b7280",
|
|
104
|
+
"--optare-border": "#e5e7eb",
|
|
105
|
+
"--optare-border-strong": "#d1d5db",
|
|
106
|
+
"--optare-error": "#dc2626",
|
|
107
|
+
"--optare-error-soft": "#fef2f2",
|
|
108
|
+
"--optare-success": "#059669",
|
|
109
|
+
"--optare-shadow": "0 1px 2px rgba(16,24,40,0.04), 0 8px 24px rgba(16,24,40,0.08)",
|
|
110
|
+
"--optare-font": 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/context.tsx
|
|
115
|
+
var OptareContext = createContext(null);
|
|
116
|
+
function OptareProvider(props) {
|
|
117
|
+
const {
|
|
118
|
+
publishableKey,
|
|
119
|
+
bootstrap: bootstrapProp,
|
|
120
|
+
baseURL,
|
|
121
|
+
configURL,
|
|
122
|
+
fetch: fetchImpl,
|
|
123
|
+
headers,
|
|
124
|
+
loadingFallback,
|
|
125
|
+
errorFallback,
|
|
126
|
+
children
|
|
127
|
+
} = props;
|
|
128
|
+
const [bootstrap, setBootstrap] = useState(
|
|
129
|
+
bootstrapProp ?? null
|
|
130
|
+
);
|
|
131
|
+
const [status, setStatus] = useState(
|
|
132
|
+
bootstrapProp ? "ready" : "loading"
|
|
133
|
+
);
|
|
134
|
+
const [error, setError] = useState(null);
|
|
135
|
+
const [attempt, setAttempt] = useState(0);
|
|
136
|
+
useEffect(() => {
|
|
137
|
+
if (bootstrapProp) {
|
|
138
|
+
setBootstrap(bootstrapProp);
|
|
139
|
+
setStatus("ready");
|
|
140
|
+
setError(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let cancelled = false;
|
|
144
|
+
setStatus("loading");
|
|
145
|
+
setError(null);
|
|
146
|
+
resolveOptareConfig({ publishableKey, baseURL, configURL, fetch: fetchImpl }).then((resolved) => {
|
|
147
|
+
if (cancelled) return;
|
|
148
|
+
setBootstrap(resolved);
|
|
149
|
+
setStatus("ready");
|
|
150
|
+
}).catch((err) => {
|
|
151
|
+
if (cancelled) return;
|
|
152
|
+
setError(
|
|
153
|
+
err instanceof Error ? err : new OptareConfigError("Failed to resolve Optare config")
|
|
154
|
+
);
|
|
155
|
+
setStatus("error");
|
|
156
|
+
});
|
|
157
|
+
return () => {
|
|
158
|
+
cancelled = true;
|
|
159
|
+
};
|
|
160
|
+
}, [publishableKey, baseURL, configURL, fetchImpl, bootstrapProp, attempt]);
|
|
161
|
+
const authRef = useRef(
|
|
162
|
+
null
|
|
163
|
+
);
|
|
164
|
+
const auth = useMemo(() => {
|
|
165
|
+
if (!bootstrap) return null;
|
|
166
|
+
const cached = authRef.current;
|
|
167
|
+
if (cached && cached.baseURL === bootstrap.baseURL) return cached.client;
|
|
168
|
+
const client = createReactAuthClient({
|
|
169
|
+
bootstrap,
|
|
170
|
+
publishableKey,
|
|
171
|
+
fetch: fetchImpl,
|
|
172
|
+
headers
|
|
173
|
+
});
|
|
174
|
+
authRef.current = { baseURL: bootstrap.baseURL, client };
|
|
175
|
+
return client;
|
|
176
|
+
}, [bootstrap, publishableKey, fetchImpl, headers]);
|
|
177
|
+
const retry = useMemo(() => () => setAttempt((n) => n + 1), []);
|
|
178
|
+
const value = useMemo(
|
|
179
|
+
() => ({
|
|
180
|
+
status,
|
|
181
|
+
error,
|
|
182
|
+
bootstrap,
|
|
183
|
+
branding: bootstrap?.branding ?? null,
|
|
184
|
+
project: bootstrap?.project ?? null,
|
|
185
|
+
authMethods: bootstrap?.authMethods ?? DEFAULT_AUTH_METHODS,
|
|
186
|
+
cssVars: brandingToCssVars(bootstrap?.branding ?? null),
|
|
187
|
+
auth,
|
|
188
|
+
publishableKey,
|
|
189
|
+
retry
|
|
190
|
+
}),
|
|
191
|
+
[status, error, bootstrap, auth, publishableKey, retry]
|
|
192
|
+
);
|
|
193
|
+
let body = children;
|
|
194
|
+
if (status === "loading" && loadingFallback !== void 0) body = loadingFallback;
|
|
195
|
+
if (status === "error" && errorFallback !== void 0 && error) {
|
|
196
|
+
body = typeof errorFallback === "function" ? errorFallback(error, retry) : errorFallback;
|
|
197
|
+
}
|
|
198
|
+
return createElement(OptareContext.Provider, { value }, body);
|
|
199
|
+
}
|
|
200
|
+
function useOptare() {
|
|
201
|
+
const ctx = useContext(OptareContext);
|
|
202
|
+
if (!ctx) {
|
|
203
|
+
throw new Error("useOptare must be used inside <OptareProvider>");
|
|
204
|
+
}
|
|
205
|
+
return ctx;
|
|
206
|
+
}
|
|
207
|
+
function useOptareAuth() {
|
|
208
|
+
const { auth, status, error } = useOptare();
|
|
209
|
+
if (!auth) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
status === "error" ? `Optare provider failed to initialise: ${error?.message ?? "unknown error"}` : "Optare provider is still resolving \u2014 gate on `useOptare().status === 'ready'`"
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return auth;
|
|
215
|
+
}
|
|
216
|
+
function useOptareConfig() {
|
|
217
|
+
const { bootstrap, project, branding, status } = useOptare();
|
|
218
|
+
return { bootstrap, project, branding, status };
|
|
219
|
+
}
|
|
220
|
+
function useBranding() {
|
|
221
|
+
return useOptare().branding;
|
|
222
|
+
}
|
|
223
|
+
function useAuthMethods() {
|
|
224
|
+
return useOptare().authMethods;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/hooks.ts
|
|
228
|
+
import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useState as useState2 } from "react";
|
|
229
|
+
|
|
230
|
+
// src/session.ts
|
|
231
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
232
|
+
function subscribeSessionChanged(listener) {
|
|
233
|
+
listeners.add(listener);
|
|
234
|
+
return () => listeners.delete(listener);
|
|
235
|
+
}
|
|
236
|
+
function notifySessionChanged() {
|
|
237
|
+
for (const l of [...listeners]) l();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/hooks.ts
|
|
241
|
+
function useSession() {
|
|
242
|
+
const { auth, status } = useOptare();
|
|
243
|
+
const [data, setData] = useState2(null);
|
|
244
|
+
const [error, setError] = useState2(null);
|
|
245
|
+
const [isPending, setIsPending] = useState2(true);
|
|
246
|
+
const [tick, setTick] = useState2(0);
|
|
247
|
+
const refetch = useCallback(() => setTick((n) => n + 1), []);
|
|
248
|
+
useEffect2(() => subscribeSessionChanged(refetch), [refetch]);
|
|
249
|
+
useEffect2(() => {
|
|
250
|
+
if (status === "error") {
|
|
251
|
+
setIsPending(false);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!auth) return;
|
|
255
|
+
let cancelled = false;
|
|
256
|
+
setIsPending(true);
|
|
257
|
+
Promise.resolve(auth.getSession()).then((res) => {
|
|
258
|
+
if (cancelled) return;
|
|
259
|
+
const r = res;
|
|
260
|
+
setData(r?.data ?? null);
|
|
261
|
+
setError(r?.error ? new Error(r.error.message ?? "Session error") : null);
|
|
262
|
+
}).catch((err) => {
|
|
263
|
+
if (!cancelled) setError(err instanceof Error ? err : new Error(String(err)));
|
|
264
|
+
}).finally(() => {
|
|
265
|
+
if (!cancelled) setIsPending(false);
|
|
266
|
+
});
|
|
267
|
+
return () => {
|
|
268
|
+
cancelled = true;
|
|
269
|
+
};
|
|
270
|
+
}, [auth, status, tick]);
|
|
271
|
+
useEffect2(() => {
|
|
272
|
+
if (typeof window === "undefined") return;
|
|
273
|
+
const onFocus = () => refetch();
|
|
274
|
+
window.addEventListener("focus", onFocus);
|
|
275
|
+
return () => window.removeEventListener("focus", onFocus);
|
|
276
|
+
}, [refetch]);
|
|
277
|
+
return useMemo2(
|
|
278
|
+
() => ({
|
|
279
|
+
data,
|
|
280
|
+
user: data?.user ?? null,
|
|
281
|
+
isPending,
|
|
282
|
+
isAuthenticated: !!data,
|
|
283
|
+
error,
|
|
284
|
+
refetch
|
|
285
|
+
}),
|
|
286
|
+
[data, isPending, error, refetch]
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
function useUser() {
|
|
290
|
+
return useSession().user;
|
|
291
|
+
}
|
|
292
|
+
function useSignOut() {
|
|
293
|
+
const { auth } = useOptare();
|
|
294
|
+
const [isPending, setIsPending] = useState2(false);
|
|
295
|
+
const [error, setError] = useState2(null);
|
|
296
|
+
const signOut = useCallback(async () => {
|
|
297
|
+
if (!auth) return;
|
|
298
|
+
setIsPending(true);
|
|
299
|
+
setError(null);
|
|
300
|
+
try {
|
|
301
|
+
await auth.signOut();
|
|
302
|
+
notifySessionChanged();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
305
|
+
throw err;
|
|
306
|
+
} finally {
|
|
307
|
+
setIsPending(false);
|
|
308
|
+
}
|
|
309
|
+
}, [auth]);
|
|
310
|
+
return { signOut, isPending, error };
|
|
311
|
+
}
|
|
312
|
+
function useActiveOrganization() {
|
|
313
|
+
const { auth } = useOptare();
|
|
314
|
+
const { data } = useSession();
|
|
315
|
+
const [isPending, setIsPending] = useState2(false);
|
|
316
|
+
const activeOrganizationId = data?.session?.activeOrganizationId ?? null;
|
|
317
|
+
const setActive = useCallback(
|
|
318
|
+
async (organizationId) => {
|
|
319
|
+
if (!auth) return;
|
|
320
|
+
setIsPending(true);
|
|
321
|
+
try {
|
|
322
|
+
await auth.organization.setActive({ organizationId });
|
|
323
|
+
notifySessionChanged();
|
|
324
|
+
} finally {
|
|
325
|
+
setIsPending(false);
|
|
326
|
+
}
|
|
327
|
+
},
|
|
328
|
+
[auth]
|
|
329
|
+
);
|
|
330
|
+
return { activeOrganizationId, setActive, isPending };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/validation.ts
|
|
334
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
335
|
+
function isValidEmail(value) {
|
|
336
|
+
return EMAIL_RE.test(value.trim());
|
|
337
|
+
}
|
|
338
|
+
function checkPassword(value, min = 8) {
|
|
339
|
+
if (value.length === 0) return { ok: false, score: 0, message: "Password is required" };
|
|
340
|
+
if (value.length < min)
|
|
341
|
+
return { ok: false, score: 1, message: `Use at least ${min} characters` };
|
|
342
|
+
let score = 1;
|
|
343
|
+
if (value.length >= 12) score++;
|
|
344
|
+
if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score++;
|
|
345
|
+
if (/\d/.test(value)) score++;
|
|
346
|
+
if (/[^A-Za-z0-9]/.test(value)) score++;
|
|
347
|
+
return { ok: true, score: Math.min(score, 4), message: null };
|
|
348
|
+
}
|
|
349
|
+
function validateSignIn(fields) {
|
|
350
|
+
const errors = {};
|
|
351
|
+
if (!isValidEmail(fields.email)) errors.email = "Enter a valid email address";
|
|
352
|
+
if (fields.password.length === 0) errors.password = "Password is required";
|
|
353
|
+
return errors;
|
|
354
|
+
}
|
|
355
|
+
function validateSignUp(fields, passwordMin = 8) {
|
|
356
|
+
const errors = {};
|
|
357
|
+
if (fields.name.trim().length === 0) errors.name = "Name is required";
|
|
358
|
+
if (!isValidEmail(fields.email)) errors.email = "Enter a valid email address";
|
|
359
|
+
const pw = checkPassword(fields.password, passwordMin);
|
|
360
|
+
if (!pw.ok && pw.message) errors.password = pw.message;
|
|
361
|
+
return errors;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/components/SignIn.tsx
|
|
365
|
+
import { useState as useState4 } from "react";
|
|
366
|
+
|
|
367
|
+
// src/ui.tsx
|
|
368
|
+
import {
|
|
369
|
+
forwardRef
|
|
370
|
+
} from "react";
|
|
371
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
372
|
+
var base = {
|
|
373
|
+
card: {
|
|
374
|
+
boxSizing: "border-box",
|
|
375
|
+
width: "100%",
|
|
376
|
+
maxWidth: 400,
|
|
377
|
+
padding: 28,
|
|
378
|
+
background: "var(--optare-bg)",
|
|
379
|
+
color: "var(--optare-fg)",
|
|
380
|
+
border: "1px solid var(--optare-border)",
|
|
381
|
+
borderRadius: "var(--optare-radius)",
|
|
382
|
+
boxShadow: "var(--optare-shadow)",
|
|
383
|
+
fontFamily: "var(--optare-font)",
|
|
384
|
+
fontSize: 14,
|
|
385
|
+
lineHeight: 1.5,
|
|
386
|
+
WebkitFontSmoothing: "antialiased",
|
|
387
|
+
display: "flex",
|
|
388
|
+
flexDirection: "column",
|
|
389
|
+
gap: 20
|
|
390
|
+
},
|
|
391
|
+
header: {
|
|
392
|
+
display: "flex",
|
|
393
|
+
flexDirection: "column",
|
|
394
|
+
alignItems: "center",
|
|
395
|
+
gap: 6,
|
|
396
|
+
textAlign: "center"
|
|
397
|
+
},
|
|
398
|
+
logo: { height: 32, maxWidth: 160, objectFit: "contain", marginBottom: 2 },
|
|
399
|
+
brandName: {
|
|
400
|
+
fontSize: 13,
|
|
401
|
+
fontWeight: 600,
|
|
402
|
+
letterSpacing: 0.2,
|
|
403
|
+
color: "var(--optare-fg-muted)",
|
|
404
|
+
textTransform: "uppercase"
|
|
405
|
+
},
|
|
406
|
+
title: {
|
|
407
|
+
margin: 0,
|
|
408
|
+
fontSize: 19,
|
|
409
|
+
fontWeight: 650,
|
|
410
|
+
color: "var(--optare-fg)"
|
|
411
|
+
},
|
|
412
|
+
subtitle: {
|
|
413
|
+
margin: 0,
|
|
414
|
+
fontSize: 13.5,
|
|
415
|
+
color: "var(--optare-muted)"
|
|
416
|
+
},
|
|
417
|
+
field: { display: "flex", flexDirection: "column", gap: 6 },
|
|
418
|
+
label: { fontSize: 13, fontWeight: 550, color: "var(--optare-fg)" },
|
|
419
|
+
input: {
|
|
420
|
+
boxSizing: "border-box",
|
|
421
|
+
width: "100%",
|
|
422
|
+
padding: "10px 12px",
|
|
423
|
+
fontSize: 14,
|
|
424
|
+
fontFamily: "inherit",
|
|
425
|
+
color: "var(--optare-fg)",
|
|
426
|
+
background: "var(--optare-bg)",
|
|
427
|
+
border: "1px solid var(--optare-border-strong)",
|
|
428
|
+
borderRadius: "var(--optare-radius)",
|
|
429
|
+
outline: "none",
|
|
430
|
+
transition: "border-color 120ms, box-shadow 120ms"
|
|
431
|
+
},
|
|
432
|
+
fieldError: { fontSize: 12, color: "var(--optare-error)" },
|
|
433
|
+
button: {
|
|
434
|
+
boxSizing: "border-box",
|
|
435
|
+
display: "inline-flex",
|
|
436
|
+
alignItems: "center",
|
|
437
|
+
justifyContent: "center",
|
|
438
|
+
gap: 8,
|
|
439
|
+
width: "100%",
|
|
440
|
+
padding: "10px 16px",
|
|
441
|
+
fontSize: 14,
|
|
442
|
+
fontWeight: 600,
|
|
443
|
+
fontFamily: "inherit",
|
|
444
|
+
lineHeight: 1.2,
|
|
445
|
+
cursor: "pointer",
|
|
446
|
+
color: "var(--optare-primary-text)",
|
|
447
|
+
background: "var(--optare-primary)",
|
|
448
|
+
border: "1px solid transparent",
|
|
449
|
+
borderRadius: "var(--optare-radius)",
|
|
450
|
+
transition: "filter 120ms, opacity 120ms"
|
|
451
|
+
},
|
|
452
|
+
buttonSecondary: {
|
|
453
|
+
color: "var(--optare-fg)",
|
|
454
|
+
background: "var(--optare-bg)",
|
|
455
|
+
border: "1px solid var(--optare-border-strong)"
|
|
456
|
+
},
|
|
457
|
+
buttonDisabled: { opacity: 0.55, cursor: "not-allowed" },
|
|
458
|
+
alert: {
|
|
459
|
+
padding: "9px 12px",
|
|
460
|
+
fontSize: 13,
|
|
461
|
+
borderRadius: "var(--optare-radius)",
|
|
462
|
+
background: "var(--optare-error-soft)",
|
|
463
|
+
color: "var(--optare-error)",
|
|
464
|
+
border: "1px solid color-mix(in srgb, var(--optare-error) 30%, transparent)"
|
|
465
|
+
},
|
|
466
|
+
notice: {
|
|
467
|
+
padding: "9px 12px",
|
|
468
|
+
fontSize: 13,
|
|
469
|
+
borderRadius: "var(--optare-radius)",
|
|
470
|
+
background: "var(--optare-bg-subtle)",
|
|
471
|
+
color: "var(--optare-fg-muted)",
|
|
472
|
+
border: "1px solid var(--optare-border)"
|
|
473
|
+
},
|
|
474
|
+
link: {
|
|
475
|
+
background: "none",
|
|
476
|
+
border: "none",
|
|
477
|
+
padding: 0,
|
|
478
|
+
font: "inherit",
|
|
479
|
+
fontSize: 13,
|
|
480
|
+
color: "var(--optare-primary)",
|
|
481
|
+
cursor: "pointer",
|
|
482
|
+
textDecoration: "none",
|
|
483
|
+
fontWeight: 550
|
|
484
|
+
},
|
|
485
|
+
footer: { fontSize: 13, color: "var(--optare-muted)", textAlign: "center" },
|
|
486
|
+
divider: {
|
|
487
|
+
display: "flex",
|
|
488
|
+
alignItems: "center",
|
|
489
|
+
gap: 10,
|
|
490
|
+
fontSize: 12,
|
|
491
|
+
color: "var(--optare-muted)"
|
|
492
|
+
},
|
|
493
|
+
dividerRule: { flex: 1, height: 1, background: "var(--optare-border)" },
|
|
494
|
+
socialGrid: { display: "grid", gap: 8 },
|
|
495
|
+
spinner: {
|
|
496
|
+
display: "inline-block",
|
|
497
|
+
width: 14,
|
|
498
|
+
height: 14,
|
|
499
|
+
border: "2px solid currentColor",
|
|
500
|
+
borderTopColor: "transparent",
|
|
501
|
+
borderRadius: "50%",
|
|
502
|
+
animation: "optare-spin 0.6s linear infinite"
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
function SpinnerKeyframes() {
|
|
506
|
+
return /* @__PURE__ */ jsx("style", { children: "@keyframes optare-spin{to{transform:rotate(360deg)}}" });
|
|
507
|
+
}
|
|
508
|
+
function Spinner() {
|
|
509
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
510
|
+
/* @__PURE__ */ jsx(SpinnerKeyframes, {}),
|
|
511
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": true, style: base.spinner })
|
|
512
|
+
] });
|
|
513
|
+
}
|
|
514
|
+
function Card(props) {
|
|
515
|
+
const { cssVars } = useOptare();
|
|
516
|
+
return /* @__PURE__ */ jsx(
|
|
517
|
+
"div",
|
|
518
|
+
{
|
|
519
|
+
className: props.className,
|
|
520
|
+
style: { ...cssVars, ...base.card, ...props.style },
|
|
521
|
+
children: props.children
|
|
522
|
+
}
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
function BrandHeader(props) {
|
|
526
|
+
const { branding } = useOptare();
|
|
527
|
+
return /* @__PURE__ */ jsxs("div", { style: base.header, children: [
|
|
528
|
+
branding?.logoUrl ? /* @__PURE__ */ jsx("img", { src: branding.logoUrl, alt: branding.name, style: base.logo }) : branding?.name ? /* @__PURE__ */ jsx("span", { style: base.brandName, children: branding.name }) : null,
|
|
529
|
+
/* @__PURE__ */ jsx("h2", { style: base.title, children: props.title }),
|
|
530
|
+
props.subtitle ? /* @__PURE__ */ jsx("p", { style: base.subtitle, children: props.subtitle }) : null
|
|
531
|
+
] });
|
|
532
|
+
}
|
|
533
|
+
var TextInput = forwardRef(function TextInput2({ label, error, hint, id, onFocus, onBlur, ...rest }, ref) {
|
|
534
|
+
const inputId = id ?? `optare-${label.toLowerCase().replace(/\s+/g, "-")}`;
|
|
535
|
+
return /* @__PURE__ */ jsxs("div", { style: base.field, children: [
|
|
536
|
+
/* @__PURE__ */ jsx("label", { htmlFor: inputId, style: base.label, children: label }),
|
|
537
|
+
/* @__PURE__ */ jsx(
|
|
538
|
+
"input",
|
|
539
|
+
{
|
|
540
|
+
ref,
|
|
541
|
+
id: inputId,
|
|
542
|
+
style: {
|
|
543
|
+
...base.input,
|
|
544
|
+
...error ? { borderColor: "var(--optare-error)" } : null
|
|
545
|
+
},
|
|
546
|
+
"aria-invalid": error ? true : void 0,
|
|
547
|
+
onFocus: (e) => {
|
|
548
|
+
if (!error) {
|
|
549
|
+
e.currentTarget.style.borderColor = "var(--optare-primary)";
|
|
550
|
+
e.currentTarget.style.boxShadow = "0 0 0 3px var(--optare-ring)";
|
|
551
|
+
}
|
|
552
|
+
onFocus?.(e);
|
|
553
|
+
},
|
|
554
|
+
onBlur: (e) => {
|
|
555
|
+
e.currentTarget.style.borderColor = error ? "var(--optare-error)" : "var(--optare-border-strong)";
|
|
556
|
+
e.currentTarget.style.boxShadow = "none";
|
|
557
|
+
onBlur?.(e);
|
|
558
|
+
},
|
|
559
|
+
...rest
|
|
560
|
+
}
|
|
561
|
+
),
|
|
562
|
+
error ? /* @__PURE__ */ jsx("span", { style: base.fieldError, children: error }) : hint ? /* @__PURE__ */ jsx("span", { style: { ...base.subtitle, fontSize: 12 }, children: hint }) : null
|
|
563
|
+
] });
|
|
564
|
+
});
|
|
565
|
+
function Button({
|
|
566
|
+
variant = "primary",
|
|
567
|
+
busy = false,
|
|
568
|
+
style,
|
|
569
|
+
disabled,
|
|
570
|
+
children,
|
|
571
|
+
...rest
|
|
572
|
+
}) {
|
|
573
|
+
const isDisabled = disabled || busy;
|
|
574
|
+
return /* @__PURE__ */ jsxs(
|
|
575
|
+
"button",
|
|
576
|
+
{
|
|
577
|
+
style: {
|
|
578
|
+
...base.button,
|
|
579
|
+
...variant === "secondary" ? base.buttonSecondary : null,
|
|
580
|
+
...isDisabled ? base.buttonDisabled : null,
|
|
581
|
+
...style
|
|
582
|
+
},
|
|
583
|
+
disabled: isDisabled,
|
|
584
|
+
onMouseEnter: (e) => {
|
|
585
|
+
if (!isDisabled) e.currentTarget.style.filter = "brightness(0.94)";
|
|
586
|
+
},
|
|
587
|
+
onMouseLeave: (e) => {
|
|
588
|
+
e.currentTarget.style.filter = "none";
|
|
589
|
+
},
|
|
590
|
+
...rest,
|
|
591
|
+
children: [
|
|
592
|
+
busy ? /* @__PURE__ */ jsx(Spinner, {}) : null,
|
|
593
|
+
children
|
|
594
|
+
]
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
function Alert({ children }) {
|
|
599
|
+
return /* @__PURE__ */ jsx("div", { role: "alert", style: base.alert, children });
|
|
600
|
+
}
|
|
601
|
+
function Notice({ children }) {
|
|
602
|
+
return /* @__PURE__ */ jsx("div", { style: base.notice, children });
|
|
603
|
+
}
|
|
604
|
+
function LinkButton(props) {
|
|
605
|
+
return /* @__PURE__ */ jsx("button", { type: "button", style: { ...base.link, ...props.style }, ...props });
|
|
606
|
+
}
|
|
607
|
+
function Footer({ children }) {
|
|
608
|
+
return /* @__PURE__ */ jsx("div", { style: base.footer, children });
|
|
609
|
+
}
|
|
610
|
+
function Divider({ children }) {
|
|
611
|
+
if (!children) return /* @__PURE__ */ jsx("div", { style: base.dividerRule });
|
|
612
|
+
return /* @__PURE__ */ jsxs("div", { style: base.divider, children: [
|
|
613
|
+
/* @__PURE__ */ jsx("span", { style: base.dividerRule }),
|
|
614
|
+
/* @__PURE__ */ jsx("span", { children }),
|
|
615
|
+
/* @__PURE__ */ jsx("span", { style: base.dividerRule })
|
|
616
|
+
] });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/components/SocialButtons.tsx
|
|
620
|
+
import { useState as useState3 } from "react";
|
|
621
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
622
|
+
var MARKS = {
|
|
623
|
+
google: {
|
|
624
|
+
name: "Google",
|
|
625
|
+
icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 48 48", "aria-hidden": true, children: [
|
|
626
|
+
/* @__PURE__ */ jsx2("path", { fill: "#EA4335", d: "M24 9.5c3.5 0 6.6 1.2 9 3.6l6.7-6.7C35.6 2.4 30.2 0 24 0 14.6 0 6.5 5.4 2.6 13.2l7.8 6.1C12.2 13.3 17.6 9.5 24 9.5z" }),
|
|
627
|
+
/* @__PURE__ */ jsx2("path", { fill: "#4285F4", d: "M46.5 24.5c0-1.6-.1-3.1-.4-4.5H24v9h12.7c-.6 3-2.3 5.5-4.9 7.2l7.6 5.9c4.4-4.1 7.1-10.1 7.1-17.6z" }),
|
|
628
|
+
/* @__PURE__ */ jsx2("path", { fill: "#FBBC05", d: "M10.4 28.3c-.5-1.5-.8-3.1-.8-4.8s.3-3.3.8-4.8l-7.8-6.1C.9 15.9 0 19.8 0 23.5s.9 7.6 2.6 10.9l7.8-6.1z" }),
|
|
629
|
+
/* @__PURE__ */ jsx2("path", { fill: "#34A853", d: "M24 47c6.2 0 11.5-2 15.3-5.5l-7.6-5.9c-2.1 1.4-4.8 2.3-7.7 2.3-6.4 0-11.8-3.8-13.6-9.3l-7.8 6.1C6.5 41.6 14.6 47 24 47z" })
|
|
630
|
+
] })
|
|
631
|
+
},
|
|
632
|
+
github: {
|
|
633
|
+
name: "GitHub",
|
|
634
|
+
icon: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M12 .5A11.5 11.5 0 0 0 .5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.53-1.34-1.3-1.7-1.3-1.7-1.06-.72.08-.71.08-.71 1.17.08 1.79 1.2 1.79 1.2 1.04 1.78 2.73 1.27 3.4.97.1-.76.41-1.27.74-1.56-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11 11 0 0 1 5.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.41-2.69 5.38-5.25 5.67.42.36.8 1.08.8 2.18v3.23c0 .31.21.67.8.56A11.5 11.5 0 0 0 23.5 12 11.5 11.5 0 0 0 12 .5z" }) })
|
|
635
|
+
},
|
|
636
|
+
microsoft: {
|
|
637
|
+
name: "Microsoft",
|
|
638
|
+
icon: /* @__PURE__ */ jsxs2("svg", { width: "16", height: "16", viewBox: "0 0 23 23", "aria-hidden": true, children: [
|
|
639
|
+
/* @__PURE__ */ jsx2("path", { fill: "#F25022", d: "M1 1h10v10H1z" }),
|
|
640
|
+
/* @__PURE__ */ jsx2("path", { fill: "#7FBA00", d: "M12 1h10v10H12z" }),
|
|
641
|
+
/* @__PURE__ */ jsx2("path", { fill: "#00A4EF", d: "M1 12h10v10H1z" }),
|
|
642
|
+
/* @__PURE__ */ jsx2("path", { fill: "#FFB900", d: "M12 12h10v10H12z" })
|
|
643
|
+
] })
|
|
644
|
+
},
|
|
645
|
+
facebook: {
|
|
646
|
+
name: "Facebook",
|
|
647
|
+
icon: /* @__PURE__ */ jsx2("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "#1877F2", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "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.89v2.26h3.32l-.53 3.49h-2.79V24C19.61 23.1 24 18.1 24 12.07z" }) })
|
|
648
|
+
},
|
|
649
|
+
twitter: {
|
|
650
|
+
name: "X",
|
|
651
|
+
icon: /* @__PURE__ */ jsx2("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": true, children: /* @__PURE__ */ jsx2("path", { d: "M18.9 1.15h3.68l-8.04 9.19L24 22.85h-7.41l-5.8-7.58-6.64 7.58H.46l8.6-9.83L0 1.15h7.6l5.24 6.93 6.06-6.93zm-1.29 19.5h2.04L6.49 3.24H4.3l13.31 17.41z" }) })
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
function SocialButtons(props) {
|
|
655
|
+
const { auth, status } = useOptare();
|
|
656
|
+
const { social } = useAuthMethods();
|
|
657
|
+
const [pending, setPending] = useState3(null);
|
|
658
|
+
const providers = (props.only ?? social).filter((p) => social.includes(p));
|
|
659
|
+
if (providers.length === 0) return null;
|
|
660
|
+
const verb = props.label ?? "Continue with";
|
|
661
|
+
const nameOnly = providers.length > 1;
|
|
662
|
+
const ready = status === "ready" && !!auth;
|
|
663
|
+
async function go(provider) {
|
|
664
|
+
if (!auth) return;
|
|
665
|
+
setPending(provider);
|
|
666
|
+
try {
|
|
667
|
+
await auth.signIn.social({
|
|
668
|
+
provider,
|
|
669
|
+
callbackURL: props.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0),
|
|
670
|
+
errorCallbackURL: props.errorCallbackURL
|
|
671
|
+
});
|
|
672
|
+
} catch {
|
|
673
|
+
setPending(null);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
677
|
+
props.divider !== false ? /* @__PURE__ */ jsx2(Divider, { children: props.dividerLabel ?? "or" }) : null,
|
|
678
|
+
/* @__PURE__ */ jsx2(
|
|
679
|
+
"div",
|
|
680
|
+
{
|
|
681
|
+
style: {
|
|
682
|
+
display: "grid",
|
|
683
|
+
gap: 8,
|
|
684
|
+
gridTemplateColumns: providers.length > 1 ? "1fr 1fr" : "1fr"
|
|
685
|
+
},
|
|
686
|
+
children: providers.map((p) => /* @__PURE__ */ jsxs2(
|
|
687
|
+
Button,
|
|
688
|
+
{
|
|
689
|
+
type: "button",
|
|
690
|
+
variant: "secondary",
|
|
691
|
+
busy: pending === p,
|
|
692
|
+
disabled: !ready || pending !== null && pending !== p,
|
|
693
|
+
onClick: () => go(p),
|
|
694
|
+
style: { fontWeight: 550 },
|
|
695
|
+
children: [
|
|
696
|
+
pending === p ? null : MARKS[p].icon,
|
|
697
|
+
/* @__PURE__ */ jsx2("span", { children: nameOnly ? MARKS[p].name : `${verb} ${MARKS[p].name}` })
|
|
698
|
+
]
|
|
699
|
+
},
|
|
700
|
+
p
|
|
701
|
+
))
|
|
702
|
+
}
|
|
703
|
+
)
|
|
704
|
+
] });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/components/SignIn.tsx
|
|
708
|
+
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
709
|
+
function SignIn(props) {
|
|
710
|
+
const { auth, status } = useOptare();
|
|
711
|
+
const methods = useAuthMethods();
|
|
712
|
+
const allowMagicLink = (props.allowMagicLink ?? methods.magicLink) && methods.magicLink;
|
|
713
|
+
const allowEmailOtp = (props.allowEmailOtp ?? methods.emailOtp) && methods.emailOtp;
|
|
714
|
+
const allowSocial = props.allowSocial ?? true;
|
|
715
|
+
const allowSso = (props.allowSso ?? methods.sso) && methods.sso;
|
|
716
|
+
const callbackURL = props.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
717
|
+
const [mode, setMode] = useState4("password");
|
|
718
|
+
const [email, setEmail] = useState4("");
|
|
719
|
+
const [password, setPassword] = useState4("");
|
|
720
|
+
const [otp, setOtp] = useState4("");
|
|
721
|
+
const [otpSent, setOtpSent] = useState4(false);
|
|
722
|
+
const [fieldErrors, setFieldErrors] = useState4({});
|
|
723
|
+
const [formError, setFormError] = useState4(null);
|
|
724
|
+
const [busy, setBusy] = useState4(false);
|
|
725
|
+
const [magicLinkSent, setMagicLinkSent] = useState4(false);
|
|
726
|
+
const ready = status === "ready" && !!auth;
|
|
727
|
+
function resetTransient() {
|
|
728
|
+
setFormError(null);
|
|
729
|
+
setFieldErrors({});
|
|
730
|
+
}
|
|
731
|
+
function switchMode(next) {
|
|
732
|
+
setMode(next);
|
|
733
|
+
setOtp("");
|
|
734
|
+
setOtpSent(false);
|
|
735
|
+
resetTransient();
|
|
736
|
+
}
|
|
737
|
+
async function onSubmit(e) {
|
|
738
|
+
e.preventDefault();
|
|
739
|
+
if (!auth) return;
|
|
740
|
+
setFormError(null);
|
|
741
|
+
if (mode === "sso") {
|
|
742
|
+
const errs2 = isValidEmail(email) ? {} : { email: "Enter your work email address" };
|
|
743
|
+
setFieldErrors(errs2);
|
|
744
|
+
if (Object.keys(errs2).length) return;
|
|
745
|
+
setBusy(true);
|
|
746
|
+
try {
|
|
747
|
+
const res = await auth.signIn.sso({
|
|
748
|
+
email,
|
|
749
|
+
callbackURL
|
|
750
|
+
});
|
|
751
|
+
if (res?.error) {
|
|
752
|
+
setFormError(
|
|
753
|
+
res.error.message ?? "We couldn't find a single sign-on provider for that email domain."
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
} catch (err) {
|
|
757
|
+
setFormError(err instanceof Error ? err.message : "SSO sign-in failed");
|
|
758
|
+
} finally {
|
|
759
|
+
setBusy(false);
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (mode === "magic-link") {
|
|
764
|
+
const errs2 = isValidEmail(email) ? {} : { email: "Enter a valid email address" };
|
|
765
|
+
setFieldErrors(errs2);
|
|
766
|
+
if (Object.keys(errs2).length) return;
|
|
767
|
+
setBusy(true);
|
|
768
|
+
try {
|
|
769
|
+
const res = await auth.signIn.magicLink({ email, callbackURL });
|
|
770
|
+
if (res.error) {
|
|
771
|
+
setFormError(res.error.message ?? "Could not send the link");
|
|
772
|
+
} else {
|
|
773
|
+
setMagicLinkSent(true);
|
|
774
|
+
}
|
|
775
|
+
} catch (err) {
|
|
776
|
+
setFormError(err instanceof Error ? err.message : "Could not send the link");
|
|
777
|
+
} finally {
|
|
778
|
+
setBusy(false);
|
|
779
|
+
}
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (mode === "email-otp") {
|
|
783
|
+
if (!otpSent) {
|
|
784
|
+
const errs3 = isValidEmail(email) ? {} : { email: "Enter a valid email address" };
|
|
785
|
+
setFieldErrors(errs3);
|
|
786
|
+
if (Object.keys(errs3).length) return;
|
|
787
|
+
setBusy(true);
|
|
788
|
+
try {
|
|
789
|
+
const res = await auth.emailOtp.sendVerificationOtp({ email, type: "sign-in" });
|
|
790
|
+
if (res.error) {
|
|
791
|
+
setFormError(res.error.message ?? "Could not send the code");
|
|
792
|
+
} else {
|
|
793
|
+
setOtpSent(true);
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
setFormError(err instanceof Error ? err.message : "Could not send the code");
|
|
797
|
+
} finally {
|
|
798
|
+
setBusy(false);
|
|
799
|
+
}
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const errs2 = otp.trim().length >= 4 ? {} : { otp: "Enter the code from your email" };
|
|
803
|
+
setFieldErrors(errs2);
|
|
804
|
+
if (Object.keys(errs2).length) return;
|
|
805
|
+
setBusy(true);
|
|
806
|
+
try {
|
|
807
|
+
const res = await auth.signIn.emailOtp({ email, otp: otp.trim() });
|
|
808
|
+
if (res.error) {
|
|
809
|
+
setFormError(res.error.message ?? "That code didn't work");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (res.data?.twoFactorRedirect) return props.onTwoFactor?.();
|
|
813
|
+
notifySessionChanged();
|
|
814
|
+
if (props.onSuccess) props.onSuccess();
|
|
815
|
+
else if (props.redirectTo && typeof window !== "undefined")
|
|
816
|
+
window.location.assign(props.redirectTo);
|
|
817
|
+
} catch (err) {
|
|
818
|
+
setFormError(err instanceof Error ? err.message : "Sign-in failed");
|
|
819
|
+
} finally {
|
|
820
|
+
setBusy(false);
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const errs = validateSignIn({ email, password });
|
|
825
|
+
setFieldErrors(errs);
|
|
826
|
+
if (Object.keys(errs).length) return;
|
|
827
|
+
setBusy(true);
|
|
828
|
+
try {
|
|
829
|
+
const res = await auth.signIn.email({
|
|
830
|
+
email,
|
|
831
|
+
password,
|
|
832
|
+
callbackURL
|
|
833
|
+
});
|
|
834
|
+
if (res.error) {
|
|
835
|
+
setFormError(res.error.message ?? "Incorrect email or password");
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (res.data?.twoFactorRedirect) {
|
|
839
|
+
props.onTwoFactor?.();
|
|
840
|
+
if (!props.onTwoFactor) setFormError("Two-factor authentication is required to continue.");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
notifySessionChanged();
|
|
844
|
+
if (props.onSuccess) props.onSuccess();
|
|
845
|
+
else if (props.redirectTo && typeof window !== "undefined")
|
|
846
|
+
window.location.assign(props.redirectTo);
|
|
847
|
+
} catch (err) {
|
|
848
|
+
setFormError(err instanceof Error ? err.message : "Sign-in failed");
|
|
849
|
+
} finally {
|
|
850
|
+
setBusy(false);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (magicLinkSent) {
|
|
854
|
+
return /* @__PURE__ */ jsxs3(Card, { children: [
|
|
855
|
+
/* @__PURE__ */ jsx3(BrandHeader, { title: "Check your email", subtitle: `We sent a sign-in link to ${email}.` }),
|
|
856
|
+
/* @__PURE__ */ jsx3(Footer, { children: /* @__PURE__ */ jsx3(LinkButton, { onClick: () => {
|
|
857
|
+
setMagicLinkSent(false);
|
|
858
|
+
switchMode("password");
|
|
859
|
+
}, children: "Back to sign in" }) })
|
|
860
|
+
] });
|
|
861
|
+
}
|
|
862
|
+
const submitLabel = busy ? "Please wait\u2026" : mode === "magic-link" ? "Email me a link" : mode === "email-otp" ? otpSent ? "Sign in" : "Email me a code" : mode === "sso" ? "Continue with SSO" : "Sign in";
|
|
863
|
+
return /* @__PURE__ */ jsxs3(Card, { children: [
|
|
864
|
+
/* @__PURE__ */ jsx3(BrandHeader, { title: props.title ?? "Sign in", subtitle: props.subtitle }),
|
|
865
|
+
allowSocial ? /* @__PURE__ */ jsx3(SocialButtons, { callbackURL, divider: false }) : null,
|
|
866
|
+
allowSocial && methods.social.length > 0 ? /* @__PURE__ */ jsx3(Divider, { children: "or" }) : null,
|
|
867
|
+
/* @__PURE__ */ jsxs3("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
|
|
868
|
+
formError ? /* @__PURE__ */ jsx3(Alert, { children: formError }) : null,
|
|
869
|
+
mode === "email-otp" && otpSent ? /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
870
|
+
/* @__PURE__ */ jsxs3(Notice, { children: [
|
|
871
|
+
"Enter the 6-digit code we emailed to ",
|
|
872
|
+
email,
|
|
873
|
+
"."
|
|
874
|
+
] }),
|
|
875
|
+
/* @__PURE__ */ jsx3(
|
|
876
|
+
TextInput,
|
|
877
|
+
{
|
|
878
|
+
label: "Code",
|
|
879
|
+
inputMode: "numeric",
|
|
880
|
+
autoComplete: "one-time-code",
|
|
881
|
+
value: otp,
|
|
882
|
+
onChange: (e) => setOtp(e.currentTarget.value),
|
|
883
|
+
error: fieldErrors.otp,
|
|
884
|
+
disabled: busy
|
|
885
|
+
}
|
|
886
|
+
)
|
|
887
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment3, { children: [
|
|
888
|
+
mode === "sso" ? /* @__PURE__ */ jsx3(Notice, { children: "Sign in through your organization's identity provider." }) : null,
|
|
889
|
+
/* @__PURE__ */ jsx3(
|
|
890
|
+
TextInput,
|
|
891
|
+
{
|
|
892
|
+
label: mode === "sso" ? "Work email" : "Email",
|
|
893
|
+
type: "email",
|
|
894
|
+
autoComplete: "email",
|
|
895
|
+
value: email,
|
|
896
|
+
onChange: (e) => setEmail(e.currentTarget.value),
|
|
897
|
+
error: fieldErrors.email,
|
|
898
|
+
disabled: busy
|
|
899
|
+
}
|
|
900
|
+
),
|
|
901
|
+
mode === "password" ? /* @__PURE__ */ jsx3(
|
|
902
|
+
TextInput,
|
|
903
|
+
{
|
|
904
|
+
label: "Password",
|
|
905
|
+
type: "password",
|
|
906
|
+
autoComplete: "current-password",
|
|
907
|
+
value: password,
|
|
908
|
+
onChange: (e) => setPassword(e.currentTarget.value),
|
|
909
|
+
error: fieldErrors.password,
|
|
910
|
+
disabled: busy
|
|
911
|
+
}
|
|
912
|
+
) : null
|
|
913
|
+
] }),
|
|
914
|
+
/* @__PURE__ */ jsx3(Button, { type: "submit", busy, disabled: !ready, children: submitLabel }),
|
|
915
|
+
mode === "password" && props.onForgotPassword ? /* @__PURE__ */ jsx3(Footer, { children: /* @__PURE__ */ jsx3(LinkButton, { onClick: props.onForgotPassword, children: "Forgot password?" }) }) : null,
|
|
916
|
+
allowMagicLink || allowEmailOtp || allowSso ? /* @__PURE__ */ jsx3(Footer, { children: /* @__PURE__ */ jsxs3("div", { style: { display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }, children: [
|
|
917
|
+
mode !== "password" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("password"), children: "Use a password" }) : null,
|
|
918
|
+
allowMagicLink && mode !== "magic-link" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("magic-link"), children: "Email me a link" }) : null,
|
|
919
|
+
allowEmailOtp && mode !== "email-otp" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("email-otp"), children: "Email me a code" }) : null,
|
|
920
|
+
allowSso && mode !== "sso" ? /* @__PURE__ */ jsx3(LinkButton, { onClick: () => switchMode("sso"), children: "Single sign-on" }) : null
|
|
921
|
+
] }) }) : null
|
|
922
|
+
] }),
|
|
923
|
+
props.footer ? /* @__PURE__ */ jsx3(Footer, { children: props.footer }) : null
|
|
924
|
+
] });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/components/SignUp.tsx
|
|
928
|
+
import { useState as useState5 } from "react";
|
|
929
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
930
|
+
function SignUp(props) {
|
|
931
|
+
const passwordMin = props.passwordMinLength ?? 8;
|
|
932
|
+
const { auth, status } = useOptare();
|
|
933
|
+
const methods = useAuthMethods();
|
|
934
|
+
const allowSocial = props.allowSocial ?? true;
|
|
935
|
+
const callbackURL = props.callbackURL ?? (typeof window !== "undefined" ? window.location.href : void 0);
|
|
936
|
+
const [name, setName] = useState5("");
|
|
937
|
+
const [email, setEmail] = useState5("");
|
|
938
|
+
const [password, setPassword] = useState5("");
|
|
939
|
+
const [fieldErrors, setFieldErrors] = useState5({});
|
|
940
|
+
const [formError, setFormError] = useState5(null);
|
|
941
|
+
const [busy, setBusy] = useState5(false);
|
|
942
|
+
const ready = status === "ready" && !!auth;
|
|
943
|
+
const pwCheck = checkPassword(password, passwordMin);
|
|
944
|
+
async function onSubmit(e) {
|
|
945
|
+
e.preventDefault();
|
|
946
|
+
if (!auth) return;
|
|
947
|
+
setFormError(null);
|
|
948
|
+
const errs = validateSignUp({ name, email, password }, passwordMin);
|
|
949
|
+
setFieldErrors(errs);
|
|
950
|
+
if (Object.keys(errs).length) return;
|
|
951
|
+
setBusy(true);
|
|
952
|
+
try {
|
|
953
|
+
const res = await auth.signUp.email({
|
|
954
|
+
name,
|
|
955
|
+
email,
|
|
956
|
+
password,
|
|
957
|
+
callbackURL
|
|
958
|
+
});
|
|
959
|
+
if (res.error) {
|
|
960
|
+
setFormError(res.error.message ?? "Could not create your account");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (res.data && !res.data.token) {
|
|
964
|
+
props.onVerificationRequired?.(email);
|
|
965
|
+
if (!props.onVerificationRequired)
|
|
966
|
+
setFormError("Check your email to confirm your address, then sign in.");
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
notifySessionChanged();
|
|
970
|
+
if (props.onSuccess) props.onSuccess();
|
|
971
|
+
else if (props.redirectTo && typeof window !== "undefined")
|
|
972
|
+
window.location.assign(props.redirectTo);
|
|
973
|
+
} catch (err) {
|
|
974
|
+
setFormError(err instanceof Error ? err.message : "Sign-up failed");
|
|
975
|
+
} finally {
|
|
976
|
+
setBusy(false);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return /* @__PURE__ */ jsxs4(Card, { children: [
|
|
980
|
+
/* @__PURE__ */ jsx4(BrandHeader, { title: props.title ?? "Create your account", subtitle: props.subtitle }),
|
|
981
|
+
allowSocial ? /* @__PURE__ */ jsx4(SocialButtons, { label: "Sign up with", callbackURL, divider: false }) : null,
|
|
982
|
+
allowSocial && methods.social.length > 0 ? /* @__PURE__ */ jsx4(Divider, { children: "or" }) : null,
|
|
983
|
+
/* @__PURE__ */ jsxs4("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
|
|
984
|
+
formError ? /* @__PURE__ */ jsx4(Alert, { children: formError }) : null,
|
|
985
|
+
/* @__PURE__ */ jsx4(
|
|
986
|
+
TextInput,
|
|
987
|
+
{
|
|
988
|
+
label: "Name",
|
|
989
|
+
autoComplete: "name",
|
|
990
|
+
value: name,
|
|
991
|
+
onChange: (e) => setName(e.currentTarget.value),
|
|
992
|
+
error: fieldErrors.name,
|
|
993
|
+
disabled: busy
|
|
994
|
+
}
|
|
995
|
+
),
|
|
996
|
+
/* @__PURE__ */ jsx4(
|
|
997
|
+
TextInput,
|
|
998
|
+
{
|
|
999
|
+
label: "Email",
|
|
1000
|
+
type: "email",
|
|
1001
|
+
autoComplete: "email",
|
|
1002
|
+
value: email,
|
|
1003
|
+
onChange: (e) => setEmail(e.currentTarget.value),
|
|
1004
|
+
error: fieldErrors.email,
|
|
1005
|
+
disabled: busy
|
|
1006
|
+
}
|
|
1007
|
+
),
|
|
1008
|
+
/* @__PURE__ */ jsx4(
|
|
1009
|
+
TextInput,
|
|
1010
|
+
{
|
|
1011
|
+
label: "Password",
|
|
1012
|
+
type: "password",
|
|
1013
|
+
autoComplete: "new-password",
|
|
1014
|
+
value: password,
|
|
1015
|
+
onChange: (e) => setPassword(e.currentTarget.value),
|
|
1016
|
+
error: fieldErrors.password,
|
|
1017
|
+
disabled: busy
|
|
1018
|
+
}
|
|
1019
|
+
),
|
|
1020
|
+
password && !fieldErrors.password ? /* @__PURE__ */ jsx4(
|
|
1021
|
+
"div",
|
|
1022
|
+
{
|
|
1023
|
+
"aria-hidden": true,
|
|
1024
|
+
style: {
|
|
1025
|
+
height: 4,
|
|
1026
|
+
borderRadius: 2,
|
|
1027
|
+
background: "var(--optare-border)",
|
|
1028
|
+
overflow: "hidden"
|
|
1029
|
+
},
|
|
1030
|
+
children: /* @__PURE__ */ jsx4(
|
|
1031
|
+
"div",
|
|
1032
|
+
{
|
|
1033
|
+
style: {
|
|
1034
|
+
height: "100%",
|
|
1035
|
+
width: `${pwCheck.score / 4 * 100}%`,
|
|
1036
|
+
background: pwCheck.score >= 3 ? "var(--optare-primary)" : "var(--optare-muted)",
|
|
1037
|
+
transition: "width 150ms"
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
)
|
|
1041
|
+
}
|
|
1042
|
+
) : null,
|
|
1043
|
+
/* @__PURE__ */ jsx4(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Creating account\u2026" : "Sign up" })
|
|
1044
|
+
] }),
|
|
1045
|
+
props.footer ? /* @__PURE__ */ jsx4(Footer, { children: props.footer }) : null
|
|
1046
|
+
] });
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/components/AccountButton.tsx
|
|
1050
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState6 } from "react";
|
|
1051
|
+
import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1052
|
+
function initials(nameOrEmail) {
|
|
1053
|
+
const parts = nameOrEmail.trim().split(/\s+/);
|
|
1054
|
+
if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase();
|
|
1055
|
+
return nameOrEmail.slice(0, 2).toUpperCase();
|
|
1056
|
+
}
|
|
1057
|
+
var s = {
|
|
1058
|
+
wrap: { position: "relative", display: "inline-block", fontFamily: "var(--optare-font)" },
|
|
1059
|
+
trigger: {
|
|
1060
|
+
display: "flex",
|
|
1061
|
+
alignItems: "center",
|
|
1062
|
+
gap: 8,
|
|
1063
|
+
padding: "6px 10px",
|
|
1064
|
+
background: "var(--optare-bg)",
|
|
1065
|
+
color: "var(--optare-fg)",
|
|
1066
|
+
border: "1px solid var(--optare-border)",
|
|
1067
|
+
borderRadius: "var(--optare-radius)",
|
|
1068
|
+
cursor: "pointer",
|
|
1069
|
+
font: "inherit",
|
|
1070
|
+
fontSize: 14
|
|
1071
|
+
},
|
|
1072
|
+
avatar: {
|
|
1073
|
+
width: 28,
|
|
1074
|
+
height: 28,
|
|
1075
|
+
borderRadius: "50%",
|
|
1076
|
+
background: "var(--optare-primary)",
|
|
1077
|
+
color: "var(--optare-primary-text)",
|
|
1078
|
+
display: "flex",
|
|
1079
|
+
alignItems: "center",
|
|
1080
|
+
justifyContent: "center",
|
|
1081
|
+
fontSize: 12,
|
|
1082
|
+
fontWeight: 700,
|
|
1083
|
+
objectFit: "cover",
|
|
1084
|
+
flexShrink: 0
|
|
1085
|
+
},
|
|
1086
|
+
menu: {
|
|
1087
|
+
position: "absolute",
|
|
1088
|
+
right: 0,
|
|
1089
|
+
marginTop: 6,
|
|
1090
|
+
minWidth: 200,
|
|
1091
|
+
background: "var(--optare-bg)",
|
|
1092
|
+
color: "var(--optare-fg)",
|
|
1093
|
+
border: "1px solid var(--optare-border)",
|
|
1094
|
+
borderRadius: "var(--optare-radius)",
|
|
1095
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.12)",
|
|
1096
|
+
padding: 6,
|
|
1097
|
+
zIndex: 50
|
|
1098
|
+
},
|
|
1099
|
+
meta: { padding: "8px 10px", borderBottom: "1px solid var(--optare-border)", marginBottom: 4 },
|
|
1100
|
+
name: { fontSize: 14, fontWeight: 600 },
|
|
1101
|
+
email: { fontSize: 12, color: "var(--optare-muted)" },
|
|
1102
|
+
item: {
|
|
1103
|
+
display: "block",
|
|
1104
|
+
width: "100%",
|
|
1105
|
+
textAlign: "left",
|
|
1106
|
+
padding: "8px 10px",
|
|
1107
|
+
background: "none",
|
|
1108
|
+
border: "none",
|
|
1109
|
+
borderRadius: "calc(var(--optare-radius) - 2px)",
|
|
1110
|
+
font: "inherit",
|
|
1111
|
+
fontSize: 13,
|
|
1112
|
+
color: "inherit",
|
|
1113
|
+
cursor: "pointer"
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
function AccountButton(props) {
|
|
1117
|
+
const { cssVars } = useOptare();
|
|
1118
|
+
const { user, isPending, isAuthenticated } = useSession();
|
|
1119
|
+
const { signOut, isPending: signingOut } = useSignOut();
|
|
1120
|
+
const [open, setOpen] = useState6(false);
|
|
1121
|
+
const wrapRef = useRef2(null);
|
|
1122
|
+
useEffect3(() => {
|
|
1123
|
+
if (!open) return;
|
|
1124
|
+
const onDoc = (e) => {
|
|
1125
|
+
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
|
|
1126
|
+
};
|
|
1127
|
+
const onEsc = (e) => e.key === "Escape" && setOpen(false);
|
|
1128
|
+
document.addEventListener("mousedown", onDoc);
|
|
1129
|
+
document.addEventListener("keydown", onEsc);
|
|
1130
|
+
return () => {
|
|
1131
|
+
document.removeEventListener("mousedown", onDoc);
|
|
1132
|
+
document.removeEventListener("keydown", onEsc);
|
|
1133
|
+
};
|
|
1134
|
+
}, [open]);
|
|
1135
|
+
if (isPending) return null;
|
|
1136
|
+
if (!isAuthenticated || !user) return /* @__PURE__ */ jsx5(Fragment4, { children: props.signInSlot ?? null });
|
|
1137
|
+
const u = user;
|
|
1138
|
+
const display = u.name || u.email || "Account";
|
|
1139
|
+
return /* @__PURE__ */ jsxs5("div", { ref: wrapRef, style: { ...cssVars, ...s.wrap }, children: [
|
|
1140
|
+
/* @__PURE__ */ jsxs5(
|
|
1141
|
+
"button",
|
|
1142
|
+
{
|
|
1143
|
+
type: "button",
|
|
1144
|
+
style: s.trigger,
|
|
1145
|
+
"aria-haspopup": "menu",
|
|
1146
|
+
"aria-expanded": open,
|
|
1147
|
+
onClick: () => setOpen((o) => !o),
|
|
1148
|
+
children: [
|
|
1149
|
+
u.image ? /* @__PURE__ */ jsx5("img", { src: u.image, alt: "", style: s.avatar }) : /* @__PURE__ */ jsx5("span", { style: s.avatar, children: initials(display) }),
|
|
1150
|
+
!props.compact ? /* @__PURE__ */ jsx5("span", { children: display }) : null
|
|
1151
|
+
]
|
|
1152
|
+
}
|
|
1153
|
+
),
|
|
1154
|
+
open ? /* @__PURE__ */ jsxs5("div", { role: "menu", style: s.menu, children: [
|
|
1155
|
+
/* @__PURE__ */ jsxs5("div", { style: s.meta, children: [
|
|
1156
|
+
/* @__PURE__ */ jsx5("div", { style: s.name, children: u.name || "\u2014" }),
|
|
1157
|
+
u.email ? /* @__PURE__ */ jsx5("div", { style: s.email, children: u.email }) : null
|
|
1158
|
+
] }),
|
|
1159
|
+
props.menuItems,
|
|
1160
|
+
/* @__PURE__ */ jsx5(
|
|
1161
|
+
"button",
|
|
1162
|
+
{
|
|
1163
|
+
type: "button",
|
|
1164
|
+
role: "menuitem",
|
|
1165
|
+
style: s.item,
|
|
1166
|
+
disabled: signingOut,
|
|
1167
|
+
onClick: async () => {
|
|
1168
|
+
await signOut();
|
|
1169
|
+
setOpen(false);
|
|
1170
|
+
props.onSignedOut?.();
|
|
1171
|
+
},
|
|
1172
|
+
children: signingOut ? "Signing out\u2026" : "Sign out"
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
] }) : null
|
|
1176
|
+
] });
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// src/components/ForgotPassword.tsx
|
|
1180
|
+
import { useState as useState7 } from "react";
|
|
1181
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
1182
|
+
function ForgotPassword(props) {
|
|
1183
|
+
const { auth, status } = useOptare();
|
|
1184
|
+
const [email, setEmail] = useState7("");
|
|
1185
|
+
const [fieldError, setFieldError] = useState7();
|
|
1186
|
+
const [formError, setFormError] = useState7(null);
|
|
1187
|
+
const [busy, setBusy] = useState7(false);
|
|
1188
|
+
const [sent, setSent] = useState7(false);
|
|
1189
|
+
const ready = status === "ready" && !!auth;
|
|
1190
|
+
async function onSubmit(e) {
|
|
1191
|
+
e.preventDefault();
|
|
1192
|
+
if (!auth) return;
|
|
1193
|
+
if (!isValidEmail(email)) {
|
|
1194
|
+
setFieldError("Enter a valid email address");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
setFieldError(void 0);
|
|
1198
|
+
setFormError(null);
|
|
1199
|
+
setBusy(true);
|
|
1200
|
+
try {
|
|
1201
|
+
const res = await auth.forgetPassword({
|
|
1202
|
+
email,
|
|
1203
|
+
// Absolute by default — the reset link is built on the Optare origin, so
|
|
1204
|
+
// a relative path would drop the user there instead of on this app.
|
|
1205
|
+
redirectTo: props.redirectTo ?? (typeof window !== "undefined" ? `${window.location.origin}/reset-password` : "/reset-password")
|
|
1206
|
+
});
|
|
1207
|
+
if (res?.error && res.error.message && /rate|too many/i.test(res.error.message)) {
|
|
1208
|
+
setFormError(res.error.message);
|
|
1209
|
+
} else {
|
|
1210
|
+
setSent(true);
|
|
1211
|
+
}
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
setFormError(err instanceof Error ? err.message : "Could not send the email");
|
|
1214
|
+
} finally {
|
|
1215
|
+
setBusy(false);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (sent) {
|
|
1219
|
+
return /* @__PURE__ */ jsxs6(Card, { children: [
|
|
1220
|
+
/* @__PURE__ */ jsx6(
|
|
1221
|
+
BrandHeader,
|
|
1222
|
+
{
|
|
1223
|
+
title: "Check your email",
|
|
1224
|
+
subtitle: `If an account exists for ${email}, a reset link is on its way.`
|
|
1225
|
+
}
|
|
1226
|
+
),
|
|
1227
|
+
props.onBack ? /* @__PURE__ */ jsx6(Footer, { children: /* @__PURE__ */ jsx6(LinkButton, { onClick: props.onBack, children: "Back to sign in" }) }) : null
|
|
1228
|
+
] });
|
|
1229
|
+
}
|
|
1230
|
+
return /* @__PURE__ */ jsxs6(Card, { children: [
|
|
1231
|
+
/* @__PURE__ */ jsx6(
|
|
1232
|
+
BrandHeader,
|
|
1233
|
+
{
|
|
1234
|
+
title: props.title ?? "Reset your password",
|
|
1235
|
+
subtitle: props.subtitle ?? "We'll email you a link to set a new one."
|
|
1236
|
+
}
|
|
1237
|
+
),
|
|
1238
|
+
/* @__PURE__ */ jsxs6("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
|
|
1239
|
+
formError ? /* @__PURE__ */ jsx6(Alert, { children: formError }) : null,
|
|
1240
|
+
/* @__PURE__ */ jsx6(
|
|
1241
|
+
TextInput,
|
|
1242
|
+
{
|
|
1243
|
+
label: "Email",
|
|
1244
|
+
type: "email",
|
|
1245
|
+
autoComplete: "email",
|
|
1246
|
+
value: email,
|
|
1247
|
+
onChange: (e) => setEmail(e.currentTarget.value),
|
|
1248
|
+
error: fieldError,
|
|
1249
|
+
disabled: busy
|
|
1250
|
+
}
|
|
1251
|
+
),
|
|
1252
|
+
/* @__PURE__ */ jsx6(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Sending\u2026" : "Send reset link" }),
|
|
1253
|
+
props.onBack ? /* @__PURE__ */ jsx6(Footer, { children: /* @__PURE__ */ jsx6(LinkButton, { onClick: props.onBack, children: "Back to sign in" }) }) : null
|
|
1254
|
+
] }),
|
|
1255
|
+
props.footer ? /* @__PURE__ */ jsx6(Footer, { children: props.footer }) : null
|
|
1256
|
+
] });
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// src/components/ResetPassword.tsx
|
|
1260
|
+
import { useEffect as useEffect4, useState as useState8 } from "react";
|
|
1261
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1262
|
+
function tokenFromUrl() {
|
|
1263
|
+
if (typeof window === "undefined") return null;
|
|
1264
|
+
const q = new URLSearchParams(window.location.search);
|
|
1265
|
+
return q.get("token") || q.get("t") || null;
|
|
1266
|
+
}
|
|
1267
|
+
function ResetPassword(props) {
|
|
1268
|
+
const { auth, status } = useOptare();
|
|
1269
|
+
const min = props.passwordMinLength ?? 12;
|
|
1270
|
+
const [token, setToken] = useState8(props.token ?? null);
|
|
1271
|
+
const [password, setPassword] = useState8("");
|
|
1272
|
+
const [confirm, setConfirm] = useState8("");
|
|
1273
|
+
const [fieldErrors, setFieldErrors] = useState8({});
|
|
1274
|
+
const [formError, setFormError] = useState8(null);
|
|
1275
|
+
const [busy, setBusy] = useState8(false);
|
|
1276
|
+
const [done, setDone] = useState8(false);
|
|
1277
|
+
useEffect4(() => {
|
|
1278
|
+
if (!props.token) setToken(tokenFromUrl());
|
|
1279
|
+
}, [props.token]);
|
|
1280
|
+
const ready = status === "ready" && !!auth;
|
|
1281
|
+
const pw = checkPassword(password, min);
|
|
1282
|
+
async function onSubmit(e) {
|
|
1283
|
+
e.preventDefault();
|
|
1284
|
+
if (!auth || !token) return;
|
|
1285
|
+
const errs = {};
|
|
1286
|
+
if (!pw.ok && pw.message) errs.password = pw.message;
|
|
1287
|
+
if (confirm !== password) errs.confirm = "Passwords don't match";
|
|
1288
|
+
setFieldErrors(errs);
|
|
1289
|
+
if (Object.keys(errs).length) return;
|
|
1290
|
+
setFormError(null);
|
|
1291
|
+
setBusy(true);
|
|
1292
|
+
try {
|
|
1293
|
+
const res = await auth.resetPassword({ newPassword: password, token });
|
|
1294
|
+
if (res?.error) {
|
|
1295
|
+
setFormError(res.error.message ?? "That reset link is no longer valid");
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
setDone(true);
|
|
1299
|
+
if (props.onSuccess) props.onSuccess();
|
|
1300
|
+
else if (props.redirectTo && typeof window !== "undefined")
|
|
1301
|
+
window.location.assign(props.redirectTo);
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
setFormError(err instanceof Error ? err.message : "Could not reset the password");
|
|
1304
|
+
} finally {
|
|
1305
|
+
setBusy(false);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (done) {
|
|
1309
|
+
return /* @__PURE__ */ jsxs7(Card, { children: [
|
|
1310
|
+
/* @__PURE__ */ jsx7(BrandHeader, { title: "Password updated", subtitle: "You can sign in with your new password now." }),
|
|
1311
|
+
props.onBack ? /* @__PURE__ */ jsx7(Footer, { children: /* @__PURE__ */ jsx7(LinkButton, { onClick: props.onBack, children: "Go to sign in" }) }) : null
|
|
1312
|
+
] });
|
|
1313
|
+
}
|
|
1314
|
+
if (!token) {
|
|
1315
|
+
return /* @__PURE__ */ jsxs7(Card, { children: [
|
|
1316
|
+
/* @__PURE__ */ jsx7(
|
|
1317
|
+
BrandHeader,
|
|
1318
|
+
{
|
|
1319
|
+
title: "Link expired",
|
|
1320
|
+
subtitle: "This reset link is missing or has already been used. Request a new one."
|
|
1321
|
+
}
|
|
1322
|
+
),
|
|
1323
|
+
props.onBack ? /* @__PURE__ */ jsx7(Footer, { children: /* @__PURE__ */ jsx7(LinkButton, { onClick: props.onBack, children: "Back to sign in" }) }) : null
|
|
1324
|
+
] });
|
|
1325
|
+
}
|
|
1326
|
+
return /* @__PURE__ */ jsxs7(Card, { children: [
|
|
1327
|
+
/* @__PURE__ */ jsx7(BrandHeader, { title: props.title ?? "Set a new password" }),
|
|
1328
|
+
/* @__PURE__ */ jsxs7("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
|
|
1329
|
+
formError ? /* @__PURE__ */ jsx7(Alert, { children: formError }) : null,
|
|
1330
|
+
/* @__PURE__ */ jsx7(
|
|
1331
|
+
TextInput,
|
|
1332
|
+
{
|
|
1333
|
+
label: "New password",
|
|
1334
|
+
type: "password",
|
|
1335
|
+
autoComplete: "new-password",
|
|
1336
|
+
value: password,
|
|
1337
|
+
onChange: (e) => setPassword(e.currentTarget.value),
|
|
1338
|
+
error: fieldErrors.password,
|
|
1339
|
+
disabled: busy
|
|
1340
|
+
}
|
|
1341
|
+
),
|
|
1342
|
+
/* @__PURE__ */ jsx7(
|
|
1343
|
+
TextInput,
|
|
1344
|
+
{
|
|
1345
|
+
label: "Confirm password",
|
|
1346
|
+
type: "password",
|
|
1347
|
+
autoComplete: "new-password",
|
|
1348
|
+
value: confirm,
|
|
1349
|
+
onChange: (e) => setConfirm(e.currentTarget.value),
|
|
1350
|
+
error: fieldErrors.confirm,
|
|
1351
|
+
disabled: busy
|
|
1352
|
+
}
|
|
1353
|
+
),
|
|
1354
|
+
/* @__PURE__ */ jsx7(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Saving\u2026" : "Update password" })
|
|
1355
|
+
] }),
|
|
1356
|
+
props.footer ? /* @__PURE__ */ jsx7(Footer, { children: props.footer }) : null
|
|
1357
|
+
] });
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/components/TwoFactorChallenge.tsx
|
|
1361
|
+
import { useState as useState9 } from "react";
|
|
1362
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
1363
|
+
function TwoFactorChallenge(props) {
|
|
1364
|
+
const { auth, status } = useOptare();
|
|
1365
|
+
const allowBackup = props.allowBackupCode ?? true;
|
|
1366
|
+
const [useBackup, setUseBackup] = useState9(false);
|
|
1367
|
+
const [code, setCode] = useState9("");
|
|
1368
|
+
const [fieldError, setFieldError] = useState9();
|
|
1369
|
+
const [formError, setFormError] = useState9(null);
|
|
1370
|
+
const [busy, setBusy] = useState9(false);
|
|
1371
|
+
const ready = status === "ready" && !!auth;
|
|
1372
|
+
async function onSubmit(e) {
|
|
1373
|
+
e.preventDefault();
|
|
1374
|
+
if (!auth) return;
|
|
1375
|
+
const trimmed = code.trim().replace(/\s+/g, "");
|
|
1376
|
+
if (trimmed.length < (useBackup ? 8 : 6)) {
|
|
1377
|
+
setFieldError(useBackup ? "Enter a full backup code" : "Enter the 6-digit code");
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
setFieldError(void 0);
|
|
1381
|
+
setFormError(null);
|
|
1382
|
+
setBusy(true);
|
|
1383
|
+
try {
|
|
1384
|
+
const call = useBackup ? auth.twoFactor.verifyBackupCode({ code: trimmed }) : auth.twoFactor.verifyTotp({ code: trimmed });
|
|
1385
|
+
const res = await call;
|
|
1386
|
+
if (res?.error) {
|
|
1387
|
+
setFormError(res.error.message ?? "That code didn't work");
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
notifySessionChanged();
|
|
1391
|
+
if (props.onSuccess) props.onSuccess();
|
|
1392
|
+
else if (props.redirectTo && typeof window !== "undefined")
|
|
1393
|
+
window.location.assign(props.redirectTo);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
setFormError(err instanceof Error ? err.message : "Verification failed");
|
|
1396
|
+
} finally {
|
|
1397
|
+
setBusy(false);
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
return /* @__PURE__ */ jsxs8(Card, { children: [
|
|
1401
|
+
/* @__PURE__ */ jsx8(
|
|
1402
|
+
BrandHeader,
|
|
1403
|
+
{
|
|
1404
|
+
title: props.title ?? "Two-factor authentication",
|
|
1405
|
+
subtitle: props.subtitle ?? (useBackup ? "Enter one of your saved backup codes." : "Enter the code from your authenticator app.")
|
|
1406
|
+
}
|
|
1407
|
+
),
|
|
1408
|
+
/* @__PURE__ */ jsxs8("form", { onSubmit, noValidate: true, style: { display: "flex", flexDirection: "column", gap: 16 }, children: [
|
|
1409
|
+
formError ? /* @__PURE__ */ jsx8(Alert, { children: formError }) : null,
|
|
1410
|
+
/* @__PURE__ */ jsx8(
|
|
1411
|
+
TextInput,
|
|
1412
|
+
{
|
|
1413
|
+
label: useBackup ? "Backup code" : "Authentication code",
|
|
1414
|
+
inputMode: useBackup ? "text" : "numeric",
|
|
1415
|
+
autoComplete: "one-time-code",
|
|
1416
|
+
value: code,
|
|
1417
|
+
onChange: (e) => setCode(e.currentTarget.value),
|
|
1418
|
+
error: fieldError,
|
|
1419
|
+
disabled: busy
|
|
1420
|
+
}
|
|
1421
|
+
),
|
|
1422
|
+
/* @__PURE__ */ jsx8(Button, { type: "submit", busy, disabled: !ready, children: busy ? "Verifying\u2026" : "Verify" }),
|
|
1423
|
+
/* @__PURE__ */ jsx8(Footer, { children: /* @__PURE__ */ jsxs8("div", { style: { display: "flex", gap: 12, justifyContent: "center", flexWrap: "wrap" }, children: [
|
|
1424
|
+
allowBackup ? /* @__PURE__ */ jsx8(LinkButton, { onClick: () => {
|
|
1425
|
+
setUseBackup((b) => !b);
|
|
1426
|
+
setCode("");
|
|
1427
|
+
setFieldError(void 0);
|
|
1428
|
+
}, children: useBackup ? "Use an authenticator code" : "Use a backup code" }) : null,
|
|
1429
|
+
props.onCancel ? /* @__PURE__ */ jsx8(LinkButton, { onClick: props.onCancel, children: "Cancel" }) : null
|
|
1430
|
+
] }) })
|
|
1431
|
+
] }),
|
|
1432
|
+
props.footer ? /* @__PURE__ */ jsx8(Footer, { children: props.footer }) : null
|
|
1433
|
+
] });
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/index.ts
|
|
1437
|
+
import {
|
|
1438
|
+
resolveOptareConfig as resolveOptareConfig2,
|
|
1439
|
+
OptareConfigError as OptareConfigError2,
|
|
1440
|
+
DEFAULT_OPTARE_ORIGIN,
|
|
1441
|
+
DEFAULT_AUTH_METHODS as DEFAULT_AUTH_METHODS2
|
|
1442
|
+
} from "@optare/client";
|
|
1443
|
+
export {
|
|
1444
|
+
AccountButton,
|
|
1445
|
+
DEFAULT_AUTH_METHODS2 as DEFAULT_AUTH_METHODS,
|
|
1446
|
+
DEFAULT_OPTARE_ORIGIN,
|
|
1447
|
+
DEFAULT_PRIMARY,
|
|
1448
|
+
DEFAULT_RADIUS,
|
|
1449
|
+
ForgotPassword,
|
|
1450
|
+
OptareConfigError2 as OptareConfigError,
|
|
1451
|
+
OptareProvider,
|
|
1452
|
+
ResetPassword,
|
|
1453
|
+
SignIn,
|
|
1454
|
+
SignUp,
|
|
1455
|
+
SocialButtons,
|
|
1456
|
+
TwoFactorChallenge,
|
|
1457
|
+
brandingToCssVars,
|
|
1458
|
+
checkPassword,
|
|
1459
|
+
contrastText,
|
|
1460
|
+
createReactAuthClient,
|
|
1461
|
+
isValidEmail,
|
|
1462
|
+
notifySessionChanged,
|
|
1463
|
+
resolveOptareConfig2 as resolveOptareConfig,
|
|
1464
|
+
safeColor,
|
|
1465
|
+
safeLength,
|
|
1466
|
+
useActiveOrganization,
|
|
1467
|
+
useAuthMethods,
|
|
1468
|
+
useBranding,
|
|
1469
|
+
useOptare,
|
|
1470
|
+
useOptareAuth,
|
|
1471
|
+
useOptareConfig,
|
|
1472
|
+
useSession,
|
|
1473
|
+
useSignOut,
|
|
1474
|
+
useUser,
|
|
1475
|
+
validateSignIn,
|
|
1476
|
+
validateSignUp
|
|
1477
|
+
};
|