@poly-x/react 0.1.0-alpha.0 → 0.1.0-alpha.10
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.cjs +1394 -30
- package/dist/index.d.cts +282 -11
- package/dist/index.d.mts +282 -11
- package/dist/index.mjs +1393 -33
- package/dist/styles.css +635 -0
- package/package.json +8 -5
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { AuthClient, FetchTransport, InMemoryLock, InMemoryPkceStore, InMemorySessionChannel, InMemorySessionStore, createSessionEngine, generatePkce, randomState, resolveConfig } from "@poly-x/core";
|
|
3
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
4
|
-
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Children, Fragment, createContext, isValidElement, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/auth-channel.ts
|
|
6
6
|
var BroadcastAuthChannel = class {
|
|
7
7
|
supported;
|
|
@@ -108,7 +108,7 @@ function createLoginController(deps) {
|
|
|
108
108
|
redirectUri,
|
|
109
109
|
createdAt: Date.now()
|
|
110
110
|
});
|
|
111
|
-
const url = authClient.
|
|
111
|
+
const url = authClient.buildSignInUrl({
|
|
112
112
|
redirectUri,
|
|
113
113
|
state,
|
|
114
114
|
challenge,
|
|
@@ -256,12 +256,13 @@ var WebLocksLock = class {
|
|
|
256
256
|
};
|
|
257
257
|
//#endregion
|
|
258
258
|
//#region src/provider.tsx
|
|
259
|
-
function PolyXProvider({ publishableKey, baseUrl, children }) {
|
|
259
|
+
function PolyXProvider({ publishableKey, baseUrl, signInUrl, children }) {
|
|
260
260
|
const value = useMemo(() => {
|
|
261
261
|
const authClient = new AuthClient({
|
|
262
262
|
config: resolveConfig({
|
|
263
263
|
key: publishableKey,
|
|
264
264
|
baseUrl,
|
|
265
|
+
signInUrl,
|
|
265
266
|
context: "browser"
|
|
266
267
|
}),
|
|
267
268
|
transport: new FetchTransport()
|
|
@@ -283,7 +284,11 @@ function PolyXProvider({ publishableKey, baseUrl, children }) {
|
|
|
283
284
|
channel: new BroadcastAuthChannel()
|
|
284
285
|
})
|
|
285
286
|
};
|
|
286
|
-
}, [
|
|
287
|
+
}, [
|
|
288
|
+
publishableKey,
|
|
289
|
+
baseUrl,
|
|
290
|
+
signInUrl
|
|
291
|
+
]);
|
|
287
292
|
useEffect(() => {
|
|
288
293
|
value.engine.hydrate();
|
|
289
294
|
return () => value.engine.dispose();
|
|
@@ -294,31 +299,188 @@ function PolyXProvider({ publishableKey, baseUrl, children }) {
|
|
|
294
299
|
});
|
|
295
300
|
}
|
|
296
301
|
//#endregion
|
|
302
|
+
//#region src/bff-session.ts
|
|
303
|
+
/**
|
|
304
|
+
* Client session under BFF custody (ADR-0004).
|
|
305
|
+
*
|
|
306
|
+
* `@poly-x/next` seals the tokens in an httpOnly cookie, so a consuming app mounts no
|
|
307
|
+
* `<PolyXProvider>` and there is no client engine to read — there is nothing for it to own.
|
|
308
|
+
* The hooks still have to work there, so they fall back to this: a single module-level store
|
|
309
|
+
* that reads server truth from the `session` route handler once and feeds the *same*
|
|
310
|
+
* `SessionState` machine the engine drives. That is what ADR-0004 meant by custody being
|
|
311
|
+
* "invisible to the consumer API surface".
|
|
312
|
+
*
|
|
313
|
+
* It follows `<SignIn/>`'s precedent — work without a provider, degrade quietly — rather than
|
|
314
|
+
* asking BFF apps to adopt a provider whose whole purpose is holding tokens they don't have.
|
|
315
|
+
*/
|
|
316
|
+
const SESSION_ENDPOINT = "/api/polyx/session";
|
|
317
|
+
const SIGNOUT_ENDPOINT = "/api/polyx/signout";
|
|
318
|
+
var BffSessionStore = class {
|
|
319
|
+
state = { status: "resolving" };
|
|
320
|
+
listeners = /* @__PURE__ */ new Set();
|
|
321
|
+
loading = false;
|
|
322
|
+
watching = false;
|
|
323
|
+
getState = () => this.state;
|
|
324
|
+
/** SSR has no session to read; render the resolving branch and settle on the client. */
|
|
325
|
+
getServerState = () => ({ status: "resolving" });
|
|
326
|
+
subscribe = (listener) => {
|
|
327
|
+
this.listeners.add(listener);
|
|
328
|
+
this.load();
|
|
329
|
+
this.watchFocus();
|
|
330
|
+
return () => {
|
|
331
|
+
this.listeners.delete(listener);
|
|
332
|
+
};
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* The platform revokes every session for a user when an admin changes their permission,
|
|
336
|
+
* suspends them, or resets their password — and it pushes nothing, so a revoked user keeps
|
|
337
|
+
* looking signed in until they happen to make a request. Re-checking when the tab regains
|
|
338
|
+
* focus is the cheapest way to notice: come back to the tab, discover you're signed out.
|
|
339
|
+
*
|
|
340
|
+
* It is not a replacement for the 401 the next request will get; it just moves the discovery
|
|
341
|
+
* to a moment the user is actually looking at the screen.
|
|
342
|
+
*/
|
|
343
|
+
watchFocus() {
|
|
344
|
+
if (this.watching || typeof window === "undefined") return;
|
|
345
|
+
this.watching = true;
|
|
346
|
+
const recheck = () => {
|
|
347
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
|
|
348
|
+
this.revalidate();
|
|
349
|
+
};
|
|
350
|
+
window.addEventListener("focus", recheck);
|
|
351
|
+
document?.addEventListener?.("visibilitychange", recheck);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Re-read the session without passing through `resolving` — a confirmed session must not
|
|
355
|
+
* flicker the UI back to a loading state. Only a definite signed-out answer signs the user
|
|
356
|
+
* out: an unreachable server is a blip, not a revocation.
|
|
357
|
+
*/
|
|
358
|
+
async revalidate() {
|
|
359
|
+
if (this.loading || this.state.status === "resolving") return;
|
|
360
|
+
this.loading = true;
|
|
361
|
+
try {
|
|
362
|
+
const response = await fetch(SESSION_ENDPOINT, {
|
|
363
|
+
credentials: "same-origin",
|
|
364
|
+
headers: { accept: "application/json" }
|
|
365
|
+
});
|
|
366
|
+
const body = response.ok ? await response.json() : null;
|
|
367
|
+
const signedIn = Boolean(body?.isSignedIn && body.claims);
|
|
368
|
+
if (!signedIn && this.state.status !== "signed-out") this.set({
|
|
369
|
+
status: "signed-out",
|
|
370
|
+
reason: "revoked"
|
|
371
|
+
});
|
|
372
|
+
else if (signedIn && body?.claims) this.set({
|
|
373
|
+
status: "authenticated",
|
|
374
|
+
session: { claims: body.claims }
|
|
375
|
+
});
|
|
376
|
+
} catch {} finally {
|
|
377
|
+
this.loading = false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Reading the session is a one-shot per page load, shared by every component that asks —
|
|
382
|
+
* a user menu and three gates must not mean four round-trips.
|
|
383
|
+
*/
|
|
384
|
+
async load() {
|
|
385
|
+
if (this.loading || this.state.status !== "resolving") return;
|
|
386
|
+
this.loading = true;
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch(SESSION_ENDPOINT, {
|
|
389
|
+
credentials: "same-origin",
|
|
390
|
+
headers: { accept: "application/json" }
|
|
391
|
+
});
|
|
392
|
+
const body = response.ok ? await response.json() : null;
|
|
393
|
+
this.set(body?.isSignedIn && body.claims ? {
|
|
394
|
+
status: "authenticated",
|
|
395
|
+
session: { claims: body.claims }
|
|
396
|
+
} : {
|
|
397
|
+
status: "signed-out",
|
|
398
|
+
reason: "signed-out"
|
|
399
|
+
});
|
|
400
|
+
} catch {
|
|
401
|
+
this.set({
|
|
402
|
+
status: "signed-out",
|
|
403
|
+
reason: "signed-out"
|
|
404
|
+
});
|
|
405
|
+
} finally {
|
|
406
|
+
this.loading = false;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
set(next) {
|
|
410
|
+
this.state = next;
|
|
411
|
+
for (const listener of this.listeners) listener();
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Sign-out has to reach the server — only it can unseal an httpOnly cookie, and only it
|
|
415
|
+
* holds the token needed to revoke the session upstream (X002).
|
|
416
|
+
*
|
|
417
|
+
* A same-origin fetch, not a navigation: the response's Set-Cookie still applies, and it
|
|
418
|
+
* leaves the destination to the caller. That is why the handler is content-negotiated —
|
|
419
|
+
* a server-side redirect target would be an open redirect on the auth path, and the client
|
|
420
|
+
* already knows where it wants to land.
|
|
421
|
+
*/
|
|
422
|
+
signOut = async (options) => {
|
|
423
|
+
const query = options?.scope === "everywhere" ? "?scope=everywhere" : "";
|
|
424
|
+
try {
|
|
425
|
+
await fetch(`${SIGNOUT_ENDPOINT}${query}`, {
|
|
426
|
+
method: "POST",
|
|
427
|
+
credentials: "same-origin",
|
|
428
|
+
headers: { accept: "application/json" }
|
|
429
|
+
});
|
|
430
|
+
} catch {}
|
|
431
|
+
this.set({
|
|
432
|
+
status: "signed-out",
|
|
433
|
+
reason: "signed-out"
|
|
434
|
+
});
|
|
435
|
+
window.location.assign(options?.redirectTo ?? "/");
|
|
436
|
+
};
|
|
437
|
+
/** Null by design: under BFF custody the token never reaches the browser (FR-SES-3). */
|
|
438
|
+
getToken = async () => null;
|
|
439
|
+
};
|
|
440
|
+
let store = new BffSessionStore();
|
|
441
|
+
function getBffSessionStore() {
|
|
442
|
+
return store;
|
|
443
|
+
}
|
|
444
|
+
//#endregion
|
|
297
445
|
//#region src/hooks.ts
|
|
298
|
-
function
|
|
446
|
+
function useSessionSource() {
|
|
299
447
|
const ctx = useContext(PolyXContext);
|
|
300
|
-
if (!ctx)
|
|
301
|
-
|
|
448
|
+
if (!ctx) return {
|
|
449
|
+
source: getBffSessionStore(),
|
|
450
|
+
canSignIn: false
|
|
451
|
+
};
|
|
452
|
+
const { engine, controller } = ctx;
|
|
453
|
+
return {
|
|
454
|
+
source: {
|
|
455
|
+
getState: () => engine.getState(),
|
|
456
|
+
subscribe: (listener) => engine.subscribe(listener),
|
|
457
|
+
signOut: () => engine.signOut(),
|
|
458
|
+
getToken: async () => {
|
|
459
|
+
try {
|
|
460
|
+
return await engine.getAccessToken();
|
|
461
|
+
} catch {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
canSignIn: Boolean(controller)
|
|
467
|
+
};
|
|
302
468
|
}
|
|
303
469
|
/** The raw session state machine value — tearing-free under concurrent React. */
|
|
304
470
|
function useSession() {
|
|
305
|
-
const {
|
|
306
|
-
|
|
307
|
-
const getSnapshot = useCallback(() => engine.getState(), [engine]);
|
|
308
|
-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
471
|
+
const { source } = useSessionSource();
|
|
472
|
+
return useSyncExternalStore(useCallback((onChange) => source.subscribe(onChange), [source]), useCallback(() => source.getState(), [source]), useCallback(() => (source.getServerState ?? source.getState)(), [source]));
|
|
309
473
|
}
|
|
310
474
|
function useAuth() {
|
|
311
|
-
const
|
|
475
|
+
const ctx = useContext(PolyXContext);
|
|
476
|
+
const { source } = useSessionSource();
|
|
312
477
|
const state = useSession();
|
|
313
|
-
const signIn = useCallback((options) =>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
}, [engine]);
|
|
478
|
+
const signIn = useCallback(async (options) => {
|
|
479
|
+
if (!ctx) throw new Error("PolyX: signIn() needs a <PolyXProvider> (it owns the publishable key and the popup flow). Under BFF custody, render <SignIn/> or link to your sign-in route instead.");
|
|
480
|
+
return ctx.controller.signIn(options);
|
|
481
|
+
}, [ctx]);
|
|
482
|
+
const signOut = useCallback((options) => source.signOut(options), [source]);
|
|
483
|
+
const getToken = useCallback(() => source.getToken(), [source]);
|
|
322
484
|
return {
|
|
323
485
|
isLoaded: state.status !== "resolving",
|
|
324
486
|
isSignedIn: state.status === "authenticated" || state.status === "refreshing",
|
|
@@ -329,8 +491,11 @@ function useAuth() {
|
|
|
329
491
|
}
|
|
330
492
|
/** Run this on your callback route to complete a sign-in (see `<AuthCallback>`). */
|
|
331
493
|
function useAuthCallback() {
|
|
332
|
-
const
|
|
333
|
-
return useCallback(() =>
|
|
494
|
+
const ctx = useContext(PolyXContext);
|
|
495
|
+
return useCallback(() => {
|
|
496
|
+
if (!ctx) throw new Error("PolyX: <AuthCallback> / useAuthCallback() must be used within a <PolyXProvider>.");
|
|
497
|
+
return ctx.controller.handleCallback();
|
|
498
|
+
}, [ctx]);
|
|
334
499
|
}
|
|
335
500
|
function useUser() {
|
|
336
501
|
const state = useSession();
|
|
@@ -346,14 +511,890 @@ function Protected({ children, fallback = null, loading = null }) {
|
|
|
346
511
|
}
|
|
347
512
|
//#endregion
|
|
348
513
|
//#region src/components/sign-in.tsx
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
514
|
+
const DEFAULT_LABELS$3 = {
|
|
515
|
+
heading: "Sign in",
|
|
516
|
+
subheading: "Enter your credentials to continue.",
|
|
517
|
+
tagline: "",
|
|
518
|
+
emailLabel: "Email",
|
|
519
|
+
emailPlaceholder: "you@company.com",
|
|
520
|
+
passwordLabel: "Password",
|
|
521
|
+
passwordPlaceholder: "••••••••",
|
|
522
|
+
showPassword: "Show password",
|
|
523
|
+
hidePassword: "Hide password",
|
|
524
|
+
submit: "Sign in",
|
|
525
|
+
submitting: "Signing in…",
|
|
526
|
+
forgotPassword: "Forgot password?",
|
|
527
|
+
chooseWorkspace: "Choose a workspace",
|
|
528
|
+
chooseWorkspaceSubheading: "You have access to more than one workspace.",
|
|
529
|
+
securedBy: "Secured by PolyX",
|
|
530
|
+
setPasswordHeading: "Choose a new password",
|
|
531
|
+
setPasswordSubheading: "Your temporary password must be replaced before you continue.",
|
|
532
|
+
newPasswordLabel: "New password",
|
|
533
|
+
confirmPasswordLabel: "Confirm new password",
|
|
534
|
+
setPasswordSubmit: "Set password and sign in",
|
|
535
|
+
passwordMismatch: "Those passwords don't match.",
|
|
536
|
+
weakPassword: "Password must be at least 6 characters.",
|
|
537
|
+
invalidCredentials: "Invalid email or password.",
|
|
538
|
+
noAccess: "You don't have access to this application.",
|
|
539
|
+
passwordChangeRequired: "You must reset your password before you can sign in.",
|
|
540
|
+
genericError: "Something went wrong. Please try again."
|
|
541
|
+
};
|
|
542
|
+
/** Mirrors poly-auth's own floor (Joi: min 6). */
|
|
543
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
544
|
+
function EyeIcon() {
|
|
545
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
546
|
+
viewBox: "0 0 24 24",
|
|
547
|
+
fill: "none",
|
|
548
|
+
stroke: "currentColor",
|
|
549
|
+
strokeWidth: "2",
|
|
550
|
+
strokeLinecap: "round",
|
|
551
|
+
strokeLinejoin: "round",
|
|
552
|
+
"aria-hidden": "true",
|
|
553
|
+
focusable: "false",
|
|
554
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z" }), /* @__PURE__ */ jsx("circle", {
|
|
555
|
+
cx: "12",
|
|
556
|
+
cy: "12",
|
|
557
|
+
r: "3"
|
|
558
|
+
})]
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
function EyeOffIcon() {
|
|
562
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
563
|
+
viewBox: "0 0 24 24",
|
|
564
|
+
fill: "none",
|
|
565
|
+
stroke: "currentColor",
|
|
566
|
+
strokeWidth: "2",
|
|
567
|
+
strokeLinecap: "round",
|
|
568
|
+
strokeLinejoin: "round",
|
|
569
|
+
"aria-hidden": "true",
|
|
570
|
+
focusable: "false",
|
|
571
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M9.88 4.24A9.1 9.1 0 0 1 12 4c6.5 0 10 7 10 7a13.2 13.2 0 0 1-1.67 2.68M6.61 6.61A13.5 13.5 0 0 0 2 12s3.5 7 10 7a9.7 9.7 0 0 0 5.39-1.61" }), /* @__PURE__ */ jsx("line", {
|
|
572
|
+
x1: "2",
|
|
573
|
+
y1: "2",
|
|
574
|
+
x2: "22",
|
|
575
|
+
y2: "22"
|
|
576
|
+
})]
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
/** A password input with an accessible show/hide reveal toggle (own visibility state). */
|
|
580
|
+
function PasswordField(props) {
|
|
581
|
+
const [visible, setVisible] = useState(false);
|
|
582
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
583
|
+
className: "polyx-signin__field",
|
|
584
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
585
|
+
className: "polyx-signin__label",
|
|
586
|
+
htmlFor: props.id,
|
|
587
|
+
children: props.label
|
|
588
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
589
|
+
className: "polyx-signin__input-wrap",
|
|
590
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
591
|
+
id: props.id,
|
|
592
|
+
className: "polyx-signin__input polyx-signin__input--has-reveal",
|
|
593
|
+
type: visible ? "text" : "password",
|
|
594
|
+
name: props.name,
|
|
595
|
+
autoComplete: props.autoComplete,
|
|
596
|
+
placeholder: props.placeholder,
|
|
597
|
+
required: true,
|
|
598
|
+
minLength: props.minLength,
|
|
599
|
+
value: props.value,
|
|
600
|
+
disabled: props.disabled,
|
|
601
|
+
onChange: (event) => props.onChange(event.target.value)
|
|
602
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
603
|
+
type: "button",
|
|
604
|
+
className: "polyx-signin__reveal",
|
|
605
|
+
"aria-pressed": visible,
|
|
606
|
+
"aria-label": visible ? props.hideLabel : props.showLabel,
|
|
607
|
+
title: visible ? props.hideLabel : props.showLabel,
|
|
608
|
+
disabled: props.disabled,
|
|
609
|
+
onClick: () => setVisible((value) => !value),
|
|
610
|
+
children: visible ? /* @__PURE__ */ jsx(EyeOffIcon, {}) : /* @__PURE__ */ jsx(EyeIcon, {})
|
|
611
|
+
})]
|
|
612
|
+
})]
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
|
|
617
|
+
* consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
|
|
618
|
+
* runs the credential authorize server-side and seals an httpOnly session — no
|
|
619
|
+
* redirect to a hosted PolyX login page, no tokens or credentials in the browser
|
|
620
|
+
* after submit. Handles the workspace picker (select_tenant) inline.
|
|
621
|
+
*
|
|
622
|
+
* Ships styled: `import "@poly-x/react/styles.css"` once, then theme it by
|
|
623
|
+
* overriding the `--polyx-*` custom properties on any ancestor.
|
|
624
|
+
*/
|
|
625
|
+
function SignIn(props) {
|
|
626
|
+
const action = props.action ?? "/api/polyx/authenticate";
|
|
627
|
+
const variant = props.variant ?? "card";
|
|
628
|
+
const labels = {
|
|
629
|
+
...DEFAULT_LABELS$3,
|
|
630
|
+
...props.labels
|
|
631
|
+
};
|
|
632
|
+
const [email, setEmail] = useState("");
|
|
633
|
+
const [password, setPassword] = useState("");
|
|
634
|
+
const [newPassword, setNewPassword] = useState("");
|
|
635
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
636
|
+
const [mustSetPassword, setMustSetPassword] = useState(false);
|
|
637
|
+
const [tenants, setTenants] = useState(null);
|
|
638
|
+
const [submitting, setSubmitting] = useState(false);
|
|
639
|
+
const [error, setError] = useState(null);
|
|
640
|
+
const polyx = useContext(PolyXContext);
|
|
641
|
+
const [branding, setBranding] = useState({});
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
if (!polyx) return;
|
|
644
|
+
let active = true;
|
|
645
|
+
polyx.authClient.fetchBranding(props.organizationCode ? { organizationCode: props.organizationCode } : {}).then((tokens) => {
|
|
646
|
+
if (active) setBranding(tokens);
|
|
647
|
+
});
|
|
648
|
+
return () => {
|
|
649
|
+
active = false;
|
|
650
|
+
};
|
|
651
|
+
}, [polyx, props.organizationCode]);
|
|
652
|
+
const brandingStyle = {};
|
|
653
|
+
if (branding.primaryColor) brandingStyle["--polyx-brand"] = branding.primaryColor;
|
|
654
|
+
if (branding.backgroundColor) {
|
|
655
|
+
brandingStyle["--polyx-bg"] = branding.backgroundColor;
|
|
656
|
+
brandingStyle["--polyx-page-bg"] = branding.backgroundColor;
|
|
657
|
+
}
|
|
658
|
+
const brandingLogo = props.logo ?? (branding.logoUrl ? /* @__PURE__ */ jsx("img", {
|
|
659
|
+
className: "polyx-signin__brand-logo",
|
|
660
|
+
src: branding.logoUrl,
|
|
661
|
+
alt: branding.displayName ?? ""
|
|
662
|
+
}) : void 0);
|
|
663
|
+
/**
|
|
664
|
+
* One round-trip to the BFF. `tenantId` picks a workspace; `passwords` completes
|
|
665
|
+
* the temp-password flow (the BFF re-verifies the temporary password, sets the
|
|
666
|
+
* new one, and signs the user straight in).
|
|
667
|
+
*/
|
|
668
|
+
async function authenticate(options = {}) {
|
|
669
|
+
setSubmitting(true);
|
|
670
|
+
setError(null);
|
|
671
|
+
try {
|
|
672
|
+
const body = await (await fetch(action, {
|
|
673
|
+
method: "POST",
|
|
674
|
+
headers: { "content-type": "application/json" },
|
|
675
|
+
body: JSON.stringify({
|
|
676
|
+
email,
|
|
677
|
+
password,
|
|
678
|
+
organizationCode: props.organizationCode,
|
|
679
|
+
tenantId: options.tenantId,
|
|
680
|
+
...options.passwords
|
|
681
|
+
})
|
|
682
|
+
})).json().catch(() => ({}));
|
|
683
|
+
switch (body.status) {
|
|
684
|
+
case "success":
|
|
685
|
+
window.location.assign(props.afterSignInPath ?? body.redirectTo ?? "/");
|
|
686
|
+
return;
|
|
687
|
+
case "select_tenant":
|
|
688
|
+
setTenants(body.tenants ?? []);
|
|
689
|
+
return;
|
|
690
|
+
case "password_change_required":
|
|
691
|
+
setMustSetPassword(true);
|
|
692
|
+
if (body.userId) props.onPasswordChangeRequired?.(body.userId);
|
|
693
|
+
return;
|
|
694
|
+
case "password_mismatch":
|
|
695
|
+
setError(labels.passwordMismatch);
|
|
696
|
+
return;
|
|
697
|
+
case "weak_password":
|
|
698
|
+
setError(labels.weakPassword);
|
|
699
|
+
return;
|
|
700
|
+
case "invalid_credentials":
|
|
701
|
+
setError(labels.invalidCredentials);
|
|
702
|
+
return;
|
|
703
|
+
case "no_access":
|
|
704
|
+
setError(labels.noAccess);
|
|
705
|
+
return;
|
|
706
|
+
default:
|
|
707
|
+
setError(labels.genericError);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
setError(labels.genericError);
|
|
712
|
+
} finally {
|
|
713
|
+
setSubmitting(false);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
function onSubmit(event) {
|
|
717
|
+
event.preventDefault();
|
|
718
|
+
authenticate();
|
|
719
|
+
}
|
|
720
|
+
function onSetPasswordSubmit(event) {
|
|
721
|
+
event.preventDefault();
|
|
722
|
+
if (newPassword !== confirmPassword) {
|
|
723
|
+
setError(labels.passwordMismatch);
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
authenticate({ passwords: {
|
|
727
|
+
newPassword,
|
|
728
|
+
confirmPassword
|
|
729
|
+
} });
|
|
730
|
+
}
|
|
731
|
+
const picking = tenants !== null;
|
|
732
|
+
const showHeaderLogo = Boolean(brandingLogo) && !(variant === "page");
|
|
733
|
+
const rootClass = [
|
|
734
|
+
"polyx-signin",
|
|
735
|
+
`polyx-signin--${variant}`,
|
|
736
|
+
props.className
|
|
737
|
+
].filter(Boolean).join(" ");
|
|
738
|
+
const step = picking ? "select_tenant" : mustSetPassword ? "set_password" : "credentials";
|
|
739
|
+
const HEADINGS = {
|
|
740
|
+
credentials: {
|
|
741
|
+
heading: labels.heading,
|
|
742
|
+
subheading: labels.subheading
|
|
743
|
+
},
|
|
744
|
+
select_tenant: {
|
|
745
|
+
heading: labels.chooseWorkspace,
|
|
746
|
+
subheading: labels.chooseWorkspaceSubheading
|
|
747
|
+
},
|
|
748
|
+
set_password: {
|
|
749
|
+
heading: labels.setPasswordHeading,
|
|
750
|
+
subheading: labels.setPasswordSubheading
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
const header = /* @__PURE__ */ jsxs("div", {
|
|
754
|
+
className: "polyx-signin__header",
|
|
755
|
+
children: [
|
|
756
|
+
showHeaderLogo ? /* @__PURE__ */ jsx("div", {
|
|
757
|
+
className: "polyx-signin__logo",
|
|
758
|
+
children: brandingLogo
|
|
759
|
+
}) : null,
|
|
760
|
+
/* @__PURE__ */ jsx("h1", {
|
|
761
|
+
className: "polyx-signin__heading",
|
|
762
|
+
children: HEADINGS[step].heading
|
|
763
|
+
}),
|
|
764
|
+
/* @__PURE__ */ jsx("p", {
|
|
765
|
+
className: "polyx-signin__subheading",
|
|
766
|
+
children: HEADINGS[step].subheading
|
|
767
|
+
})
|
|
768
|
+
]
|
|
769
|
+
});
|
|
770
|
+
const errorNode = error ? /* @__PURE__ */ jsx("p", {
|
|
771
|
+
role: "alert",
|
|
772
|
+
className: "polyx-signin__error",
|
|
773
|
+
children: error
|
|
774
|
+
}) : null;
|
|
775
|
+
const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
|
|
776
|
+
className: "polyx-signin__footer",
|
|
777
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
778
|
+
className: "polyx-signin__shield",
|
|
779
|
+
viewBox: "0 0 16 16",
|
|
780
|
+
fill: "currentColor",
|
|
781
|
+
"aria-hidden": "true",
|
|
782
|
+
focusable: "false",
|
|
783
|
+
children: /* @__PURE__ */ jsx("path", { d: "M8 0.5 2 3v4.2c0 3.5 2.5 6.7 6 7.8 3.5-1.1 6-4.3 6-7.8V3L8 .5Zm2.9 5.6-3.4 3.4a.7.7 0 0 1-1 0L5.1 8.1a.7.7 0 1 1 1-1l.9.9 2.9-2.9a.7.7 0 0 1 1 1Z" })
|
|
784
|
+
}), labels.securedBy]
|
|
785
|
+
});
|
|
786
|
+
const tenantPicker = /* @__PURE__ */ jsxs("div", {
|
|
787
|
+
className: "polyx-signin__form",
|
|
788
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
789
|
+
className: "polyx-signin__tenants",
|
|
790
|
+
children: (tenants ?? []).map((tenant) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
|
|
791
|
+
type: "button",
|
|
792
|
+
className: "polyx-signin__tenant",
|
|
793
|
+
disabled: submitting,
|
|
794
|
+
onClick: () => void authenticate({ tenantId: tenant.tenantId }),
|
|
795
|
+
children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
|
|
796
|
+
}) }, tenant.tenantId))
|
|
797
|
+
}), errorNode]
|
|
798
|
+
});
|
|
799
|
+
const setPasswordForm = /* @__PURE__ */ jsxs("form", {
|
|
800
|
+
className: "polyx-signin__form",
|
|
801
|
+
onSubmit: onSetPasswordSubmit,
|
|
802
|
+
children: [
|
|
803
|
+
/* @__PURE__ */ jsx(PasswordField, {
|
|
804
|
+
id: "polyx-signin-new-password",
|
|
805
|
+
label: labels.newPasswordLabel,
|
|
806
|
+
name: "newPassword",
|
|
807
|
+
autoComplete: "new-password",
|
|
808
|
+
placeholder: labels.passwordPlaceholder,
|
|
809
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
810
|
+
value: newPassword,
|
|
811
|
+
disabled: submitting,
|
|
812
|
+
onChange: setNewPassword,
|
|
813
|
+
showLabel: labels.showPassword,
|
|
814
|
+
hideLabel: labels.hidePassword
|
|
815
|
+
}),
|
|
816
|
+
/* @__PURE__ */ jsx(PasswordField, {
|
|
817
|
+
id: "polyx-signin-confirm-password",
|
|
818
|
+
label: labels.confirmPasswordLabel,
|
|
819
|
+
name: "confirmPassword",
|
|
820
|
+
autoComplete: "new-password",
|
|
821
|
+
placeholder: labels.passwordPlaceholder,
|
|
822
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
823
|
+
value: confirmPassword,
|
|
824
|
+
disabled: submitting,
|
|
825
|
+
onChange: setConfirmPassword,
|
|
826
|
+
showLabel: labels.showPassword,
|
|
827
|
+
hideLabel: labels.hidePassword
|
|
828
|
+
}),
|
|
829
|
+
errorNode,
|
|
830
|
+
/* @__PURE__ */ jsx("button", {
|
|
831
|
+
type: "submit",
|
|
832
|
+
className: "polyx-signin__submit",
|
|
833
|
+
disabled: submitting,
|
|
834
|
+
children: submitting ? labels.submitting : labels.setPasswordSubmit
|
|
835
|
+
})
|
|
836
|
+
]
|
|
837
|
+
});
|
|
838
|
+
const card = /* @__PURE__ */ jsxs("div", {
|
|
839
|
+
className: "polyx-signin__card",
|
|
840
|
+
children: [
|
|
841
|
+
header,
|
|
842
|
+
step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ jsxs("form", {
|
|
843
|
+
className: "polyx-signin__form",
|
|
844
|
+
onSubmit,
|
|
845
|
+
children: [
|
|
846
|
+
/* @__PURE__ */ jsxs("label", {
|
|
847
|
+
className: "polyx-signin__field",
|
|
848
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
849
|
+
className: "polyx-signin__label",
|
|
850
|
+
children: labels.emailLabel
|
|
851
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
852
|
+
className: "polyx-signin__input",
|
|
853
|
+
type: "email",
|
|
854
|
+
name: "email",
|
|
855
|
+
autoComplete: "username",
|
|
856
|
+
placeholder: labels.emailPlaceholder,
|
|
857
|
+
required: true,
|
|
858
|
+
value: email,
|
|
859
|
+
disabled: submitting,
|
|
860
|
+
onChange: (event) => setEmail(event.target.value)
|
|
861
|
+
})]
|
|
862
|
+
}),
|
|
863
|
+
/* @__PURE__ */ jsx(PasswordField, {
|
|
864
|
+
id: "polyx-signin-password",
|
|
865
|
+
label: labels.passwordLabel,
|
|
866
|
+
name: "password",
|
|
867
|
+
autoComplete: "current-password",
|
|
868
|
+
placeholder: labels.passwordPlaceholder,
|
|
869
|
+
value: password,
|
|
870
|
+
disabled: submitting,
|
|
871
|
+
onChange: setPassword,
|
|
872
|
+
showLabel: labels.showPassword,
|
|
873
|
+
hideLabel: labels.hidePassword
|
|
874
|
+
}),
|
|
875
|
+
props.forgotPasswordUrl ? /* @__PURE__ */ jsx("div", {
|
|
876
|
+
className: "polyx-signin__forgot",
|
|
877
|
+
children: /* @__PURE__ */ jsx("a", {
|
|
878
|
+
className: "polyx-signin__link",
|
|
879
|
+
href: props.forgotPasswordUrl,
|
|
880
|
+
children: labels.forgotPassword
|
|
881
|
+
})
|
|
882
|
+
}) : null,
|
|
883
|
+
errorNode,
|
|
884
|
+
/* @__PURE__ */ jsx("button", {
|
|
885
|
+
type: "submit",
|
|
886
|
+
className: "polyx-signin__submit",
|
|
887
|
+
disabled: submitting,
|
|
888
|
+
children: submitting ? labels.submitting : labels.submit
|
|
889
|
+
})
|
|
890
|
+
]
|
|
891
|
+
}),
|
|
892
|
+
footer
|
|
893
|
+
]
|
|
894
|
+
});
|
|
895
|
+
if (variant === "page") return /* @__PURE__ */ jsxs("div", {
|
|
896
|
+
className: rootClass,
|
|
897
|
+
"data-polyx-signin": step,
|
|
898
|
+
style: brandingStyle,
|
|
899
|
+
children: [/* @__PURE__ */ jsx("aside", {
|
|
900
|
+
className: "polyx-signin__panel",
|
|
901
|
+
children: props.panel ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [brandingLogo ? /* @__PURE__ */ jsx("div", {
|
|
902
|
+
className: "polyx-signin__panel-logo",
|
|
903
|
+
children: brandingLogo
|
|
904
|
+
}) : null, labels.tagline ? /* @__PURE__ */ jsx("p", {
|
|
905
|
+
className: "polyx-signin__tagline",
|
|
906
|
+
children: labels.tagline
|
|
907
|
+
}) : null] })
|
|
908
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
909
|
+
className: "polyx-signin__main",
|
|
910
|
+
children: card
|
|
911
|
+
})]
|
|
912
|
+
});
|
|
913
|
+
return /* @__PURE__ */ jsx("div", {
|
|
914
|
+
className: rootClass,
|
|
915
|
+
"data-polyx-signin": step,
|
|
916
|
+
style: brandingStyle,
|
|
917
|
+
children: card
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
//#endregion
|
|
921
|
+
//#region src/components/_recovery-status.tsx
|
|
922
|
+
const ICONS = {
|
|
923
|
+
mail: /* @__PURE__ */ jsxs("svg", {
|
|
924
|
+
viewBox: "0 0 24 24",
|
|
925
|
+
fill: "none",
|
|
926
|
+
stroke: "currentColor",
|
|
927
|
+
strokeWidth: "1.75",
|
|
928
|
+
"aria-hidden": "true",
|
|
929
|
+
focusable: "false",
|
|
930
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
931
|
+
x: "3",
|
|
932
|
+
y: "5",
|
|
933
|
+
width: "18",
|
|
934
|
+
height: "14",
|
|
935
|
+
rx: "2.5"
|
|
936
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
937
|
+
d: "m4 7 8 6 8-6",
|
|
938
|
+
strokeLinecap: "round",
|
|
939
|
+
strokeLinejoin: "round"
|
|
940
|
+
})]
|
|
941
|
+
}),
|
|
942
|
+
check: /* @__PURE__ */ jsx("svg", {
|
|
943
|
+
viewBox: "0 0 24 24",
|
|
944
|
+
fill: "none",
|
|
945
|
+
stroke: "currentColor",
|
|
946
|
+
strokeWidth: "2",
|
|
947
|
+
"aria-hidden": "true",
|
|
948
|
+
focusable: "false",
|
|
949
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
950
|
+
d: "m5 12.5 4.5 4.5L19 7",
|
|
951
|
+
strokeLinecap: "round",
|
|
952
|
+
strokeLinejoin: "round"
|
|
953
|
+
})
|
|
954
|
+
}),
|
|
955
|
+
warning: /* @__PURE__ */ jsxs("svg", {
|
|
956
|
+
viewBox: "0 0 24 24",
|
|
957
|
+
fill: "none",
|
|
958
|
+
stroke: "currentColor",
|
|
959
|
+
strokeWidth: "1.75",
|
|
960
|
+
"aria-hidden": "true",
|
|
961
|
+
focusable: "false",
|
|
962
|
+
children: [
|
|
963
|
+
/* @__PURE__ */ jsx("circle", {
|
|
964
|
+
cx: "12",
|
|
965
|
+
cy: "12",
|
|
966
|
+
r: "9"
|
|
967
|
+
}),
|
|
968
|
+
/* @__PURE__ */ jsx("path", {
|
|
969
|
+
d: "M12 7.5v5",
|
|
970
|
+
strokeLinecap: "round"
|
|
971
|
+
}),
|
|
972
|
+
/* @__PURE__ */ jsx("circle", {
|
|
973
|
+
cx: "12",
|
|
974
|
+
cy: "16",
|
|
975
|
+
r: "0.6",
|
|
976
|
+
fill: "currentColor",
|
|
977
|
+
stroke: "none"
|
|
978
|
+
})
|
|
979
|
+
]
|
|
980
|
+
})
|
|
981
|
+
};
|
|
982
|
+
/**
|
|
983
|
+
* A centered, icon-topped confirmation card — the shape every recovery terminal
|
|
984
|
+
* state shares (link sent, password updated, link expired). Reuses the `<SignIn/>`
|
|
985
|
+
* class anatomy so it themes from the same `--polyx-*` custom properties.
|
|
986
|
+
*/
|
|
987
|
+
function RecoveryStatus(props) {
|
|
988
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
989
|
+
className: "polyx-signin__card",
|
|
990
|
+
children: [
|
|
991
|
+
/* @__PURE__ */ jsxs("div", {
|
|
992
|
+
className: "polyx-signin__header polyx-signin__header--center",
|
|
993
|
+
children: [
|
|
994
|
+
/* @__PURE__ */ jsx("span", {
|
|
995
|
+
className: `polyx-signin__status-icon polyx-signin__status-icon--${props.tone ?? "brand"}`,
|
|
996
|
+
children: ICONS[props.icon]
|
|
997
|
+
}),
|
|
998
|
+
/* @__PURE__ */ jsx("h1", {
|
|
999
|
+
className: "polyx-signin__heading",
|
|
1000
|
+
children: props.heading
|
|
1001
|
+
}),
|
|
1002
|
+
/* @__PURE__ */ jsx("p", {
|
|
1003
|
+
className: "polyx-signin__subheading",
|
|
1004
|
+
children: props.children
|
|
1005
|
+
})
|
|
1006
|
+
]
|
|
1007
|
+
}),
|
|
1008
|
+
props.actions ? /* @__PURE__ */ jsx("div", {
|
|
1009
|
+
className: "polyx-signin__actions",
|
|
1010
|
+
children: props.actions
|
|
1011
|
+
}) : null,
|
|
1012
|
+
props.footer
|
|
1013
|
+
]
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
//#endregion
|
|
1017
|
+
//#region src/components/forgot-password.tsx
|
|
1018
|
+
const DEFAULT_LABELS$2 = {
|
|
1019
|
+
heading: "Reset your password",
|
|
1020
|
+
subheading: "Enter your email and we'll send you a reset link.",
|
|
1021
|
+
emailLabel: "Email",
|
|
1022
|
+
emailPlaceholder: "you@company.com",
|
|
1023
|
+
submit: "Send reset link",
|
|
1024
|
+
submitting: "Sending…",
|
|
1025
|
+
sentHeading: "Check your email",
|
|
1026
|
+
sentSubheading: "If an account exists for {email}, a password reset link is on its way. It expires in 30 minutes.",
|
|
1027
|
+
differentEmail: "Use a different email",
|
|
1028
|
+
backToSignIn: "Back to sign in",
|
|
1029
|
+
securedBy: "Secured by PolyX",
|
|
1030
|
+
genericError: "Something went wrong. Please try again."
|
|
1031
|
+
};
|
|
1032
|
+
/** Splits a label on `{email}` and drops the entered address in, emphasized. */
|
|
1033
|
+
function withEmail(template, email) {
|
|
1034
|
+
return template.split("{email}").map((chunk, index, all) => /* @__PURE__ */ jsxs(Fragment, { children: [chunk, index < all.length - 1 ? /* @__PURE__ */ jsx("span", {
|
|
1035
|
+
className: "polyx-signin__email",
|
|
1036
|
+
children: email || "that address"
|
|
1037
|
+
}) : null] }, index));
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* The self-service "forgot password" form (poly-x v5 / F040). Renders an email field
|
|
1041
|
+
* inside the consuming app and POSTs to the SDK's BFF (`@poly-x/next` `forgotPassword`),
|
|
1042
|
+
* which asks poly-auth to email a single-use reset link SERVER-SIDE. The response is
|
|
1043
|
+
* always the same — no token, no account-existence signal — so after submit the form
|
|
1044
|
+
* shows an identical "check your email" state whether or not the account exists.
|
|
1045
|
+
*
|
|
1046
|
+
* Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
|
|
1047
|
+
* via the `--polyx-*` custom properties. Target `.polyx-forgot` for form-specific tweaks.
|
|
1048
|
+
*/
|
|
1049
|
+
function ForgotPassword(props) {
|
|
1050
|
+
const action = props.action ?? "/api/polyx/forgot-password";
|
|
1051
|
+
const signInPath = props.signInPath ?? "/sign-in";
|
|
1052
|
+
const labels = {
|
|
1053
|
+
...DEFAULT_LABELS$2,
|
|
1054
|
+
...props.labels
|
|
1055
|
+
};
|
|
1056
|
+
const [email, setEmail] = useState("");
|
|
1057
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1058
|
+
const [sent, setSent] = useState(false);
|
|
1059
|
+
const [error, setError] = useState(null);
|
|
1060
|
+
async function onSubmit(event) {
|
|
1061
|
+
event.preventDefault();
|
|
1062
|
+
setSubmitting(true);
|
|
1063
|
+
setError(null);
|
|
1064
|
+
try {
|
|
1065
|
+
if ((await (await fetch(action, {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
headers: { "content-type": "application/json" },
|
|
1068
|
+
body: JSON.stringify({ email })
|
|
1069
|
+
})).json().catch(() => ({}))).status === "accepted") {
|
|
1070
|
+
setSent(true);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
setError(labels.genericError);
|
|
1074
|
+
} catch {
|
|
1075
|
+
setError(labels.genericError);
|
|
1076
|
+
} finally {
|
|
1077
|
+
setSubmitting(false);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const rootClass = [
|
|
1081
|
+
"polyx-signin",
|
|
1082
|
+
"polyx-forgot",
|
|
1083
|
+
props.className
|
|
1084
|
+
].filter(Boolean).join(" ");
|
|
1085
|
+
const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
|
|
1086
|
+
className: "polyx-signin__footer",
|
|
1087
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
1088
|
+
className: "polyx-signin__shield",
|
|
1089
|
+
viewBox: "0 0 16 16",
|
|
1090
|
+
fill: "currentColor",
|
|
1091
|
+
"aria-hidden": "true",
|
|
1092
|
+
focusable: "false",
|
|
1093
|
+
children: /* @__PURE__ */ jsx("path", { d: "M8 0.5 2 3v4.2c0 3.5 2.5 6.7 6 7.8 3.5-1.1 6-4.3 6-7.8V3L8 .5Zm2.9 5.6-3.4 3.4a.7.7 0 0 1-1 0L5.1 8.1a.7.7 0 1 1 1-1l.9.9 2.9-2.9a.7.7 0 0 1 1 1Z" })
|
|
1094
|
+
}), labels.securedBy]
|
|
1095
|
+
});
|
|
1096
|
+
const header = (heading, subheading) => /* @__PURE__ */ jsxs("div", {
|
|
1097
|
+
className: "polyx-signin__header",
|
|
1098
|
+
children: [
|
|
1099
|
+
props.logo ? /* @__PURE__ */ jsx("div", {
|
|
1100
|
+
className: "polyx-signin__logo",
|
|
1101
|
+
children: props.logo
|
|
1102
|
+
}) : null,
|
|
1103
|
+
/* @__PURE__ */ jsx("h1", {
|
|
1104
|
+
className: "polyx-signin__heading",
|
|
1105
|
+
children: heading
|
|
1106
|
+
}),
|
|
1107
|
+
/* @__PURE__ */ jsx("p", {
|
|
1108
|
+
className: "polyx-signin__subheading",
|
|
1109
|
+
children: subheading
|
|
1110
|
+
})
|
|
1111
|
+
]
|
|
1112
|
+
});
|
|
1113
|
+
if (sent) return /* @__PURE__ */ jsx("div", {
|
|
1114
|
+
className: rootClass,
|
|
1115
|
+
"data-polyx-forgot": "sent",
|
|
1116
|
+
children: /* @__PURE__ */ jsx(RecoveryStatus, {
|
|
1117
|
+
icon: "mail",
|
|
1118
|
+
heading: labels.sentHeading,
|
|
1119
|
+
footer,
|
|
1120
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
|
|
1121
|
+
type: "button",
|
|
1122
|
+
className: "polyx-signin__link",
|
|
1123
|
+
onClick: () => setSent(false),
|
|
1124
|
+
children: labels.differentEmail
|
|
1125
|
+
}), /* @__PURE__ */ jsx("a", {
|
|
1126
|
+
className: "polyx-signin__link",
|
|
1127
|
+
href: signInPath,
|
|
1128
|
+
children: labels.backToSignIn
|
|
1129
|
+
})] }),
|
|
1130
|
+
children: withEmail(labels.sentSubheading, email)
|
|
1131
|
+
})
|
|
1132
|
+
});
|
|
1133
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1134
|
+
className: rootClass,
|
|
1135
|
+
"data-polyx-forgot": "request",
|
|
1136
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1137
|
+
className: "polyx-signin__card",
|
|
1138
|
+
children: [
|
|
1139
|
+
header(labels.heading, labels.subheading),
|
|
1140
|
+
/* @__PURE__ */ jsxs("form", {
|
|
1141
|
+
className: "polyx-signin__form",
|
|
1142
|
+
onSubmit,
|
|
1143
|
+
children: [
|
|
1144
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1145
|
+
className: "polyx-signin__field",
|
|
1146
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1147
|
+
className: "polyx-signin__label",
|
|
1148
|
+
children: labels.emailLabel
|
|
1149
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1150
|
+
className: "polyx-signin__input",
|
|
1151
|
+
type: "email",
|
|
1152
|
+
name: "email",
|
|
1153
|
+
autoComplete: "username",
|
|
1154
|
+
placeholder: labels.emailPlaceholder,
|
|
1155
|
+
required: true,
|
|
1156
|
+
value: email,
|
|
1157
|
+
disabled: submitting,
|
|
1158
|
+
onChange: (event) => setEmail(event.target.value)
|
|
1159
|
+
})]
|
|
1160
|
+
}),
|
|
1161
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
1162
|
+
role: "alert",
|
|
1163
|
+
className: "polyx-signin__error",
|
|
1164
|
+
children: error
|
|
1165
|
+
}) : null,
|
|
1166
|
+
/* @__PURE__ */ jsx("button", {
|
|
1167
|
+
type: "submit",
|
|
1168
|
+
className: "polyx-signin__submit",
|
|
1169
|
+
disabled: submitting,
|
|
1170
|
+
children: submitting ? labels.submitting : labels.submit
|
|
1171
|
+
})
|
|
1172
|
+
]
|
|
1173
|
+
}),
|
|
1174
|
+
footer
|
|
1175
|
+
]
|
|
1176
|
+
})
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/components/reset-password.tsx
|
|
1181
|
+
const DEFAULT_LABELS$1 = {
|
|
1182
|
+
heading: "Choose a new password",
|
|
1183
|
+
subheading: "Enter and confirm your new password.",
|
|
1184
|
+
newPasswordLabel: "New password",
|
|
1185
|
+
confirmPasswordLabel: "Confirm new password",
|
|
1186
|
+
passwordPlaceholder: "••••••••",
|
|
1187
|
+
submit: "Reset password",
|
|
1188
|
+
submitting: "Resetting…",
|
|
1189
|
+
successHeading: "Password updated",
|
|
1190
|
+
successSubheading: "You can now sign in with your new password.",
|
|
1191
|
+
invalidHeading: "This link is invalid or expired",
|
|
1192
|
+
invalidSubheading: "Password reset links can be used once and expire quickly. Request a new one.",
|
|
1193
|
+
requestNewLink: "Request a new link",
|
|
1194
|
+
backToSignIn: "Back to sign in",
|
|
1195
|
+
securedBy: "Secured by PolyX",
|
|
1196
|
+
passwordMismatch: "Those passwords don't match.",
|
|
1197
|
+
weakPassword: "Password must be at least 8 characters.",
|
|
1198
|
+
genericError: "Something went wrong. Please try again."
|
|
1199
|
+
};
|
|
1200
|
+
/** Matches poly-auth's v5 reset floor (F040). */
|
|
1201
|
+
const DEFAULT_MIN_LENGTH = 8;
|
|
1202
|
+
/**
|
|
1203
|
+
* The reset-completion form (poly-x v5 / F040). Reads the opaque token from the URL,
|
|
1204
|
+
* collects a new password, and POSTs to the SDK's BFF (`@poly-x/next` `resetPassword`),
|
|
1205
|
+
* which redeems the token single-use server-side. The token is never decoded in the
|
|
1206
|
+
* browser — identity, validity, and expiry are all enforced by the backend.
|
|
1207
|
+
*
|
|
1208
|
+
* Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
|
|
1209
|
+
* via the `--polyx-*` custom properties. Target `.polyx-reset` for form-specific tweaks.
|
|
1210
|
+
*/
|
|
1211
|
+
function ResetPassword(props) {
|
|
1212
|
+
const action = props.action ?? "/api/polyx/reset-password";
|
|
1213
|
+
const minLength = props.minLength ?? DEFAULT_MIN_LENGTH;
|
|
1214
|
+
const signInPath = props.signInPath ?? "/sign-in";
|
|
1215
|
+
const forgotPasswordPath = props.forgotPasswordPath ?? "/forgot-password";
|
|
1216
|
+
const labels = {
|
|
1217
|
+
...DEFAULT_LABELS$1,
|
|
1218
|
+
...props.labels
|
|
1219
|
+
};
|
|
1220
|
+
const [token] = useState(() => props.token ?? (typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("token") ?? "" : ""));
|
|
1221
|
+
const [newPassword, setNewPassword] = useState("");
|
|
1222
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
1223
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1224
|
+
const [error, setError] = useState(null);
|
|
1225
|
+
const [step, setStep] = useState(token ? "form" : "invalid");
|
|
1226
|
+
async function onSubmit(event) {
|
|
1227
|
+
event.preventDefault();
|
|
1228
|
+
if (newPassword !== confirmPassword) {
|
|
1229
|
+
setError(labels.passwordMismatch);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
setSubmitting(true);
|
|
1233
|
+
setError(null);
|
|
1234
|
+
try {
|
|
1235
|
+
switch ((await (await fetch(action, {
|
|
1236
|
+
method: "POST",
|
|
1237
|
+
headers: { "content-type": "application/json" },
|
|
1238
|
+
body: JSON.stringify({
|
|
1239
|
+
token,
|
|
1240
|
+
newPassword,
|
|
1241
|
+
confirmPassword
|
|
1242
|
+
})
|
|
1243
|
+
})).json().catch(() => ({}))).status) {
|
|
1244
|
+
case "success":
|
|
1245
|
+
setStep("success");
|
|
1246
|
+
if (props.afterResetPath) window.location.assign(props.afterResetPath);
|
|
1247
|
+
return;
|
|
1248
|
+
case "weak_password":
|
|
1249
|
+
setError(labels.weakPassword);
|
|
1250
|
+
return;
|
|
1251
|
+
case "password_mismatch":
|
|
1252
|
+
setError(labels.passwordMismatch);
|
|
1253
|
+
return;
|
|
1254
|
+
case "invalid_or_expired":
|
|
1255
|
+
setStep("invalid");
|
|
1256
|
+
return;
|
|
1257
|
+
default:
|
|
1258
|
+
setError(labels.genericError);
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
} catch {
|
|
1262
|
+
setError(labels.genericError);
|
|
1263
|
+
} finally {
|
|
1264
|
+
setSubmitting(false);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
const rootClass = [
|
|
1268
|
+
"polyx-signin",
|
|
1269
|
+
"polyx-reset",
|
|
1270
|
+
props.className
|
|
1271
|
+
].filter(Boolean).join(" ");
|
|
1272
|
+
const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
|
|
1273
|
+
className: "polyx-signin__footer",
|
|
1274
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
1275
|
+
className: "polyx-signin__shield",
|
|
1276
|
+
viewBox: "0 0 16 16",
|
|
1277
|
+
fill: "currentColor",
|
|
1278
|
+
"aria-hidden": "true",
|
|
1279
|
+
focusable: "false",
|
|
1280
|
+
children: /* @__PURE__ */ jsx("path", { d: "M8 0.5 2 3v4.2c0 3.5 2.5 6.7 6 7.8 3.5-1.1 6-4.3 6-7.8V3L8 .5Zm2.9 5.6-3.4 3.4a.7.7 0 0 1-1 0L5.1 8.1a.7.7 0 1 1 1-1l.9.9 2.9-2.9a.7.7 0 0 1 1 1Z" })
|
|
1281
|
+
}), labels.securedBy]
|
|
1282
|
+
});
|
|
1283
|
+
const header = (heading, subheading) => /* @__PURE__ */ jsxs("div", {
|
|
1284
|
+
className: "polyx-signin__header",
|
|
1285
|
+
children: [
|
|
1286
|
+
props.logo ? /* @__PURE__ */ jsx("div", {
|
|
1287
|
+
className: "polyx-signin__logo",
|
|
1288
|
+
children: props.logo
|
|
1289
|
+
}) : null,
|
|
1290
|
+
/* @__PURE__ */ jsx("h1", {
|
|
1291
|
+
className: "polyx-signin__heading",
|
|
1292
|
+
children: heading
|
|
1293
|
+
}),
|
|
1294
|
+
/* @__PURE__ */ jsx("p", {
|
|
1295
|
+
className: "polyx-signin__subheading",
|
|
1296
|
+
children: subheading
|
|
1297
|
+
})
|
|
1298
|
+
]
|
|
1299
|
+
});
|
|
1300
|
+
if (step === "success") return /* @__PURE__ */ jsx("div", {
|
|
1301
|
+
className: rootClass,
|
|
1302
|
+
"data-polyx-reset": "success",
|
|
1303
|
+
children: /* @__PURE__ */ jsx(RecoveryStatus, {
|
|
1304
|
+
icon: "check",
|
|
1305
|
+
heading: labels.successHeading,
|
|
1306
|
+
footer,
|
|
1307
|
+
actions: /* @__PURE__ */ jsx("a", {
|
|
1308
|
+
className: "polyx-signin__link",
|
|
1309
|
+
href: signInPath,
|
|
1310
|
+
children: labels.backToSignIn
|
|
1311
|
+
}),
|
|
1312
|
+
children: labels.successSubheading
|
|
1313
|
+
})
|
|
1314
|
+
});
|
|
1315
|
+
if (step === "invalid") return /* @__PURE__ */ jsx("div", {
|
|
1316
|
+
className: rootClass,
|
|
1317
|
+
"data-polyx-reset": "invalid",
|
|
1318
|
+
children: /* @__PURE__ */ jsx(RecoveryStatus, {
|
|
1319
|
+
icon: "warning",
|
|
1320
|
+
tone: "danger",
|
|
1321
|
+
heading: labels.invalidHeading,
|
|
1322
|
+
footer,
|
|
1323
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("a", {
|
|
1324
|
+
className: "polyx-signin__link",
|
|
1325
|
+
href: forgotPasswordPath,
|
|
1326
|
+
children: labels.requestNewLink
|
|
1327
|
+
}), /* @__PURE__ */ jsx("a", {
|
|
1328
|
+
className: "polyx-signin__link",
|
|
1329
|
+
href: signInPath,
|
|
1330
|
+
children: labels.backToSignIn
|
|
1331
|
+
})] }),
|
|
1332
|
+
children: labels.invalidSubheading
|
|
1333
|
+
})
|
|
1334
|
+
});
|
|
1335
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1336
|
+
className: rootClass,
|
|
1337
|
+
"data-polyx-reset": step,
|
|
1338
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1339
|
+
className: "polyx-signin__card",
|
|
1340
|
+
children: [
|
|
1341
|
+
header(labels.heading, labels.subheading),
|
|
1342
|
+
/* @__PURE__ */ jsxs("form", {
|
|
1343
|
+
className: "polyx-signin__form",
|
|
1344
|
+
onSubmit,
|
|
1345
|
+
children: [
|
|
1346
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1347
|
+
className: "polyx-signin__field",
|
|
1348
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1349
|
+
className: "polyx-signin__label",
|
|
1350
|
+
children: labels.newPasswordLabel
|
|
1351
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1352
|
+
className: "polyx-signin__input",
|
|
1353
|
+
type: "password",
|
|
1354
|
+
name: "newPassword",
|
|
1355
|
+
autoComplete: "new-password",
|
|
1356
|
+
placeholder: labels.passwordPlaceholder,
|
|
1357
|
+
required: true,
|
|
1358
|
+
minLength,
|
|
1359
|
+
value: newPassword,
|
|
1360
|
+
disabled: submitting,
|
|
1361
|
+
onChange: (event) => setNewPassword(event.target.value)
|
|
1362
|
+
})]
|
|
1363
|
+
}),
|
|
1364
|
+
/* @__PURE__ */ jsxs("label", {
|
|
1365
|
+
className: "polyx-signin__field",
|
|
1366
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1367
|
+
className: "polyx-signin__label",
|
|
1368
|
+
children: labels.confirmPasswordLabel
|
|
1369
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
1370
|
+
className: "polyx-signin__input",
|
|
1371
|
+
type: "password",
|
|
1372
|
+
name: "confirmPassword",
|
|
1373
|
+
autoComplete: "new-password",
|
|
1374
|
+
placeholder: labels.passwordPlaceholder,
|
|
1375
|
+
required: true,
|
|
1376
|
+
minLength,
|
|
1377
|
+
value: confirmPassword,
|
|
1378
|
+
disabled: submitting,
|
|
1379
|
+
onChange: (event) => setConfirmPassword(event.target.value)
|
|
1380
|
+
})]
|
|
1381
|
+
}),
|
|
1382
|
+
error ? /* @__PURE__ */ jsx("p", {
|
|
1383
|
+
role: "alert",
|
|
1384
|
+
className: "polyx-signin__error",
|
|
1385
|
+
children: error
|
|
1386
|
+
}) : null,
|
|
1387
|
+
/* @__PURE__ */ jsx("button", {
|
|
1388
|
+
type: "submit",
|
|
1389
|
+
className: "polyx-signin__submit",
|
|
1390
|
+
disabled: submitting,
|
|
1391
|
+
children: submitting ? labels.submitting : labels.submit
|
|
1392
|
+
})
|
|
1393
|
+
]
|
|
1394
|
+
}),
|
|
1395
|
+
footer
|
|
1396
|
+
]
|
|
1397
|
+
})
|
|
357
1398
|
});
|
|
358
1399
|
}
|
|
359
1400
|
//#endregion
|
|
@@ -367,6 +1408,325 @@ function AuthCallback({ children = null }) {
|
|
|
367
1408
|
return children;
|
|
368
1409
|
}
|
|
369
1410
|
//#endregion
|
|
1411
|
+
//#region src/components/user-avatar.tsx
|
|
1412
|
+
/**
|
|
1413
|
+
* Up to two initials from a display name. Handles the shapes the platform actually
|
|
1414
|
+
* produces — "Ali Ikram", a bare username, an email address.
|
|
1415
|
+
*/
|
|
1416
|
+
function initialsFrom(name) {
|
|
1417
|
+
const source = (name ?? "").trim();
|
|
1418
|
+
if (source.length === 0) return "?";
|
|
1419
|
+
const words = (source.includes("@") ? source.split("@")[0] : source).split(/[\s._-]+/).filter(Boolean);
|
|
1420
|
+
if (words.length === 0) return "?";
|
|
1421
|
+
return (words.length === 1 ? [words[0][0]] : [words[0][0], words[words.length - 1][0]]).join("").toUpperCase();
|
|
1422
|
+
}
|
|
1423
|
+
/**
|
|
1424
|
+
* The user's picture, or their initials. A missing avatar is the ordinary case rather than
|
|
1425
|
+
* an error, so initials are a first-class state, not a broken image.
|
|
1426
|
+
*/
|
|
1427
|
+
function UserAvatar({ src, name, size = 32, className }) {
|
|
1428
|
+
const rootClass = ["polyx-userbutton__avatar", className].filter(Boolean).join(" ");
|
|
1429
|
+
const style = {
|
|
1430
|
+
width: `${size}px`,
|
|
1431
|
+
height: `${size}px`
|
|
1432
|
+
};
|
|
1433
|
+
if (src) return /* @__PURE__ */ jsx("img", {
|
|
1434
|
+
className: rootClass,
|
|
1435
|
+
style,
|
|
1436
|
+
src,
|
|
1437
|
+
alt: name ?? ""
|
|
1438
|
+
});
|
|
1439
|
+
return /* @__PURE__ */ jsx("span", {
|
|
1440
|
+
className: rootClass,
|
|
1441
|
+
style,
|
|
1442
|
+
"aria-hidden": "true",
|
|
1443
|
+
children: initialsFrom(name)
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
//#endregion
|
|
1447
|
+
//#region src/components/user-button.tsx
|
|
1448
|
+
const DEFAULT_LABELS = {
|
|
1449
|
+
trigger: "Account menu",
|
|
1450
|
+
manageAccount: "Manage account",
|
|
1451
|
+
signOut: "Sign out",
|
|
1452
|
+
signOutEverywhere: "Sign out everywhere"
|
|
1453
|
+
};
|
|
1454
|
+
const BUILT_INS = [
|
|
1455
|
+
"manageAccount",
|
|
1456
|
+
"signOut",
|
|
1457
|
+
"signOutEverywhere"
|
|
1458
|
+
];
|
|
1459
|
+
function isBuiltIn(label) {
|
|
1460
|
+
return BUILT_INS.includes(label);
|
|
1461
|
+
}
|
|
1462
|
+
/**
|
|
1463
|
+
* Declarative markers. `<UserButton/>` reads their props off the element tree and renders the
|
|
1464
|
+
* menu itself, so these never render anything — which is also what keeps the menu's markup
|
|
1465
|
+
* ours and a consumer's styles out of it (FR-BRAND-1). Their public prop types come from the
|
|
1466
|
+
* signatures declared on the exported object below.
|
|
1467
|
+
*/
|
|
1468
|
+
function MenuItems() {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
function Action() {
|
|
1472
|
+
return null;
|
|
1473
|
+
}
|
|
1474
|
+
function Link() {
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
/**
|
|
1478
|
+
* Read the declarative children into a flat item list. A custom entry naming a built-in
|
|
1479
|
+
* *moves* it; unnamed built-ins keep their default order at the end. This is the whole reason
|
|
1480
|
+
* the API is declarative rather than a config array: it composes without letting a consumer
|
|
1481
|
+
* inject arbitrary markup or styles into the menu (FR-BRAND-1).
|
|
1482
|
+
*/
|
|
1483
|
+
function resolveItems(children, builtIns) {
|
|
1484
|
+
const custom = [];
|
|
1485
|
+
const placed = /* @__PURE__ */ new Set();
|
|
1486
|
+
Children.forEach(children, (child) => {
|
|
1487
|
+
if (!isValidElement(child) || child.type !== MenuItems) return;
|
|
1488
|
+
Children.forEach(child.props.children, (item, index) => {
|
|
1489
|
+
if (!isValidElement(item)) return;
|
|
1490
|
+
if (item.type === Action) {
|
|
1491
|
+
const props = item.props;
|
|
1492
|
+
if (isBuiltIn(props.label)) {
|
|
1493
|
+
const builtIn = builtIns.get(props.label);
|
|
1494
|
+
if (builtIn) {
|
|
1495
|
+
custom.push(builtIn);
|
|
1496
|
+
placed.add(props.label);
|
|
1497
|
+
}
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
custom.push({
|
|
1501
|
+
key: `action-${index}`,
|
|
1502
|
+
label: props.label,
|
|
1503
|
+
labelIcon: props.labelIcon,
|
|
1504
|
+
onClick: props.onClick
|
|
1505
|
+
});
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
if (item.type === Link) {
|
|
1509
|
+
const props = item.props;
|
|
1510
|
+
custom.push({
|
|
1511
|
+
key: `link-${index}`,
|
|
1512
|
+
label: props.label,
|
|
1513
|
+
labelIcon: props.labelIcon,
|
|
1514
|
+
href: props.href
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
const remaining = [...builtIns.entries()].filter(([name]) => !placed.has(name)).map(([, item]) => item);
|
|
1520
|
+
return [...custom, ...remaining];
|
|
1521
|
+
}
|
|
1522
|
+
function UserButtonRoot(props) {
|
|
1523
|
+
const labels = {
|
|
1524
|
+
...DEFAULT_LABELS,
|
|
1525
|
+
...props.labels
|
|
1526
|
+
};
|
|
1527
|
+
const { isLoaded, isSignedIn, signOut } = useAuth();
|
|
1528
|
+
const user = useUser();
|
|
1529
|
+
const [open, setOpen] = useState(props.defaultOpen ?? false);
|
|
1530
|
+
const [focused, setFocused] = useState(0);
|
|
1531
|
+
const triggerRef = useRef(null);
|
|
1532
|
+
const itemRefs = useRef([]);
|
|
1533
|
+
const menuId = useId();
|
|
1534
|
+
const close = useCallback((returnFocus) => {
|
|
1535
|
+
setOpen(false);
|
|
1536
|
+
if (returnFocus) triggerRef.current?.focus();
|
|
1537
|
+
}, []);
|
|
1538
|
+
useEffect(() => {
|
|
1539
|
+
if (!open) return;
|
|
1540
|
+
const onKeyDown = (event) => {
|
|
1541
|
+
if (event.key === "Escape") close(true);
|
|
1542
|
+
};
|
|
1543
|
+
const onPointerDown = (event) => {
|
|
1544
|
+
const root = triggerRef.current?.closest(".polyx-userbutton");
|
|
1545
|
+
if (root && !root.contains(event.target)) close(false);
|
|
1546
|
+
};
|
|
1547
|
+
document.addEventListener("keydown", onKeyDown);
|
|
1548
|
+
document.addEventListener("mousedown", onPointerDown);
|
|
1549
|
+
return () => {
|
|
1550
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
1551
|
+
document.removeEventListener("mousedown", onPointerDown);
|
|
1552
|
+
};
|
|
1553
|
+
}, [open, close]);
|
|
1554
|
+
useEffect(() => {
|
|
1555
|
+
if (open) itemRefs.current[focused]?.focus();
|
|
1556
|
+
}, [open, focused]);
|
|
1557
|
+
if (!isLoaded) return props.fallback ?? null;
|
|
1558
|
+
if (!isSignedIn || !user) return null;
|
|
1559
|
+
const name = user.displayName ?? user.email ?? "";
|
|
1560
|
+
const builtIns = /* @__PURE__ */ new Map();
|
|
1561
|
+
if (props.manageAccountUrl) builtIns.set("manageAccount", {
|
|
1562
|
+
key: "manageAccount",
|
|
1563
|
+
label: labels.manageAccount,
|
|
1564
|
+
labelIcon: /* @__PURE__ */ jsx(GearIcon, {}),
|
|
1565
|
+
href: props.manageAccountUrl
|
|
1566
|
+
});
|
|
1567
|
+
builtIns.set("signOut", {
|
|
1568
|
+
key: "signOut",
|
|
1569
|
+
label: labels.signOut,
|
|
1570
|
+
labelIcon: /* @__PURE__ */ jsx(ExitIcon, {}),
|
|
1571
|
+
onClick: () => void signOut({ redirectTo: props.afterSignOutUrl })
|
|
1572
|
+
});
|
|
1573
|
+
if (props.showSignOutEverywhere) builtIns.set("signOutEverywhere", {
|
|
1574
|
+
key: "signOutEverywhere",
|
|
1575
|
+
label: labels.signOutEverywhere,
|
|
1576
|
+
labelIcon: /* @__PURE__ */ jsx(ExitIcon, {}),
|
|
1577
|
+
onClick: () => void signOut({
|
|
1578
|
+
scope: "everywhere",
|
|
1579
|
+
redirectTo: props.afterSignOutUrl
|
|
1580
|
+
})
|
|
1581
|
+
});
|
|
1582
|
+
const items = resolveItems(props.children, builtIns);
|
|
1583
|
+
const onMenuKeyDown = (event) => {
|
|
1584
|
+
if (event.key === "ArrowDown") {
|
|
1585
|
+
event.preventDefault();
|
|
1586
|
+
setFocused((current) => (current + 1) % items.length);
|
|
1587
|
+
} else if (event.key === "ArrowUp") {
|
|
1588
|
+
event.preventDefault();
|
|
1589
|
+
setFocused((current) => (current - 1 + items.length) % items.length);
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1593
|
+
className: [
|
|
1594
|
+
"polyx-signin",
|
|
1595
|
+
"polyx-userbutton",
|
|
1596
|
+
props.className
|
|
1597
|
+
].filter(Boolean).join(" "),
|
|
1598
|
+
"data-polyx-userbutton": open ? "open" : "closed",
|
|
1599
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
1600
|
+
ref: triggerRef,
|
|
1601
|
+
type: "button",
|
|
1602
|
+
className: "polyx-userbutton__trigger",
|
|
1603
|
+
"aria-haspopup": "menu",
|
|
1604
|
+
"aria-expanded": open,
|
|
1605
|
+
"aria-controls": open ? menuId : void 0,
|
|
1606
|
+
"aria-label": labels.trigger,
|
|
1607
|
+
onClick: () => {
|
|
1608
|
+
setFocused(0);
|
|
1609
|
+
setOpen((current) => !current);
|
|
1610
|
+
},
|
|
1611
|
+
children: [/* @__PURE__ */ jsx(UserAvatar, {
|
|
1612
|
+
src: user.avatarUrl,
|
|
1613
|
+
name,
|
|
1614
|
+
size: props.avatarSize
|
|
1615
|
+
}), props.showName ? /* @__PURE__ */ jsx("span", {
|
|
1616
|
+
className: "polyx-userbutton__trigger-name",
|
|
1617
|
+
children: name
|
|
1618
|
+
}) : null]
|
|
1619
|
+
}), open ? /* @__PURE__ */ jsxs("div", {
|
|
1620
|
+
id: menuId,
|
|
1621
|
+
role: "menu",
|
|
1622
|
+
className: "polyx-userbutton__menu",
|
|
1623
|
+
onKeyDown: onMenuKeyDown,
|
|
1624
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1625
|
+
className: "polyx-userbutton__identity",
|
|
1626
|
+
children: [/* @__PURE__ */ jsx(UserAvatar, {
|
|
1627
|
+
src: user.avatarUrl,
|
|
1628
|
+
name,
|
|
1629
|
+
size: 36
|
|
1630
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1631
|
+
className: "polyx-userbutton__identity-text",
|
|
1632
|
+
children: [user.displayName ? /* @__PURE__ */ jsx("span", {
|
|
1633
|
+
className: "polyx-userbutton__name",
|
|
1634
|
+
children: user.displayName
|
|
1635
|
+
}) : null, user.email ? /* @__PURE__ */ jsx("span", {
|
|
1636
|
+
className: "polyx-userbutton__email",
|
|
1637
|
+
children: user.email
|
|
1638
|
+
}) : null]
|
|
1639
|
+
})]
|
|
1640
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
1641
|
+
className: "polyx-userbutton__items",
|
|
1642
|
+
children: items.map((item, index) => /* @__PURE__ */ jsx("li", { children: item.href ? /* @__PURE__ */ jsxs("a", {
|
|
1643
|
+
ref: (node) => {
|
|
1644
|
+
itemRefs.current[index] = node;
|
|
1645
|
+
},
|
|
1646
|
+
role: "menuitem",
|
|
1647
|
+
tabIndex: index === focused ? 0 : -1,
|
|
1648
|
+
className: "polyx-userbutton__item",
|
|
1649
|
+
href: item.href,
|
|
1650
|
+
onClick: () => close(false),
|
|
1651
|
+
children: [item.labelIcon, item.label]
|
|
1652
|
+
}) : /* @__PURE__ */ jsxs("button", {
|
|
1653
|
+
ref: (node) => {
|
|
1654
|
+
itemRefs.current[index] = node;
|
|
1655
|
+
},
|
|
1656
|
+
role: "menuitem",
|
|
1657
|
+
tabIndex: index === focused ? 0 : -1,
|
|
1658
|
+
type: "button",
|
|
1659
|
+
className: "polyx-userbutton__item",
|
|
1660
|
+
onClick: () => {
|
|
1661
|
+
close(false);
|
|
1662
|
+
item.onClick?.();
|
|
1663
|
+
},
|
|
1664
|
+
children: [item.labelIcon, item.label]
|
|
1665
|
+
}) }, item.key))
|
|
1666
|
+
})]
|
|
1667
|
+
}) : null]
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
function GearIcon() {
|
|
1671
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
1672
|
+
className: "polyx-userbutton__icon",
|
|
1673
|
+
viewBox: "0 0 16 16",
|
|
1674
|
+
fill: "none",
|
|
1675
|
+
"aria-hidden": "true",
|
|
1676
|
+
focusable: "false",
|
|
1677
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
1678
|
+
cx: "8",
|
|
1679
|
+
cy: "8",
|
|
1680
|
+
r: "2.25",
|
|
1681
|
+
stroke: "currentColor",
|
|
1682
|
+
strokeWidth: "1.3"
|
|
1683
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
1684
|
+
d: "M8 1.75v1.5M8 12.75v1.5M14.25 8h-1.5M3.25 8h-1.5M12.42 3.58l-1.06 1.06M4.64 11.36l-1.06 1.06M12.42 12.42l-1.06-1.06M4.64 4.64L3.58 3.58",
|
|
1685
|
+
stroke: "currentColor",
|
|
1686
|
+
strokeWidth: "1.3",
|
|
1687
|
+
strokeLinecap: "round"
|
|
1688
|
+
})]
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
function ExitIcon() {
|
|
1692
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
1693
|
+
className: "polyx-userbutton__icon",
|
|
1694
|
+
viewBox: "0 0 16 16",
|
|
1695
|
+
fill: "none",
|
|
1696
|
+
"aria-hidden": "true",
|
|
1697
|
+
focusable: "false",
|
|
1698
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1699
|
+
d: "M6 2.75H3.75a1 1 0 0 0-1 1v8.5a1 1 0 0 0 1 1H6M10.5 10.5 13 8l-2.5-2.5M13 8H6",
|
|
1700
|
+
stroke: "currentColor",
|
|
1701
|
+
strokeWidth: "1.3",
|
|
1702
|
+
strokeLinecap: "round",
|
|
1703
|
+
strokeLinejoin: "round"
|
|
1704
|
+
})
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* The signed-in user surface: avatar trigger + a menu with their identity and sign-out
|
|
1709
|
+
* (FR-SIGNIN-6). Reads only the hooks, so it behaves identically under either custody model
|
|
1710
|
+
* — an httpOnly BFF session or an in-memory SPA one — and needs no provider in a
|
|
1711
|
+
* `@poly-x/next` app.
|
|
1712
|
+
*
|
|
1713
|
+
* Custom entries compose declaratively:
|
|
1714
|
+
*
|
|
1715
|
+
* ```tsx
|
|
1716
|
+
* <UserButton manageAccountUrl="/account">
|
|
1717
|
+
* <UserButton.MenuItems>
|
|
1718
|
+
* <UserButton.Action label="signOut" /> // reposition a built-in
|
|
1719
|
+
* <UserButton.Link label="Docs" href="/docs" />
|
|
1720
|
+
* </UserButton.MenuItems>
|
|
1721
|
+
* </UserButton>
|
|
1722
|
+
* ```
|
|
1723
|
+
*/
|
|
1724
|
+
const UserButton = Object.assign(UserButtonRoot, {
|
|
1725
|
+
MenuItems,
|
|
1726
|
+
Action,
|
|
1727
|
+
Link
|
|
1728
|
+
});
|
|
1729
|
+
//#endregion
|
|
370
1730
|
//#region src/index.ts
|
|
371
1731
|
/**
|
|
372
1732
|
* @poly-x/react — PolyX SDK for React SPAs. Sign-in UI + orchestration land in
|
|
@@ -375,4 +1735,4 @@ function AuthCallback({ children = null }) {
|
|
|
375
1735
|
const PACKAGE_NAME = "@poly-x/react";
|
|
376
1736
|
const version = "0.0.0";
|
|
377
1737
|
//#endregion
|
|
378
|
-
export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, SessionStoragePkceStore, SignIn, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
|
|
1738
|
+
export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, UserAvatar, UserButton, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
|