@poly-x/react 0.1.0-alpha.3 → 0.1.0-alpha.4
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 +168 -30
- package/dist/index.d.cts +20 -1
- package/dist/index.d.mts +20 -1
- package/dist/index.mjs +169 -31
- package/dist/styles.css +116 -11
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -355,6 +355,7 @@ function Protected({ children, fallback = null, loading = null }) {
|
|
|
355
355
|
const DEFAULT_LABELS = {
|
|
356
356
|
heading: "Sign in",
|
|
357
357
|
subheading: "Enter your credentials to continue.",
|
|
358
|
+
tagline: "",
|
|
358
359
|
emailLabel: "Email",
|
|
359
360
|
emailPlaceholder: "you@company.com",
|
|
360
361
|
passwordLabel: "Password",
|
|
@@ -363,11 +364,21 @@ const DEFAULT_LABELS = {
|
|
|
363
364
|
submitting: "Signing in…",
|
|
364
365
|
chooseWorkspace: "Choose a workspace",
|
|
365
366
|
chooseWorkspaceSubheading: "You have access to more than one workspace.",
|
|
367
|
+
securedBy: "Secured by PolyX",
|
|
368
|
+
setPasswordHeading: "Choose a new password",
|
|
369
|
+
setPasswordSubheading: "Your temporary password must be replaced before you continue.",
|
|
370
|
+
newPasswordLabel: "New password",
|
|
371
|
+
confirmPasswordLabel: "Confirm new password",
|
|
372
|
+
setPasswordSubmit: "Set password and sign in",
|
|
373
|
+
passwordMismatch: "Those passwords don't match.",
|
|
374
|
+
weakPassword: "Password must be at least 6 characters.",
|
|
366
375
|
invalidCredentials: "Invalid email or password.",
|
|
367
376
|
noAccess: "You don't have access to this application.",
|
|
368
377
|
passwordChangeRequired: "You must reset your password before you can sign in.",
|
|
369
378
|
genericError: "Something went wrong. Please try again."
|
|
370
379
|
};
|
|
380
|
+
/** Mirrors poly-auth's own floor (Joi: min 6). */
|
|
381
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
371
382
|
/**
|
|
372
383
|
* The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
|
|
373
384
|
* consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
|
|
@@ -387,10 +398,18 @@ function SignIn(props) {
|
|
|
387
398
|
};
|
|
388
399
|
const [email, setEmail] = (0, react.useState)("");
|
|
389
400
|
const [password, setPassword] = (0, react.useState)("");
|
|
401
|
+
const [newPassword, setNewPassword] = (0, react.useState)("");
|
|
402
|
+
const [confirmPassword, setConfirmPassword] = (0, react.useState)("");
|
|
403
|
+
const [mustSetPassword, setMustSetPassword] = (0, react.useState)(false);
|
|
390
404
|
const [tenants, setTenants] = (0, react.useState)(null);
|
|
391
405
|
const [submitting, setSubmitting] = (0, react.useState)(false);
|
|
392
406
|
const [error, setError] = (0, react.useState)(null);
|
|
393
|
-
|
|
407
|
+
/**
|
|
408
|
+
* One round-trip to the BFF. `tenantId` picks a workspace; `passwords` completes
|
|
409
|
+
* the temp-password flow (the BFF re-verifies the temporary password, sets the
|
|
410
|
+
* new one, and signs the user straight in).
|
|
411
|
+
*/
|
|
412
|
+
async function authenticate(options = {}) {
|
|
394
413
|
setSubmitting(true);
|
|
395
414
|
setError(null);
|
|
396
415
|
try {
|
|
@@ -401,7 +420,8 @@ function SignIn(props) {
|
|
|
401
420
|
email,
|
|
402
421
|
password,
|
|
403
422
|
organizationCode: props.organizationCode,
|
|
404
|
-
tenantId
|
|
423
|
+
tenantId: options.tenantId,
|
|
424
|
+
...options.passwords
|
|
405
425
|
})
|
|
406
426
|
})).json().catch(() => ({}));
|
|
407
427
|
switch (body.status) {
|
|
@@ -411,16 +431,22 @@ function SignIn(props) {
|
|
|
411
431
|
case "select_tenant":
|
|
412
432
|
setTenants(body.tenants ?? []);
|
|
413
433
|
return;
|
|
434
|
+
case "password_change_required":
|
|
435
|
+
setMustSetPassword(true);
|
|
436
|
+
if (body.userId) props.onPasswordChangeRequired?.(body.userId);
|
|
437
|
+
return;
|
|
438
|
+
case "password_mismatch":
|
|
439
|
+
setError(labels.passwordMismatch);
|
|
440
|
+
return;
|
|
441
|
+
case "weak_password":
|
|
442
|
+
setError(labels.weakPassword);
|
|
443
|
+
return;
|
|
414
444
|
case "invalid_credentials":
|
|
415
445
|
setError(labels.invalidCredentials);
|
|
416
446
|
return;
|
|
417
447
|
case "no_access":
|
|
418
448
|
setError(labels.noAccess);
|
|
419
449
|
return;
|
|
420
|
-
case "password_change_required":
|
|
421
|
-
setError(labels.passwordChangeRequired);
|
|
422
|
-
if (body.userId) props.onPasswordChangeRequired?.(body.userId);
|
|
423
|
-
return;
|
|
424
450
|
default:
|
|
425
451
|
setError(labels.genericError);
|
|
426
452
|
return;
|
|
@@ -435,26 +461,55 @@ function SignIn(props) {
|
|
|
435
461
|
event.preventDefault();
|
|
436
462
|
authenticate();
|
|
437
463
|
}
|
|
464
|
+
function onSetPasswordSubmit(event) {
|
|
465
|
+
event.preventDefault();
|
|
466
|
+
if (newPassword !== confirmPassword) {
|
|
467
|
+
setError(labels.passwordMismatch);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
authenticate({ passwords: {
|
|
471
|
+
newPassword,
|
|
472
|
+
confirmPassword
|
|
473
|
+
} });
|
|
474
|
+
}
|
|
475
|
+
const picking = tenants !== null;
|
|
476
|
+
const hasPanel = variant === "page" && Boolean(props.panel || props.logo || labels.tagline);
|
|
477
|
+
const showHeaderLogo = Boolean(props.logo) && !hasPanel;
|
|
438
478
|
const rootClass = [
|
|
439
479
|
"polyx-signin",
|
|
440
480
|
`polyx-signin--${variant}`,
|
|
481
|
+
hasPanel && "polyx-signin--split",
|
|
441
482
|
props.className
|
|
442
483
|
].filter(Boolean).join(" ");
|
|
443
|
-
const
|
|
484
|
+
const step = picking ? "select_tenant" : mustSetPassword ? "set_password" : "credentials";
|
|
485
|
+
const HEADINGS = {
|
|
486
|
+
credentials: {
|
|
487
|
+
heading: labels.heading,
|
|
488
|
+
subheading: labels.subheading
|
|
489
|
+
},
|
|
490
|
+
select_tenant: {
|
|
491
|
+
heading: labels.chooseWorkspace,
|
|
492
|
+
subheading: labels.chooseWorkspaceSubheading
|
|
493
|
+
},
|
|
494
|
+
set_password: {
|
|
495
|
+
heading: labels.setPasswordHeading,
|
|
496
|
+
subheading: labels.setPasswordSubheading
|
|
497
|
+
}
|
|
498
|
+
};
|
|
444
499
|
const header = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
445
500
|
className: "polyx-signin__header",
|
|
446
501
|
children: [
|
|
447
|
-
|
|
502
|
+
showHeaderLogo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
448
503
|
className: "polyx-signin__logo",
|
|
449
504
|
children: props.logo
|
|
450
505
|
}) : null,
|
|
451
506
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
452
507
|
className: "polyx-signin__heading",
|
|
453
|
-
children:
|
|
508
|
+
children: HEADINGS[step].heading
|
|
454
509
|
}),
|
|
455
510
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
456
511
|
className: "polyx-signin__subheading",
|
|
457
|
-
children:
|
|
512
|
+
children: HEADINGS[step].subheading
|
|
458
513
|
})
|
|
459
514
|
]
|
|
460
515
|
});
|
|
@@ -463,24 +518,84 @@ function SignIn(props) {
|
|
|
463
518
|
className: "polyx-signin__error",
|
|
464
519
|
children: error
|
|
465
520
|
}) : null;
|
|
466
|
-
|
|
467
|
-
className:
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
521
|
+
const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
522
|
+
className: "polyx-signin__footer",
|
|
523
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
524
|
+
className: "polyx-signin__shield",
|
|
525
|
+
viewBox: "0 0 16 16",
|
|
526
|
+
fill: "currentColor",
|
|
527
|
+
"aria-hidden": "true",
|
|
528
|
+
focusable: "false",
|
|
529
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.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" })
|
|
530
|
+
}), labels.securedBy]
|
|
531
|
+
});
|
|
532
|
+
const tenantPicker = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
533
|
+
className: "polyx-signin__form",
|
|
534
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
535
|
+
className: "polyx-signin__tenants",
|
|
536
|
+
children: (tenants ?? []).map((tenant) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
537
|
+
type: "button",
|
|
538
|
+
className: "polyx-signin__tenant",
|
|
539
|
+
disabled: submitting,
|
|
540
|
+
onClick: () => void authenticate({ tenantId: tenant.tenantId }),
|
|
541
|
+
children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
|
|
542
|
+
}) }, tenant.tenantId))
|
|
543
|
+
}), errorNode]
|
|
544
|
+
});
|
|
545
|
+
const setPasswordForm = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
546
|
+
className: "polyx-signin__form",
|
|
547
|
+
onSubmit: onSetPasswordSubmit,
|
|
548
|
+
children: [
|
|
549
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
550
|
+
className: "polyx-signin__field",
|
|
551
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
552
|
+
className: "polyx-signin__label",
|
|
553
|
+
children: labels.newPasswordLabel
|
|
554
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
555
|
+
className: "polyx-signin__input",
|
|
556
|
+
type: "password",
|
|
557
|
+
name: "newPassword",
|
|
558
|
+
autoComplete: "new-password",
|
|
559
|
+
placeholder: labels.passwordPlaceholder,
|
|
560
|
+
required: true,
|
|
561
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
562
|
+
value: newPassword,
|
|
563
|
+
disabled: submitting,
|
|
564
|
+
onChange: (event) => setNewPassword(event.target.value)
|
|
565
|
+
})]
|
|
566
|
+
}),
|
|
567
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
568
|
+
className: "polyx-signin__field",
|
|
569
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
570
|
+
className: "polyx-signin__label",
|
|
571
|
+
children: labels.confirmPasswordLabel
|
|
572
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
573
|
+
className: "polyx-signin__input",
|
|
574
|
+
type: "password",
|
|
575
|
+
name: "confirmPassword",
|
|
576
|
+
autoComplete: "new-password",
|
|
577
|
+
placeholder: labels.passwordPlaceholder,
|
|
578
|
+
required: true,
|
|
579
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
580
|
+
value: confirmPassword,
|
|
581
|
+
disabled: submitting,
|
|
582
|
+
onChange: (event) => setConfirmPassword(event.target.value)
|
|
583
|
+
})]
|
|
584
|
+
}),
|
|
585
|
+
errorNode,
|
|
586
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
587
|
+
type: "submit",
|
|
588
|
+
className: "polyx-signin__submit",
|
|
589
|
+
disabled: submitting,
|
|
590
|
+
children: submitting ? labels.submitting : labels.setPasswordSubmit
|
|
591
|
+
})
|
|
592
|
+
]
|
|
593
|
+
});
|
|
594
|
+
const card = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
595
|
+
className: "polyx-signin__card",
|
|
596
|
+
children: [
|
|
597
|
+
header,
|
|
598
|
+
step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
484
599
|
className: "polyx-signin__form",
|
|
485
600
|
onSubmit,
|
|
486
601
|
children: [
|
|
@@ -526,8 +641,31 @@ function SignIn(props) {
|
|
|
526
641
|
children: submitting ? labels.submitting : labels.submit
|
|
527
642
|
})
|
|
528
643
|
]
|
|
529
|
-
})
|
|
530
|
-
|
|
644
|
+
}),
|
|
645
|
+
footer
|
|
646
|
+
]
|
|
647
|
+
});
|
|
648
|
+
if (variant === "page") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
649
|
+
className: rootClass,
|
|
650
|
+
"data-polyx-signin": step,
|
|
651
|
+
children: [hasPanel ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
652
|
+
className: "polyx-signin__panel",
|
|
653
|
+
children: props.panel ?? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
654
|
+
className: "polyx-signin__panel-logo",
|
|
655
|
+
children: props.logo
|
|
656
|
+
}) : null, labels.tagline ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
657
|
+
className: "polyx-signin__tagline",
|
|
658
|
+
children: labels.tagline
|
|
659
|
+
}) : null] })
|
|
660
|
+
}) : null, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
661
|
+
className: "polyx-signin__main",
|
|
662
|
+
children: card
|
|
663
|
+
})]
|
|
664
|
+
});
|
|
665
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
666
|
+
className: rootClass,
|
|
667
|
+
"data-polyx-signin": step,
|
|
668
|
+
children: card
|
|
531
669
|
});
|
|
532
670
|
}
|
|
533
671
|
//#endregion
|
package/dist/index.d.cts
CHANGED
|
@@ -195,6 +195,8 @@ declare function Protected({
|
|
|
195
195
|
interface SignInLabels {
|
|
196
196
|
heading: string;
|
|
197
197
|
subheading: string;
|
|
198
|
+
/** Headline on the `page` variant's brand panel. Empty renders a clean brand panel. */
|
|
199
|
+
tagline: string;
|
|
198
200
|
emailLabel: string;
|
|
199
201
|
emailPlaceholder: string;
|
|
200
202
|
passwordLabel: string;
|
|
@@ -203,6 +205,14 @@ interface SignInLabels {
|
|
|
203
205
|
submitting: string;
|
|
204
206
|
chooseWorkspace: string;
|
|
205
207
|
chooseWorkspaceSubheading: string;
|
|
208
|
+
securedBy: string;
|
|
209
|
+
setPasswordHeading: string;
|
|
210
|
+
setPasswordSubheading: string;
|
|
211
|
+
newPasswordLabel: string;
|
|
212
|
+
confirmPasswordLabel: string;
|
|
213
|
+
setPasswordSubmit: string;
|
|
214
|
+
passwordMismatch: string;
|
|
215
|
+
weakPassword: string;
|
|
206
216
|
invalidCredentials: string;
|
|
207
217
|
noAccess: string;
|
|
208
218
|
passwordChangeRequired: string;
|
|
@@ -210,12 +220,19 @@ interface SignInLabels {
|
|
|
210
220
|
}
|
|
211
221
|
/**
|
|
212
222
|
* `card` renders just the card — drop it into your own layout.
|
|
213
|
-
* `page` owns the viewport — a
|
|
223
|
+
* `page` owns the viewport — a split layout with a brand panel beside the form
|
|
224
|
+
* (the panel collapses away under 64rem, leaving the form on its own).
|
|
214
225
|
*/
|
|
215
226
|
type SignInVariant = "card" | "page";
|
|
216
227
|
interface SignInProps {
|
|
217
228
|
/** Layout preset. Default `card`. */
|
|
218
229
|
variant?: SignInVariant;
|
|
230
|
+
/**
|
|
231
|
+
* Content for the `page` variant's brand panel. Defaults to your `logo` plus
|
|
232
|
+
* `labels.tagline`; pass your own node to take the panel over completely.
|
|
233
|
+
* Ignored by the `card` variant.
|
|
234
|
+
*/
|
|
235
|
+
panel?: ReactNode;
|
|
219
236
|
/** BFF endpoint backed by `createAuthHandlers().authenticate`. Default `/api/polyx/authenticate`. */
|
|
220
237
|
action?: string;
|
|
221
238
|
/** Where to send the browser after success. Overrides the server's default redirect. */
|
|
@@ -224,6 +241,8 @@ interface SignInProps {
|
|
|
224
241
|
organizationCode?: string;
|
|
225
242
|
/** Rendered above the heading — your wordmark or logo. */
|
|
226
243
|
logo?: ReactNode;
|
|
244
|
+
/** Show the "Secured by PolyX" attribution under the form. Default true. */
|
|
245
|
+
showSecuredBy?: boolean;
|
|
227
246
|
/** Appended to the root element's class list, for escape-hatch styling. */
|
|
228
247
|
className?: string;
|
|
229
248
|
labels?: Partial<SignInLabels>;
|
package/dist/index.d.mts
CHANGED
|
@@ -195,6 +195,8 @@ declare function Protected({
|
|
|
195
195
|
interface SignInLabels {
|
|
196
196
|
heading: string;
|
|
197
197
|
subheading: string;
|
|
198
|
+
/** Headline on the `page` variant's brand panel. Empty renders a clean brand panel. */
|
|
199
|
+
tagline: string;
|
|
198
200
|
emailLabel: string;
|
|
199
201
|
emailPlaceholder: string;
|
|
200
202
|
passwordLabel: string;
|
|
@@ -203,6 +205,14 @@ interface SignInLabels {
|
|
|
203
205
|
submitting: string;
|
|
204
206
|
chooseWorkspace: string;
|
|
205
207
|
chooseWorkspaceSubheading: string;
|
|
208
|
+
securedBy: string;
|
|
209
|
+
setPasswordHeading: string;
|
|
210
|
+
setPasswordSubheading: string;
|
|
211
|
+
newPasswordLabel: string;
|
|
212
|
+
confirmPasswordLabel: string;
|
|
213
|
+
setPasswordSubmit: string;
|
|
214
|
+
passwordMismatch: string;
|
|
215
|
+
weakPassword: string;
|
|
206
216
|
invalidCredentials: string;
|
|
207
217
|
noAccess: string;
|
|
208
218
|
passwordChangeRequired: string;
|
|
@@ -210,12 +220,19 @@ interface SignInLabels {
|
|
|
210
220
|
}
|
|
211
221
|
/**
|
|
212
222
|
* `card` renders just the card — drop it into your own layout.
|
|
213
|
-
* `page` owns the viewport — a
|
|
223
|
+
* `page` owns the viewport — a split layout with a brand panel beside the form
|
|
224
|
+
* (the panel collapses away under 64rem, leaving the form on its own).
|
|
214
225
|
*/
|
|
215
226
|
type SignInVariant = "card" | "page";
|
|
216
227
|
interface SignInProps {
|
|
217
228
|
/** Layout preset. Default `card`. */
|
|
218
229
|
variant?: SignInVariant;
|
|
230
|
+
/**
|
|
231
|
+
* Content for the `page` variant's brand panel. Defaults to your `logo` plus
|
|
232
|
+
* `labels.tagline`; pass your own node to take the panel over completely.
|
|
233
|
+
* Ignored by the `card` variant.
|
|
234
|
+
*/
|
|
235
|
+
panel?: ReactNode;
|
|
219
236
|
/** BFF endpoint backed by `createAuthHandlers().authenticate`. Default `/api/polyx/authenticate`. */
|
|
220
237
|
action?: string;
|
|
221
238
|
/** Where to send the browser after success. Overrides the server's default redirect. */
|
|
@@ -224,6 +241,8 @@ interface SignInProps {
|
|
|
224
241
|
organizationCode?: string;
|
|
225
242
|
/** Rendered above the heading — your wordmark or logo. */
|
|
226
243
|
logo?: ReactNode;
|
|
244
|
+
/** Show the "Secured by PolyX" attribution under the form. Default true. */
|
|
245
|
+
showSecuredBy?: boolean;
|
|
227
246
|
/** Appended to the root element's class list, for escape-hatch styling. */
|
|
228
247
|
className?: string;
|
|
229
248
|
labels?: Partial<SignInLabels>;
|
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
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/auth-channel.ts
|
|
6
6
|
var BroadcastAuthChannel = class {
|
|
7
7
|
supported;
|
|
@@ -354,6 +354,7 @@ function Protected({ children, fallback = null, loading = null }) {
|
|
|
354
354
|
const DEFAULT_LABELS = {
|
|
355
355
|
heading: "Sign in",
|
|
356
356
|
subheading: "Enter your credentials to continue.",
|
|
357
|
+
tagline: "",
|
|
357
358
|
emailLabel: "Email",
|
|
358
359
|
emailPlaceholder: "you@company.com",
|
|
359
360
|
passwordLabel: "Password",
|
|
@@ -362,11 +363,21 @@ const DEFAULT_LABELS = {
|
|
|
362
363
|
submitting: "Signing in…",
|
|
363
364
|
chooseWorkspace: "Choose a workspace",
|
|
364
365
|
chooseWorkspaceSubheading: "You have access to more than one workspace.",
|
|
366
|
+
securedBy: "Secured by PolyX",
|
|
367
|
+
setPasswordHeading: "Choose a new password",
|
|
368
|
+
setPasswordSubheading: "Your temporary password must be replaced before you continue.",
|
|
369
|
+
newPasswordLabel: "New password",
|
|
370
|
+
confirmPasswordLabel: "Confirm new password",
|
|
371
|
+
setPasswordSubmit: "Set password and sign in",
|
|
372
|
+
passwordMismatch: "Those passwords don't match.",
|
|
373
|
+
weakPassword: "Password must be at least 6 characters.",
|
|
365
374
|
invalidCredentials: "Invalid email or password.",
|
|
366
375
|
noAccess: "You don't have access to this application.",
|
|
367
376
|
passwordChangeRequired: "You must reset your password before you can sign in.",
|
|
368
377
|
genericError: "Something went wrong. Please try again."
|
|
369
378
|
};
|
|
379
|
+
/** Mirrors poly-auth's own floor (Joi: min 6). */
|
|
380
|
+
const MIN_PASSWORD_LENGTH = 6;
|
|
370
381
|
/**
|
|
371
382
|
* The embedded, Clerk-style sign-in form (D2b). Renders email/password inside the
|
|
372
383
|
* consuming app and POSTs to the SDK's BFF (`@poly-x/next` `authenticate`), which
|
|
@@ -386,10 +397,18 @@ function SignIn(props) {
|
|
|
386
397
|
};
|
|
387
398
|
const [email, setEmail] = useState("");
|
|
388
399
|
const [password, setPassword] = useState("");
|
|
400
|
+
const [newPassword, setNewPassword] = useState("");
|
|
401
|
+
const [confirmPassword, setConfirmPassword] = useState("");
|
|
402
|
+
const [mustSetPassword, setMustSetPassword] = useState(false);
|
|
389
403
|
const [tenants, setTenants] = useState(null);
|
|
390
404
|
const [submitting, setSubmitting] = useState(false);
|
|
391
405
|
const [error, setError] = useState(null);
|
|
392
|
-
|
|
406
|
+
/**
|
|
407
|
+
* One round-trip to the BFF. `tenantId` picks a workspace; `passwords` completes
|
|
408
|
+
* the temp-password flow (the BFF re-verifies the temporary password, sets the
|
|
409
|
+
* new one, and signs the user straight in).
|
|
410
|
+
*/
|
|
411
|
+
async function authenticate(options = {}) {
|
|
393
412
|
setSubmitting(true);
|
|
394
413
|
setError(null);
|
|
395
414
|
try {
|
|
@@ -400,7 +419,8 @@ function SignIn(props) {
|
|
|
400
419
|
email,
|
|
401
420
|
password,
|
|
402
421
|
organizationCode: props.organizationCode,
|
|
403
|
-
tenantId
|
|
422
|
+
tenantId: options.tenantId,
|
|
423
|
+
...options.passwords
|
|
404
424
|
})
|
|
405
425
|
})).json().catch(() => ({}));
|
|
406
426
|
switch (body.status) {
|
|
@@ -410,16 +430,22 @@ function SignIn(props) {
|
|
|
410
430
|
case "select_tenant":
|
|
411
431
|
setTenants(body.tenants ?? []);
|
|
412
432
|
return;
|
|
433
|
+
case "password_change_required":
|
|
434
|
+
setMustSetPassword(true);
|
|
435
|
+
if (body.userId) props.onPasswordChangeRequired?.(body.userId);
|
|
436
|
+
return;
|
|
437
|
+
case "password_mismatch":
|
|
438
|
+
setError(labels.passwordMismatch);
|
|
439
|
+
return;
|
|
440
|
+
case "weak_password":
|
|
441
|
+
setError(labels.weakPassword);
|
|
442
|
+
return;
|
|
413
443
|
case "invalid_credentials":
|
|
414
444
|
setError(labels.invalidCredentials);
|
|
415
445
|
return;
|
|
416
446
|
case "no_access":
|
|
417
447
|
setError(labels.noAccess);
|
|
418
448
|
return;
|
|
419
|
-
case "password_change_required":
|
|
420
|
-
setError(labels.passwordChangeRequired);
|
|
421
|
-
if (body.userId) props.onPasswordChangeRequired?.(body.userId);
|
|
422
|
-
return;
|
|
423
449
|
default:
|
|
424
450
|
setError(labels.genericError);
|
|
425
451
|
return;
|
|
@@ -434,26 +460,55 @@ function SignIn(props) {
|
|
|
434
460
|
event.preventDefault();
|
|
435
461
|
authenticate();
|
|
436
462
|
}
|
|
463
|
+
function onSetPasswordSubmit(event) {
|
|
464
|
+
event.preventDefault();
|
|
465
|
+
if (newPassword !== confirmPassword) {
|
|
466
|
+
setError(labels.passwordMismatch);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
authenticate({ passwords: {
|
|
470
|
+
newPassword,
|
|
471
|
+
confirmPassword
|
|
472
|
+
} });
|
|
473
|
+
}
|
|
474
|
+
const picking = tenants !== null;
|
|
475
|
+
const hasPanel = variant === "page" && Boolean(props.panel || props.logo || labels.tagline);
|
|
476
|
+
const showHeaderLogo = Boolean(props.logo) && !hasPanel;
|
|
437
477
|
const rootClass = [
|
|
438
478
|
"polyx-signin",
|
|
439
479
|
`polyx-signin--${variant}`,
|
|
480
|
+
hasPanel && "polyx-signin--split",
|
|
440
481
|
props.className
|
|
441
482
|
].filter(Boolean).join(" ");
|
|
442
|
-
const
|
|
483
|
+
const step = picking ? "select_tenant" : mustSetPassword ? "set_password" : "credentials";
|
|
484
|
+
const HEADINGS = {
|
|
485
|
+
credentials: {
|
|
486
|
+
heading: labels.heading,
|
|
487
|
+
subheading: labels.subheading
|
|
488
|
+
},
|
|
489
|
+
select_tenant: {
|
|
490
|
+
heading: labels.chooseWorkspace,
|
|
491
|
+
subheading: labels.chooseWorkspaceSubheading
|
|
492
|
+
},
|
|
493
|
+
set_password: {
|
|
494
|
+
heading: labels.setPasswordHeading,
|
|
495
|
+
subheading: labels.setPasswordSubheading
|
|
496
|
+
}
|
|
497
|
+
};
|
|
443
498
|
const header = /* @__PURE__ */ jsxs("div", {
|
|
444
499
|
className: "polyx-signin__header",
|
|
445
500
|
children: [
|
|
446
|
-
|
|
501
|
+
showHeaderLogo ? /* @__PURE__ */ jsx("div", {
|
|
447
502
|
className: "polyx-signin__logo",
|
|
448
503
|
children: props.logo
|
|
449
504
|
}) : null,
|
|
450
505
|
/* @__PURE__ */ jsx("h1", {
|
|
451
506
|
className: "polyx-signin__heading",
|
|
452
|
-
children:
|
|
507
|
+
children: HEADINGS[step].heading
|
|
453
508
|
}),
|
|
454
509
|
/* @__PURE__ */ jsx("p", {
|
|
455
510
|
className: "polyx-signin__subheading",
|
|
456
|
-
children:
|
|
511
|
+
children: HEADINGS[step].subheading
|
|
457
512
|
})
|
|
458
513
|
]
|
|
459
514
|
});
|
|
@@ -462,24 +517,84 @@ function SignIn(props) {
|
|
|
462
517
|
className: "polyx-signin__error",
|
|
463
518
|
children: error
|
|
464
519
|
}) : null;
|
|
465
|
-
|
|
466
|
-
className:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
520
|
+
const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
|
|
521
|
+
className: "polyx-signin__footer",
|
|
522
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
523
|
+
className: "polyx-signin__shield",
|
|
524
|
+
viewBox: "0 0 16 16",
|
|
525
|
+
fill: "currentColor",
|
|
526
|
+
"aria-hidden": "true",
|
|
527
|
+
focusable: "false",
|
|
528
|
+
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" })
|
|
529
|
+
}), labels.securedBy]
|
|
530
|
+
});
|
|
531
|
+
const tenantPicker = /* @__PURE__ */ jsxs("div", {
|
|
532
|
+
className: "polyx-signin__form",
|
|
533
|
+
children: [/* @__PURE__ */ jsx("ul", {
|
|
534
|
+
className: "polyx-signin__tenants",
|
|
535
|
+
children: (tenants ?? []).map((tenant) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx("button", {
|
|
536
|
+
type: "button",
|
|
537
|
+
className: "polyx-signin__tenant",
|
|
538
|
+
disabled: submitting,
|
|
539
|
+
onClick: () => void authenticate({ tenantId: tenant.tenantId }),
|
|
540
|
+
children: tenant.name ?? tenant.slug ?? tenant.organizationCode ?? tenant.tenantId
|
|
541
|
+
}) }, tenant.tenantId))
|
|
542
|
+
}), errorNode]
|
|
543
|
+
});
|
|
544
|
+
const setPasswordForm = /* @__PURE__ */ jsxs("form", {
|
|
545
|
+
className: "polyx-signin__form",
|
|
546
|
+
onSubmit: onSetPasswordSubmit,
|
|
547
|
+
children: [
|
|
548
|
+
/* @__PURE__ */ jsxs("label", {
|
|
549
|
+
className: "polyx-signin__field",
|
|
550
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
551
|
+
className: "polyx-signin__label",
|
|
552
|
+
children: labels.newPasswordLabel
|
|
553
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
554
|
+
className: "polyx-signin__input",
|
|
555
|
+
type: "password",
|
|
556
|
+
name: "newPassword",
|
|
557
|
+
autoComplete: "new-password",
|
|
558
|
+
placeholder: labels.passwordPlaceholder,
|
|
559
|
+
required: true,
|
|
560
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
561
|
+
value: newPassword,
|
|
562
|
+
disabled: submitting,
|
|
563
|
+
onChange: (event) => setNewPassword(event.target.value)
|
|
564
|
+
})]
|
|
565
|
+
}),
|
|
566
|
+
/* @__PURE__ */ jsxs("label", {
|
|
567
|
+
className: "polyx-signin__field",
|
|
568
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
569
|
+
className: "polyx-signin__label",
|
|
570
|
+
children: labels.confirmPasswordLabel
|
|
571
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
572
|
+
className: "polyx-signin__input",
|
|
573
|
+
type: "password",
|
|
574
|
+
name: "confirmPassword",
|
|
575
|
+
autoComplete: "new-password",
|
|
576
|
+
placeholder: labels.passwordPlaceholder,
|
|
577
|
+
required: true,
|
|
578
|
+
minLength: MIN_PASSWORD_LENGTH,
|
|
579
|
+
value: confirmPassword,
|
|
580
|
+
disabled: submitting,
|
|
581
|
+
onChange: (event) => setConfirmPassword(event.target.value)
|
|
582
|
+
})]
|
|
583
|
+
}),
|
|
584
|
+
errorNode,
|
|
585
|
+
/* @__PURE__ */ jsx("button", {
|
|
586
|
+
type: "submit",
|
|
587
|
+
className: "polyx-signin__submit",
|
|
588
|
+
disabled: submitting,
|
|
589
|
+
children: submitting ? labels.submitting : labels.setPasswordSubmit
|
|
590
|
+
})
|
|
591
|
+
]
|
|
592
|
+
});
|
|
593
|
+
const card = /* @__PURE__ */ jsxs("div", {
|
|
594
|
+
className: "polyx-signin__card",
|
|
595
|
+
children: [
|
|
596
|
+
header,
|
|
597
|
+
step === "select_tenant" ? tenantPicker : step === "set_password" ? setPasswordForm : /* @__PURE__ */ jsxs("form", {
|
|
483
598
|
className: "polyx-signin__form",
|
|
484
599
|
onSubmit,
|
|
485
600
|
children: [
|
|
@@ -525,8 +640,31 @@ function SignIn(props) {
|
|
|
525
640
|
children: submitting ? labels.submitting : labels.submit
|
|
526
641
|
})
|
|
527
642
|
]
|
|
528
|
-
})
|
|
529
|
-
|
|
643
|
+
}),
|
|
644
|
+
footer
|
|
645
|
+
]
|
|
646
|
+
});
|
|
647
|
+
if (variant === "page") return /* @__PURE__ */ jsxs("div", {
|
|
648
|
+
className: rootClass,
|
|
649
|
+
"data-polyx-signin": step,
|
|
650
|
+
children: [hasPanel ? /* @__PURE__ */ jsx("aside", {
|
|
651
|
+
className: "polyx-signin__panel",
|
|
652
|
+
children: props.panel ?? /* @__PURE__ */ jsxs(Fragment, { children: [props.logo ? /* @__PURE__ */ jsx("div", {
|
|
653
|
+
className: "polyx-signin__panel-logo",
|
|
654
|
+
children: props.logo
|
|
655
|
+
}) : null, labels.tagline ? /* @__PURE__ */ jsx("p", {
|
|
656
|
+
className: "polyx-signin__tagline",
|
|
657
|
+
children: labels.tagline
|
|
658
|
+
}) : null] })
|
|
659
|
+
}) : null, /* @__PURE__ */ jsx("div", {
|
|
660
|
+
className: "polyx-signin__main",
|
|
661
|
+
children: card
|
|
662
|
+
})]
|
|
663
|
+
});
|
|
664
|
+
return /* @__PURE__ */ jsx("div", {
|
|
665
|
+
className: rootClass,
|
|
666
|
+
"data-polyx-signin": step,
|
|
667
|
+
children: card
|
|
530
668
|
});
|
|
531
669
|
}
|
|
532
670
|
//#endregion
|
package/dist/styles.css
CHANGED
|
@@ -98,15 +98,6 @@
|
|
|
98
98
|
|
|
99
99
|
/* --- Variants ------------------------------------------------------------ */
|
|
100
100
|
|
|
101
|
-
/* `page`: owns the viewport — centered card on a full-bleed background. */
|
|
102
|
-
.polyx-signin--page {
|
|
103
|
-
display: grid;
|
|
104
|
-
place-items: center;
|
|
105
|
-
min-height: 100dvh;
|
|
106
|
-
padding: 1.5rem;
|
|
107
|
-
background: var(--polyx-page-bg);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
101
|
/* `card`: just the card — drop it anywhere in your own layout. */
|
|
111
102
|
.polyx-signin--card {
|
|
112
103
|
display: block;
|
|
@@ -114,15 +105,110 @@
|
|
|
114
105
|
|
|
115
106
|
.polyx-signin__card {
|
|
116
107
|
width: 100%;
|
|
117
|
-
max-width:
|
|
108
|
+
max-width: 27rem;
|
|
118
109
|
margin-inline: auto;
|
|
119
|
-
padding:
|
|
110
|
+
padding: 2.25rem;
|
|
120
111
|
background: var(--polyx-bg);
|
|
121
112
|
border: 1px solid var(--polyx-border);
|
|
122
113
|
border-radius: var(--polyx-radius);
|
|
123
114
|
box-shadow: var(--polyx-shadow);
|
|
124
115
|
}
|
|
125
116
|
|
|
117
|
+
/**
|
|
118
|
+
* `page`: owns the viewport. With brand content (a `logo`, a `tagline`, or a
|
|
119
|
+
* custom `panel`) it becomes a split layout — brand panel beside the form on
|
|
120
|
+
* wide screens, a slim brand band above it on narrow ones. With no brand content
|
|
121
|
+
* there's nothing to put in a panel, so it stays a single centered column.
|
|
122
|
+
*/
|
|
123
|
+
.polyx-signin--page {
|
|
124
|
+
display: grid;
|
|
125
|
+
grid-template-columns: 1fr;
|
|
126
|
+
min-height: 100dvh;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.polyx-signin--page.polyx-signin--split {
|
|
130
|
+
grid-template-rows: auto 1fr;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (min-width: 64rem) {
|
|
134
|
+
.polyx-signin--page.polyx-signin--split {
|
|
135
|
+
grid-template-columns: 1.1fr 1fr;
|
|
136
|
+
grid-template-rows: 1fr;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.polyx-signin__panel {
|
|
141
|
+
display: flex;
|
|
142
|
+
position: relative;
|
|
143
|
+
overflow: hidden;
|
|
144
|
+
align-items: center;
|
|
145
|
+
gap: 1rem;
|
|
146
|
+
padding: 1.25rem 1.5rem;
|
|
147
|
+
color: var(--polyx-brand-fg);
|
|
148
|
+
background: linear-gradient(145deg, var(--polyx-brand), color-mix(in srgb, var(--polyx-brand) 55%, #000));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@media (min-width: 64rem) {
|
|
152
|
+
.polyx-signin__panel {
|
|
153
|
+
flex-direction: column;
|
|
154
|
+
align-items: flex-start;
|
|
155
|
+
justify-content: space-between;
|
|
156
|
+
gap: 2rem;
|
|
157
|
+
padding: 3.5rem;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Soft light-bloom so a bare brand panel still reads as designed, not as a flat fill. */
|
|
162
|
+
.polyx-signin__panel::after {
|
|
163
|
+
content: "";
|
|
164
|
+
position: absolute;
|
|
165
|
+
inset: 0;
|
|
166
|
+
pointer-events: none;
|
|
167
|
+
background:
|
|
168
|
+
radial-gradient(circle at 82% 12%, rgb(255 255 255 / 0.18), transparent 45%),
|
|
169
|
+
radial-gradient(circle at 12% 88%, rgb(255 255 255 / 0.1), transparent 42%);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.polyx-signin__panel-logo,
|
|
173
|
+
.polyx-signin__tagline {
|
|
174
|
+
position: relative;
|
|
175
|
+
z-index: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* The tagline is the panel's headline — it only earns its space on wide screens. */
|
|
179
|
+
.polyx-signin__tagline {
|
|
180
|
+
display: none;
|
|
181
|
+
margin: 0;
|
|
182
|
+
max-width: 18ch;
|
|
183
|
+
font-size: 2rem;
|
|
184
|
+
font-weight: 600;
|
|
185
|
+
line-height: 1.2;
|
|
186
|
+
letter-spacing: -0.02em;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@media (min-width: 64rem) {
|
|
190
|
+
.polyx-signin__tagline {
|
|
191
|
+
display: block;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.polyx-signin__main {
|
|
196
|
+
display: grid;
|
|
197
|
+
place-items: center;
|
|
198
|
+
padding: 2.5rem 1.5rem;
|
|
199
|
+
background: var(--polyx-page-bg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* On the page variant the form isn't a floating card — it sits on the page itself. */
|
|
203
|
+
.polyx-signin--page .polyx-signin__card {
|
|
204
|
+
max-width: 25rem;
|
|
205
|
+
padding: 0;
|
|
206
|
+
background: transparent;
|
|
207
|
+
border: none;
|
|
208
|
+
box-shadow: none;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
126
212
|
/* --- Header -------------------------------------------------------------- */
|
|
127
213
|
|
|
128
214
|
.polyx-signin__header {
|
|
@@ -249,6 +335,25 @@
|
|
|
249
335
|
border-radius: var(--polyx-radius-sm);
|
|
250
336
|
}
|
|
251
337
|
|
|
338
|
+
/* --- Footer -------------------------------------------------------------- */
|
|
339
|
+
|
|
340
|
+
.polyx-signin__footer {
|
|
341
|
+
display: flex;
|
|
342
|
+
align-items: center;
|
|
343
|
+
justify-content: center;
|
|
344
|
+
gap: 0.375rem;
|
|
345
|
+
margin-top: 1.5rem;
|
|
346
|
+
font-size: 0.75rem;
|
|
347
|
+
color: var(--polyx-muted-fg);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.polyx-signin__shield {
|
|
351
|
+
flex: none;
|
|
352
|
+
width: 0.875rem;
|
|
353
|
+
height: 0.875rem;
|
|
354
|
+
opacity: 0.85;
|
|
355
|
+
}
|
|
356
|
+
|
|
252
357
|
/* --- Workspace picker ---------------------------------------------------- */
|
|
253
358
|
|
|
254
359
|
.polyx-signin__tenants {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poly-x/react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.4",
|
|
4
4
|
"description": "PolyX SDK for React SPAs - provider, hooks, drop-in auth UI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"module": "./dist/index.mjs",
|
|
28
28
|
"types": "./dist/index.d.cts",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@poly-x/core": "0.1.0-alpha.
|
|
30
|
+
"@poly-x/core": "0.1.0-alpha.4"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18",
|