@poly-x/react 0.1.0-alpha.6 → 0.1.0-alpha.7

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 CHANGED
@@ -352,7 +352,7 @@ function Protected({ children, fallback = null, loading = null }) {
352
352
  }
353
353
  //#endregion
354
354
  //#region src/components/sign-in.tsx
355
- const DEFAULT_LABELS = {
355
+ const DEFAULT_LABELS$2 = {
356
356
  heading: "Sign in",
357
357
  subheading: "Enter your credentials to continue.",
358
358
  tagline: "",
@@ -393,7 +393,7 @@ function SignIn(props) {
393
393
  const action = props.action ?? "/api/polyx/authenticate";
394
394
  const variant = props.variant ?? "card";
395
395
  const labels = {
396
- ...DEFAULT_LABELS,
396
+ ...DEFAULT_LABELS$2,
397
397
  ...props.labels
398
398
  };
399
399
  const [email, setEmail] = (0, react.useState)("");
@@ -668,6 +668,486 @@ function SignIn(props) {
668
668
  });
669
669
  }
670
670
  //#endregion
671
+ //#region src/components/_recovery-status.tsx
672
+ const ICONS = {
673
+ mail: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
674
+ viewBox: "0 0 24 24",
675
+ fill: "none",
676
+ stroke: "currentColor",
677
+ strokeWidth: "1.75",
678
+ "aria-hidden": "true",
679
+ focusable: "false",
680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
681
+ x: "3",
682
+ y: "5",
683
+ width: "18",
684
+ height: "14",
685
+ rx: "2.5"
686
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
687
+ d: "m4 7 8 6 8-6",
688
+ strokeLinecap: "round",
689
+ strokeLinejoin: "round"
690
+ })]
691
+ }),
692
+ check: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
693
+ viewBox: "0 0 24 24",
694
+ fill: "none",
695
+ stroke: "currentColor",
696
+ strokeWidth: "2",
697
+ "aria-hidden": "true",
698
+ focusable: "false",
699
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
700
+ d: "m5 12.5 4.5 4.5L19 7",
701
+ strokeLinecap: "round",
702
+ strokeLinejoin: "round"
703
+ })
704
+ }),
705
+ warning: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
706
+ viewBox: "0 0 24 24",
707
+ fill: "none",
708
+ stroke: "currentColor",
709
+ strokeWidth: "1.75",
710
+ "aria-hidden": "true",
711
+ focusable: "false",
712
+ children: [
713
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
714
+ cx: "12",
715
+ cy: "12",
716
+ r: "9"
717
+ }),
718
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
719
+ d: "M12 7.5v5",
720
+ strokeLinecap: "round"
721
+ }),
722
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
723
+ cx: "12",
724
+ cy: "16",
725
+ r: "0.6",
726
+ fill: "currentColor",
727
+ stroke: "none"
728
+ })
729
+ ]
730
+ })
731
+ };
732
+ /**
733
+ * A centered, icon-topped confirmation card — the shape every recovery terminal
734
+ * state shares (link sent, password updated, link expired). Reuses the `<SignIn/>`
735
+ * class anatomy so it themes from the same `--polyx-*` custom properties.
736
+ */
737
+ function RecoveryStatus(props) {
738
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
739
+ className: "polyx-signin__card",
740
+ children: [
741
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
742
+ className: "polyx-signin__header polyx-signin__header--center",
743
+ children: [
744
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
745
+ className: `polyx-signin__status-icon polyx-signin__status-icon--${props.tone ?? "brand"}`,
746
+ children: ICONS[props.icon]
747
+ }),
748
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
749
+ className: "polyx-signin__heading",
750
+ children: props.heading
751
+ }),
752
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
753
+ className: "polyx-signin__subheading",
754
+ children: props.children
755
+ })
756
+ ]
757
+ }),
758
+ props.actions ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
759
+ className: "polyx-signin__actions",
760
+ children: props.actions
761
+ }) : null,
762
+ props.footer
763
+ ]
764
+ });
765
+ }
766
+ //#endregion
767
+ //#region src/components/forgot-password.tsx
768
+ const DEFAULT_LABELS$1 = {
769
+ heading: "Reset your password",
770
+ subheading: "Enter your email and we'll send you a reset link.",
771
+ emailLabel: "Email",
772
+ emailPlaceholder: "you@company.com",
773
+ submit: "Send reset link",
774
+ submitting: "Sending…",
775
+ sentHeading: "Check your email",
776
+ sentSubheading: "If an account exists for {email}, a password reset link is on its way. It expires in 30 minutes.",
777
+ differentEmail: "Use a different email",
778
+ backToSignIn: "Back to sign in",
779
+ securedBy: "Secured by PolyX",
780
+ genericError: "Something went wrong. Please try again."
781
+ };
782
+ /** Splits a label on `{email}` and drops the entered address in, emphasized. */
783
+ function withEmail(template, email) {
784
+ return template.split("{email}").map((chunk, index, all) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [chunk, index < all.length - 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
785
+ className: "polyx-signin__email",
786
+ children: email || "that address"
787
+ }) : null] }, index));
788
+ }
789
+ /**
790
+ * The self-service "forgot password" form (poly-x v5 / F040). Renders an email field
791
+ * inside the consuming app and POSTs to the SDK's BFF (`@poly-x/next` `forgotPassword`),
792
+ * which asks poly-auth to email a single-use reset link SERVER-SIDE. The response is
793
+ * always the same — no token, no account-existence signal — so after submit the form
794
+ * shows an identical "check your email" state whether or not the account exists.
795
+ *
796
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
797
+ * via the `--polyx-*` custom properties. Target `.polyx-forgot` for form-specific tweaks.
798
+ */
799
+ function ForgotPassword(props) {
800
+ const action = props.action ?? "/api/polyx/forgot-password";
801
+ const signInPath = props.signInPath ?? "/sign-in";
802
+ const labels = {
803
+ ...DEFAULT_LABELS$1,
804
+ ...props.labels
805
+ };
806
+ const [email, setEmail] = (0, react.useState)("");
807
+ const [submitting, setSubmitting] = (0, react.useState)(false);
808
+ const [sent, setSent] = (0, react.useState)(false);
809
+ const [error, setError] = (0, react.useState)(null);
810
+ async function onSubmit(event) {
811
+ event.preventDefault();
812
+ setSubmitting(true);
813
+ setError(null);
814
+ try {
815
+ if ((await (await fetch(action, {
816
+ method: "POST",
817
+ headers: { "content-type": "application/json" },
818
+ body: JSON.stringify({ email })
819
+ })).json().catch(() => ({}))).status === "accepted") {
820
+ setSent(true);
821
+ return;
822
+ }
823
+ setError(labels.genericError);
824
+ } catch {
825
+ setError(labels.genericError);
826
+ } finally {
827
+ setSubmitting(false);
828
+ }
829
+ }
830
+ const rootClass = [
831
+ "polyx-signin",
832
+ "polyx-forgot",
833
+ props.className
834
+ ].filter(Boolean).join(" ");
835
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
836
+ className: "polyx-signin__footer",
837
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
838
+ className: "polyx-signin__shield",
839
+ viewBox: "0 0 16 16",
840
+ fill: "currentColor",
841
+ "aria-hidden": "true",
842
+ focusable: "false",
843
+ 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" })
844
+ }), labels.securedBy]
845
+ });
846
+ const header = (heading, subheading) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
847
+ className: "polyx-signin__header",
848
+ children: [
849
+ props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
850
+ className: "polyx-signin__logo",
851
+ children: props.logo
852
+ }) : null,
853
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
854
+ className: "polyx-signin__heading",
855
+ children: heading
856
+ }),
857
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
858
+ className: "polyx-signin__subheading",
859
+ children: subheading
860
+ })
861
+ ]
862
+ });
863
+ if (sent) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
864
+ className: rootClass,
865
+ "data-polyx-forgot": "sent",
866
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecoveryStatus, {
867
+ icon: "mail",
868
+ heading: labels.sentHeading,
869
+ footer,
870
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
871
+ type: "button",
872
+ className: "polyx-signin__link",
873
+ onClick: () => setSent(false),
874
+ children: labels.differentEmail
875
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
876
+ className: "polyx-signin__link",
877
+ href: signInPath,
878
+ children: labels.backToSignIn
879
+ })] }),
880
+ children: withEmail(labels.sentSubheading, email)
881
+ })
882
+ });
883
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
884
+ className: rootClass,
885
+ "data-polyx-forgot": "request",
886
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
887
+ className: "polyx-signin__card",
888
+ children: [
889
+ header(labels.heading, labels.subheading),
890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
891
+ className: "polyx-signin__form",
892
+ onSubmit,
893
+ children: [
894
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
895
+ className: "polyx-signin__field",
896
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
897
+ className: "polyx-signin__label",
898
+ children: labels.emailLabel
899
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
900
+ className: "polyx-signin__input",
901
+ type: "email",
902
+ name: "email",
903
+ autoComplete: "username",
904
+ placeholder: labels.emailPlaceholder,
905
+ required: true,
906
+ value: email,
907
+ disabled: submitting,
908
+ onChange: (event) => setEmail(event.target.value)
909
+ })]
910
+ }),
911
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
912
+ role: "alert",
913
+ className: "polyx-signin__error",
914
+ children: error
915
+ }) : null,
916
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
917
+ type: "submit",
918
+ className: "polyx-signin__submit",
919
+ disabled: submitting,
920
+ children: submitting ? labels.submitting : labels.submit
921
+ })
922
+ ]
923
+ }),
924
+ footer
925
+ ]
926
+ })
927
+ });
928
+ }
929
+ //#endregion
930
+ //#region src/components/reset-password.tsx
931
+ const DEFAULT_LABELS = {
932
+ heading: "Choose a new password",
933
+ subheading: "Enter and confirm your new password.",
934
+ newPasswordLabel: "New password",
935
+ confirmPasswordLabel: "Confirm new password",
936
+ passwordPlaceholder: "••••••••",
937
+ submit: "Reset password",
938
+ submitting: "Resetting…",
939
+ successHeading: "Password updated",
940
+ successSubheading: "You can now sign in with your new password.",
941
+ invalidHeading: "This link is invalid or expired",
942
+ invalidSubheading: "Password reset links can be used once and expire quickly. Request a new one.",
943
+ requestNewLink: "Request a new link",
944
+ backToSignIn: "Back to sign in",
945
+ securedBy: "Secured by PolyX",
946
+ passwordMismatch: "Those passwords don't match.",
947
+ weakPassword: "Password must be at least 8 characters.",
948
+ genericError: "Something went wrong. Please try again."
949
+ };
950
+ /** Matches poly-auth's v5 reset floor (F040). */
951
+ const DEFAULT_MIN_LENGTH = 8;
952
+ /**
953
+ * The reset-completion form (poly-x v5 / F040). Reads the opaque token from the URL,
954
+ * collects a new password, and POSTs to the SDK's BFF (`@poly-x/next` `resetPassword`),
955
+ * which redeems the token single-use server-side. The token is never decoded in the
956
+ * browser — identity, validity, and expiry are all enforced by the backend.
957
+ *
958
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
959
+ * via the `--polyx-*` custom properties. Target `.polyx-reset` for form-specific tweaks.
960
+ */
961
+ function ResetPassword(props) {
962
+ const action = props.action ?? "/api/polyx/reset-password";
963
+ const minLength = props.minLength ?? DEFAULT_MIN_LENGTH;
964
+ const signInPath = props.signInPath ?? "/sign-in";
965
+ const forgotPasswordPath = props.forgotPasswordPath ?? "/forgot-password";
966
+ const labels = {
967
+ ...DEFAULT_LABELS,
968
+ ...props.labels
969
+ };
970
+ const [token] = (0, react.useState)(() => props.token ?? (typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("token") ?? "" : ""));
971
+ const [newPassword, setNewPassword] = (0, react.useState)("");
972
+ const [confirmPassword, setConfirmPassword] = (0, react.useState)("");
973
+ const [submitting, setSubmitting] = (0, react.useState)(false);
974
+ const [error, setError] = (0, react.useState)(null);
975
+ const [step, setStep] = (0, react.useState)(token ? "form" : "invalid");
976
+ async function onSubmit(event) {
977
+ event.preventDefault();
978
+ if (newPassword !== confirmPassword) {
979
+ setError(labels.passwordMismatch);
980
+ return;
981
+ }
982
+ setSubmitting(true);
983
+ setError(null);
984
+ try {
985
+ switch ((await (await fetch(action, {
986
+ method: "POST",
987
+ headers: { "content-type": "application/json" },
988
+ body: JSON.stringify({
989
+ token,
990
+ newPassword,
991
+ confirmPassword
992
+ })
993
+ })).json().catch(() => ({}))).status) {
994
+ case "success":
995
+ setStep("success");
996
+ if (props.afterResetPath) window.location.assign(props.afterResetPath);
997
+ return;
998
+ case "weak_password":
999
+ setError(labels.weakPassword);
1000
+ return;
1001
+ case "password_mismatch":
1002
+ setError(labels.passwordMismatch);
1003
+ return;
1004
+ case "invalid_or_expired":
1005
+ setStep("invalid");
1006
+ return;
1007
+ default:
1008
+ setError(labels.genericError);
1009
+ return;
1010
+ }
1011
+ } catch {
1012
+ setError(labels.genericError);
1013
+ } finally {
1014
+ setSubmitting(false);
1015
+ }
1016
+ }
1017
+ const rootClass = [
1018
+ "polyx-signin",
1019
+ "polyx-reset",
1020
+ props.className
1021
+ ].filter(Boolean).join(" ");
1022
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1023
+ className: "polyx-signin__footer",
1024
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1025
+ className: "polyx-signin__shield",
1026
+ viewBox: "0 0 16 16",
1027
+ fill: "currentColor",
1028
+ "aria-hidden": "true",
1029
+ focusable: "false",
1030
+ 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" })
1031
+ }), labels.securedBy]
1032
+ });
1033
+ const header = (heading, subheading) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1034
+ className: "polyx-signin__header",
1035
+ children: [
1036
+ props.logo ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1037
+ className: "polyx-signin__logo",
1038
+ children: props.logo
1039
+ }) : null,
1040
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
1041
+ className: "polyx-signin__heading",
1042
+ children: heading
1043
+ }),
1044
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1045
+ className: "polyx-signin__subheading",
1046
+ children: subheading
1047
+ })
1048
+ ]
1049
+ });
1050
+ if (step === "success") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1051
+ className: rootClass,
1052
+ "data-polyx-reset": "success",
1053
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecoveryStatus, {
1054
+ icon: "check",
1055
+ heading: labels.successHeading,
1056
+ footer,
1057
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1058
+ className: "polyx-signin__link",
1059
+ href: signInPath,
1060
+ children: labels.backToSignIn
1061
+ }),
1062
+ children: labels.successSubheading
1063
+ })
1064
+ });
1065
+ if (step === "invalid") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1066
+ className: rootClass,
1067
+ "data-polyx-reset": "invalid",
1068
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RecoveryStatus, {
1069
+ icon: "warning",
1070
+ tone: "danger",
1071
+ heading: labels.invalidHeading,
1072
+ footer,
1073
+ actions: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1074
+ className: "polyx-signin__link",
1075
+ href: forgotPasswordPath,
1076
+ children: labels.requestNewLink
1077
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1078
+ className: "polyx-signin__link",
1079
+ href: signInPath,
1080
+ children: labels.backToSignIn
1081
+ })] }),
1082
+ children: labels.invalidSubheading
1083
+ })
1084
+ });
1085
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1086
+ className: rootClass,
1087
+ "data-polyx-reset": step,
1088
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1089
+ className: "polyx-signin__card",
1090
+ children: [
1091
+ header(labels.heading, labels.subheading),
1092
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1093
+ className: "polyx-signin__form",
1094
+ onSubmit,
1095
+ children: [
1096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1097
+ className: "polyx-signin__field",
1098
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1099
+ className: "polyx-signin__label",
1100
+ children: labels.newPasswordLabel
1101
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1102
+ className: "polyx-signin__input",
1103
+ type: "password",
1104
+ name: "newPassword",
1105
+ autoComplete: "new-password",
1106
+ placeholder: labels.passwordPlaceholder,
1107
+ required: true,
1108
+ minLength,
1109
+ value: newPassword,
1110
+ disabled: submitting,
1111
+ onChange: (event) => setNewPassword(event.target.value)
1112
+ })]
1113
+ }),
1114
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1115
+ className: "polyx-signin__field",
1116
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1117
+ className: "polyx-signin__label",
1118
+ children: labels.confirmPasswordLabel
1119
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1120
+ className: "polyx-signin__input",
1121
+ type: "password",
1122
+ name: "confirmPassword",
1123
+ autoComplete: "new-password",
1124
+ placeholder: labels.passwordPlaceholder,
1125
+ required: true,
1126
+ minLength,
1127
+ value: confirmPassword,
1128
+ disabled: submitting,
1129
+ onChange: (event) => setConfirmPassword(event.target.value)
1130
+ })]
1131
+ }),
1132
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1133
+ role: "alert",
1134
+ className: "polyx-signin__error",
1135
+ children: error
1136
+ }) : null,
1137
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1138
+ type: "submit",
1139
+ className: "polyx-signin__submit",
1140
+ disabled: submitting,
1141
+ children: submitting ? labels.submitting : labels.submit
1142
+ })
1143
+ ]
1144
+ }),
1145
+ footer
1146
+ ]
1147
+ })
1148
+ });
1149
+ }
1150
+ //#endregion
671
1151
  //#region src/components/auth-callback.tsx
672
1152
  /** Drop on your `/auth/callback` route: completes the sign-in exchange on mount. */
673
1153
  function AuthCallback({ children = null }) {
@@ -689,11 +1169,13 @@ const version = "0.0.0";
689
1169
  exports.AuthCallback = AuthCallback;
690
1170
  exports.BroadcastAuthChannel = BroadcastAuthChannel;
691
1171
  exports.BroadcastChannelSessionChannel = BroadcastChannelSessionChannel;
1172
+ exports.ForgotPassword = ForgotPassword;
692
1173
  exports.PACKAGE_NAME = PACKAGE_NAME;
693
1174
  exports.PolyXContext = PolyXContext;
694
1175
  exports.PolyXProvider = PolyXProvider;
695
1176
  exports.PopupCancelledError = PopupCancelledError;
696
1177
  exports.Protected = Protected;
1178
+ exports.ResetPassword = ResetPassword;
697
1179
  exports.SessionStoragePkceStore = SessionStoragePkceStore;
698
1180
  exports.SignIn = SignIn;
699
1181
  exports.WebLocksLock = WebLocksLock;
package/dist/index.d.cts CHANGED
@@ -261,6 +261,101 @@ interface SignInProps {
261
261
  */
262
262
  declare function SignIn(props: SignInProps): ReactNode;
263
263
  //#endregion
264
+ //#region src/components/forgot-password.d.ts
265
+ /** Every user-facing string, overridable for i18n / white-labelling. */
266
+ interface ForgotPasswordLabels {
267
+ heading: string;
268
+ subheading: string;
269
+ emailLabel: string;
270
+ emailPlaceholder: string;
271
+ submit: string;
272
+ submitting: string;
273
+ /** Shown after submit — always, regardless of whether the account exists. */
274
+ sentHeading: string;
275
+ /** Confirmation body. `{email}` is replaced with the entered address, emphasized. */
276
+ sentSubheading: string;
277
+ differentEmail: string;
278
+ backToSignIn: string;
279
+ securedBy: string;
280
+ genericError: string;
281
+ }
282
+ interface ForgotPasswordProps {
283
+ /** BFF endpoint backed by `createAuthHandlers().forgotPassword`. Default `/api/polyx/forgot-password`. */
284
+ action?: string;
285
+ /** Where the "back to sign in" link points. Default `/sign-in`. */
286
+ signInPath?: string;
287
+ /** Rendered above the heading — your wordmark or logo. */
288
+ logo?: ReactNode;
289
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
290
+ showSecuredBy?: boolean;
291
+ /** Appended to the root element's class list, for escape-hatch styling. */
292
+ className?: string;
293
+ labels?: Partial<ForgotPasswordLabels>;
294
+ }
295
+ /**
296
+ * The self-service "forgot password" form (poly-x v5 / F040). Renders an email field
297
+ * inside the consuming app and POSTs to the SDK's BFF (`@poly-x/next` `forgotPassword`),
298
+ * which asks poly-auth to email a single-use reset link SERVER-SIDE. The response is
299
+ * always the same — no token, no account-existence signal — so after submit the form
300
+ * shows an identical "check your email" state whether or not the account exists.
301
+ *
302
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
303
+ * via the `--polyx-*` custom properties. Target `.polyx-forgot` for form-specific tweaks.
304
+ */
305
+ declare function ForgotPassword(props: ForgotPasswordProps): ReactNode;
306
+ //#endregion
307
+ //#region src/components/reset-password.d.ts
308
+ /** Every user-facing string, overridable for i18n / white-labelling. */
309
+ interface ResetPasswordLabels {
310
+ heading: string;
311
+ subheading: string;
312
+ newPasswordLabel: string;
313
+ confirmPasswordLabel: string;
314
+ passwordPlaceholder: string;
315
+ submit: string;
316
+ submitting: string;
317
+ successHeading: string;
318
+ successSubheading: string;
319
+ invalidHeading: string;
320
+ invalidSubheading: string;
321
+ requestNewLink: string;
322
+ backToSignIn: string;
323
+ securedBy: string;
324
+ passwordMismatch: string;
325
+ weakPassword: string;
326
+ genericError: string;
327
+ }
328
+ interface ResetPasswordProps {
329
+ /** BFF endpoint backed by `createAuthHandlers().resetPassword`. Default `/api/polyx/reset-password`. */
330
+ action?: string;
331
+ /** The opaque reset token. Defaults to `?token=` from the URL (client-side). */
332
+ token?: string;
333
+ /** Where to send the browser after a successful reset. Default `/sign-in`. */
334
+ afterResetPath?: string;
335
+ /** Where "request a new link" / "back to sign in" point. Default `/forgot-password` and `/sign-in`. */
336
+ forgotPasswordPath?: string;
337
+ signInPath?: string;
338
+ /** Rendered above the heading — your wordmark or logo. */
339
+ logo?: ReactNode;
340
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
341
+ showSecuredBy?: boolean;
342
+ /** Client-side minimum password length (the backend enforces the real floor). Default 8. */
343
+ minLength?: number;
344
+ /** Appended to the root element's class list, for escape-hatch styling. */
345
+ className?: string;
346
+ labels?: Partial<ResetPasswordLabels>;
347
+ }
348
+ /**
349
+ * The reset-completion form (poly-x v5 / F040). Reads the opaque token from the URL,
350
+ * collects a new password, and POSTs to the SDK's BFF (`@poly-x/next` `resetPassword`),
351
+ * which redeems the token single-use server-side. The token is never decoded in the
352
+ * browser — identity, validity, and expiry are all enforced by the backend.
353
+ *
354
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
355
+ * via the `--polyx-*` custom properties. Target `.polyx-reset` for form-specific tweaks.
356
+ */
357
+ declare function ResetPassword(props: ResetPasswordProps): ReactNode;
358
+ //#endregion
264
359
  //#region src/components/auth-callback.d.ts
265
360
  interface AuthCallbackProps {
266
361
  /** Rendered while the exchange completes (e.g. a spinner). */
@@ -279,4 +374,4 @@ declare function AuthCallback({
279
374
  declare const PACKAGE_NAME = "@poly-x/react";
280
375
  declare const version = "0.0.0";
281
376
  //#endregion
282
- export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
377
+ export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
package/dist/index.d.mts CHANGED
@@ -261,6 +261,101 @@ interface SignInProps {
261
261
  */
262
262
  declare function SignIn(props: SignInProps): ReactNode;
263
263
  //#endregion
264
+ //#region src/components/forgot-password.d.ts
265
+ /** Every user-facing string, overridable for i18n / white-labelling. */
266
+ interface ForgotPasswordLabels {
267
+ heading: string;
268
+ subheading: string;
269
+ emailLabel: string;
270
+ emailPlaceholder: string;
271
+ submit: string;
272
+ submitting: string;
273
+ /** Shown after submit — always, regardless of whether the account exists. */
274
+ sentHeading: string;
275
+ /** Confirmation body. `{email}` is replaced with the entered address, emphasized. */
276
+ sentSubheading: string;
277
+ differentEmail: string;
278
+ backToSignIn: string;
279
+ securedBy: string;
280
+ genericError: string;
281
+ }
282
+ interface ForgotPasswordProps {
283
+ /** BFF endpoint backed by `createAuthHandlers().forgotPassword`. Default `/api/polyx/forgot-password`. */
284
+ action?: string;
285
+ /** Where the "back to sign in" link points. Default `/sign-in`. */
286
+ signInPath?: string;
287
+ /** Rendered above the heading — your wordmark or logo. */
288
+ logo?: ReactNode;
289
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
290
+ showSecuredBy?: boolean;
291
+ /** Appended to the root element's class list, for escape-hatch styling. */
292
+ className?: string;
293
+ labels?: Partial<ForgotPasswordLabels>;
294
+ }
295
+ /**
296
+ * The self-service "forgot password" form (poly-x v5 / F040). Renders an email field
297
+ * inside the consuming app and POSTs to the SDK's BFF (`@poly-x/next` `forgotPassword`),
298
+ * which asks poly-auth to email a single-use reset link SERVER-SIDE. The response is
299
+ * always the same — no token, no account-existence signal — so after submit the form
300
+ * shows an identical "check your email" state whether or not the account exists.
301
+ *
302
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
303
+ * via the `--polyx-*` custom properties. Target `.polyx-forgot` for form-specific tweaks.
304
+ */
305
+ declare function ForgotPassword(props: ForgotPasswordProps): ReactNode;
306
+ //#endregion
307
+ //#region src/components/reset-password.d.ts
308
+ /** Every user-facing string, overridable for i18n / white-labelling. */
309
+ interface ResetPasswordLabels {
310
+ heading: string;
311
+ subheading: string;
312
+ newPasswordLabel: string;
313
+ confirmPasswordLabel: string;
314
+ passwordPlaceholder: string;
315
+ submit: string;
316
+ submitting: string;
317
+ successHeading: string;
318
+ successSubheading: string;
319
+ invalidHeading: string;
320
+ invalidSubheading: string;
321
+ requestNewLink: string;
322
+ backToSignIn: string;
323
+ securedBy: string;
324
+ passwordMismatch: string;
325
+ weakPassword: string;
326
+ genericError: string;
327
+ }
328
+ interface ResetPasswordProps {
329
+ /** BFF endpoint backed by `createAuthHandlers().resetPassword`. Default `/api/polyx/reset-password`. */
330
+ action?: string;
331
+ /** The opaque reset token. Defaults to `?token=` from the URL (client-side). */
332
+ token?: string;
333
+ /** Where to send the browser after a successful reset. Default `/sign-in`. */
334
+ afterResetPath?: string;
335
+ /** Where "request a new link" / "back to sign in" point. Default `/forgot-password` and `/sign-in`. */
336
+ forgotPasswordPath?: string;
337
+ signInPath?: string;
338
+ /** Rendered above the heading — your wordmark or logo. */
339
+ logo?: ReactNode;
340
+ /** Show the "Secured by PolyX" attribution under the form. Default true. */
341
+ showSecuredBy?: boolean;
342
+ /** Client-side minimum password length (the backend enforces the real floor). Default 8. */
343
+ minLength?: number;
344
+ /** Appended to the root element's class list, for escape-hatch styling. */
345
+ className?: string;
346
+ labels?: Partial<ResetPasswordLabels>;
347
+ }
348
+ /**
349
+ * The reset-completion form (poly-x v5 / F040). Reads the opaque token from the URL,
350
+ * collects a new password, and POSTs to the SDK's BFF (`@poly-x/next` `resetPassword`),
351
+ * which redeems the token single-use server-side. The token is never decoded in the
352
+ * browser — identity, validity, and expiry are all enforced by the backend.
353
+ *
354
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
355
+ * via the `--polyx-*` custom properties. Target `.polyx-reset` for form-specific tweaks.
356
+ */
357
+ declare function ResetPassword(props: ResetPasswordProps): ReactNode;
358
+ //#endregion
264
359
  //#region src/components/auth-callback.d.ts
265
360
  interface AuthCallbackProps {
266
361
  /** Rendered while the exchange completes (e.g. a spinner). */
@@ -279,4 +374,4 @@ declare function AuthCallback({
279
374
  declare const PACKAGE_NAME = "@poly-x/react";
280
375
  declare const version = "0.0.0";
281
376
  //#endregion
282
- export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
377
+ export { AuthCallback, type AuthCallbackMessage, type AuthCallbackProps, type AuthChannel, BroadcastAuthChannel, BroadcastChannelSessionChannel, type BrowserBridge, type CallbackParams, ForgotPassword, type ForgotPasswordLabels, type ForgotPasswordProps, type LoginController, type LoginControllerDeps, PACKAGE_NAME, PolyXContext, type PolyXContextValue, PolyXProvider, type PolyXProviderProps, PopupCancelledError, type PopupHandle, Protected, type ProtectedProps, ResetPassword, type ResetPasswordLabels, type ResetPasswordProps, SessionStoragePkceStore, SignIn, type SignInLabels, type SignInOptions, type SignInProps, type SignInVariant, type UseAuth, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
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, useState, useSyncExternalStore } from "react";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment, createContext, useCallback, useContext, useEffect, useMemo, 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;
@@ -351,7 +351,7 @@ function Protected({ children, fallback = null, loading = null }) {
351
351
  }
352
352
  //#endregion
353
353
  //#region src/components/sign-in.tsx
354
- const DEFAULT_LABELS = {
354
+ const DEFAULT_LABELS$2 = {
355
355
  heading: "Sign in",
356
356
  subheading: "Enter your credentials to continue.",
357
357
  tagline: "",
@@ -392,7 +392,7 @@ function SignIn(props) {
392
392
  const action = props.action ?? "/api/polyx/authenticate";
393
393
  const variant = props.variant ?? "card";
394
394
  const labels = {
395
- ...DEFAULT_LABELS,
395
+ ...DEFAULT_LABELS$2,
396
396
  ...props.labels
397
397
  };
398
398
  const [email, setEmail] = useState("");
@@ -648,7 +648,7 @@ function SignIn(props) {
648
648
  "data-polyx-signin": step,
649
649
  children: [/* @__PURE__ */ jsx("aside", {
650
650
  className: "polyx-signin__panel",
651
- children: props.panel ?? /* @__PURE__ */ jsxs(Fragment, { children: [props.logo ? /* @__PURE__ */ jsx("div", {
651
+ children: props.panel ?? /* @__PURE__ */ jsxs(Fragment$1, { children: [props.logo ? /* @__PURE__ */ jsx("div", {
652
652
  className: "polyx-signin__panel-logo",
653
653
  children: props.logo
654
654
  }) : null, labels.tagline ? /* @__PURE__ */ jsx("p", {
@@ -667,6 +667,486 @@ function SignIn(props) {
667
667
  });
668
668
  }
669
669
  //#endregion
670
+ //#region src/components/_recovery-status.tsx
671
+ const ICONS = {
672
+ mail: /* @__PURE__ */ jsxs("svg", {
673
+ viewBox: "0 0 24 24",
674
+ fill: "none",
675
+ stroke: "currentColor",
676
+ strokeWidth: "1.75",
677
+ "aria-hidden": "true",
678
+ focusable: "false",
679
+ children: [/* @__PURE__ */ jsx("rect", {
680
+ x: "3",
681
+ y: "5",
682
+ width: "18",
683
+ height: "14",
684
+ rx: "2.5"
685
+ }), /* @__PURE__ */ jsx("path", {
686
+ d: "m4 7 8 6 8-6",
687
+ strokeLinecap: "round",
688
+ strokeLinejoin: "round"
689
+ })]
690
+ }),
691
+ check: /* @__PURE__ */ jsx("svg", {
692
+ viewBox: "0 0 24 24",
693
+ fill: "none",
694
+ stroke: "currentColor",
695
+ strokeWidth: "2",
696
+ "aria-hidden": "true",
697
+ focusable: "false",
698
+ children: /* @__PURE__ */ jsx("path", {
699
+ d: "m5 12.5 4.5 4.5L19 7",
700
+ strokeLinecap: "round",
701
+ strokeLinejoin: "round"
702
+ })
703
+ }),
704
+ warning: /* @__PURE__ */ jsxs("svg", {
705
+ viewBox: "0 0 24 24",
706
+ fill: "none",
707
+ stroke: "currentColor",
708
+ strokeWidth: "1.75",
709
+ "aria-hidden": "true",
710
+ focusable: "false",
711
+ children: [
712
+ /* @__PURE__ */ jsx("circle", {
713
+ cx: "12",
714
+ cy: "12",
715
+ r: "9"
716
+ }),
717
+ /* @__PURE__ */ jsx("path", {
718
+ d: "M12 7.5v5",
719
+ strokeLinecap: "round"
720
+ }),
721
+ /* @__PURE__ */ jsx("circle", {
722
+ cx: "12",
723
+ cy: "16",
724
+ r: "0.6",
725
+ fill: "currentColor",
726
+ stroke: "none"
727
+ })
728
+ ]
729
+ })
730
+ };
731
+ /**
732
+ * A centered, icon-topped confirmation card — the shape every recovery terminal
733
+ * state shares (link sent, password updated, link expired). Reuses the `<SignIn/>`
734
+ * class anatomy so it themes from the same `--polyx-*` custom properties.
735
+ */
736
+ function RecoveryStatus(props) {
737
+ return /* @__PURE__ */ jsxs("div", {
738
+ className: "polyx-signin__card",
739
+ children: [
740
+ /* @__PURE__ */ jsxs("div", {
741
+ className: "polyx-signin__header polyx-signin__header--center",
742
+ children: [
743
+ /* @__PURE__ */ jsx("span", {
744
+ className: `polyx-signin__status-icon polyx-signin__status-icon--${props.tone ?? "brand"}`,
745
+ children: ICONS[props.icon]
746
+ }),
747
+ /* @__PURE__ */ jsx("h1", {
748
+ className: "polyx-signin__heading",
749
+ children: props.heading
750
+ }),
751
+ /* @__PURE__ */ jsx("p", {
752
+ className: "polyx-signin__subheading",
753
+ children: props.children
754
+ })
755
+ ]
756
+ }),
757
+ props.actions ? /* @__PURE__ */ jsx("div", {
758
+ className: "polyx-signin__actions",
759
+ children: props.actions
760
+ }) : null,
761
+ props.footer
762
+ ]
763
+ });
764
+ }
765
+ //#endregion
766
+ //#region src/components/forgot-password.tsx
767
+ const DEFAULT_LABELS$1 = {
768
+ heading: "Reset your password",
769
+ subheading: "Enter your email and we'll send you a reset link.",
770
+ emailLabel: "Email",
771
+ emailPlaceholder: "you@company.com",
772
+ submit: "Send reset link",
773
+ submitting: "Sending…",
774
+ sentHeading: "Check your email",
775
+ sentSubheading: "If an account exists for {email}, a password reset link is on its way. It expires in 30 minutes.",
776
+ differentEmail: "Use a different email",
777
+ backToSignIn: "Back to sign in",
778
+ securedBy: "Secured by PolyX",
779
+ genericError: "Something went wrong. Please try again."
780
+ };
781
+ /** Splits a label on `{email}` and drops the entered address in, emphasized. */
782
+ function withEmail(template, email) {
783
+ return template.split("{email}").map((chunk, index, all) => /* @__PURE__ */ jsxs(Fragment, { children: [chunk, index < all.length - 1 ? /* @__PURE__ */ jsx("span", {
784
+ className: "polyx-signin__email",
785
+ children: email || "that address"
786
+ }) : null] }, index));
787
+ }
788
+ /**
789
+ * The self-service "forgot password" form (poly-x v5 / F040). Renders an email field
790
+ * inside the consuming app and POSTs to the SDK's BFF (`@poly-x/next` `forgotPassword`),
791
+ * which asks poly-auth to email a single-use reset link SERVER-SIDE. The response is
792
+ * always the same — no token, no account-existence signal — so after submit the form
793
+ * shows an identical "check your email" state whether or not the account exists.
794
+ *
795
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
796
+ * via the `--polyx-*` custom properties. Target `.polyx-forgot` for form-specific tweaks.
797
+ */
798
+ function ForgotPassword(props) {
799
+ const action = props.action ?? "/api/polyx/forgot-password";
800
+ const signInPath = props.signInPath ?? "/sign-in";
801
+ const labels = {
802
+ ...DEFAULT_LABELS$1,
803
+ ...props.labels
804
+ };
805
+ const [email, setEmail] = useState("");
806
+ const [submitting, setSubmitting] = useState(false);
807
+ const [sent, setSent] = useState(false);
808
+ const [error, setError] = useState(null);
809
+ async function onSubmit(event) {
810
+ event.preventDefault();
811
+ setSubmitting(true);
812
+ setError(null);
813
+ try {
814
+ if ((await (await fetch(action, {
815
+ method: "POST",
816
+ headers: { "content-type": "application/json" },
817
+ body: JSON.stringify({ email })
818
+ })).json().catch(() => ({}))).status === "accepted") {
819
+ setSent(true);
820
+ return;
821
+ }
822
+ setError(labels.genericError);
823
+ } catch {
824
+ setError(labels.genericError);
825
+ } finally {
826
+ setSubmitting(false);
827
+ }
828
+ }
829
+ const rootClass = [
830
+ "polyx-signin",
831
+ "polyx-forgot",
832
+ props.className
833
+ ].filter(Boolean).join(" ");
834
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
835
+ className: "polyx-signin__footer",
836
+ children: [/* @__PURE__ */ jsx("svg", {
837
+ className: "polyx-signin__shield",
838
+ viewBox: "0 0 16 16",
839
+ fill: "currentColor",
840
+ "aria-hidden": "true",
841
+ focusable: "false",
842
+ 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" })
843
+ }), labels.securedBy]
844
+ });
845
+ const header = (heading, subheading) => /* @__PURE__ */ jsxs("div", {
846
+ className: "polyx-signin__header",
847
+ children: [
848
+ props.logo ? /* @__PURE__ */ jsx("div", {
849
+ className: "polyx-signin__logo",
850
+ children: props.logo
851
+ }) : null,
852
+ /* @__PURE__ */ jsx("h1", {
853
+ className: "polyx-signin__heading",
854
+ children: heading
855
+ }),
856
+ /* @__PURE__ */ jsx("p", {
857
+ className: "polyx-signin__subheading",
858
+ children: subheading
859
+ })
860
+ ]
861
+ });
862
+ if (sent) return /* @__PURE__ */ jsx("div", {
863
+ className: rootClass,
864
+ "data-polyx-forgot": "sent",
865
+ children: /* @__PURE__ */ jsx(RecoveryStatus, {
866
+ icon: "mail",
867
+ heading: labels.sentHeading,
868
+ footer,
869
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("button", {
870
+ type: "button",
871
+ className: "polyx-signin__link",
872
+ onClick: () => setSent(false),
873
+ children: labels.differentEmail
874
+ }), /* @__PURE__ */ jsx("a", {
875
+ className: "polyx-signin__link",
876
+ href: signInPath,
877
+ children: labels.backToSignIn
878
+ })] }),
879
+ children: withEmail(labels.sentSubheading, email)
880
+ })
881
+ });
882
+ return /* @__PURE__ */ jsx("div", {
883
+ className: rootClass,
884
+ "data-polyx-forgot": "request",
885
+ children: /* @__PURE__ */ jsxs("div", {
886
+ className: "polyx-signin__card",
887
+ children: [
888
+ header(labels.heading, labels.subheading),
889
+ /* @__PURE__ */ jsxs("form", {
890
+ className: "polyx-signin__form",
891
+ onSubmit,
892
+ children: [
893
+ /* @__PURE__ */ jsxs("label", {
894
+ className: "polyx-signin__field",
895
+ children: [/* @__PURE__ */ jsx("span", {
896
+ className: "polyx-signin__label",
897
+ children: labels.emailLabel
898
+ }), /* @__PURE__ */ jsx("input", {
899
+ className: "polyx-signin__input",
900
+ type: "email",
901
+ name: "email",
902
+ autoComplete: "username",
903
+ placeholder: labels.emailPlaceholder,
904
+ required: true,
905
+ value: email,
906
+ disabled: submitting,
907
+ onChange: (event) => setEmail(event.target.value)
908
+ })]
909
+ }),
910
+ error ? /* @__PURE__ */ jsx("p", {
911
+ role: "alert",
912
+ className: "polyx-signin__error",
913
+ children: error
914
+ }) : null,
915
+ /* @__PURE__ */ jsx("button", {
916
+ type: "submit",
917
+ className: "polyx-signin__submit",
918
+ disabled: submitting,
919
+ children: submitting ? labels.submitting : labels.submit
920
+ })
921
+ ]
922
+ }),
923
+ footer
924
+ ]
925
+ })
926
+ });
927
+ }
928
+ //#endregion
929
+ //#region src/components/reset-password.tsx
930
+ const DEFAULT_LABELS = {
931
+ heading: "Choose a new password",
932
+ subheading: "Enter and confirm your new password.",
933
+ newPasswordLabel: "New password",
934
+ confirmPasswordLabel: "Confirm new password",
935
+ passwordPlaceholder: "••••••••",
936
+ submit: "Reset password",
937
+ submitting: "Resetting…",
938
+ successHeading: "Password updated",
939
+ successSubheading: "You can now sign in with your new password.",
940
+ invalidHeading: "This link is invalid or expired",
941
+ invalidSubheading: "Password reset links can be used once and expire quickly. Request a new one.",
942
+ requestNewLink: "Request a new link",
943
+ backToSignIn: "Back to sign in",
944
+ securedBy: "Secured by PolyX",
945
+ passwordMismatch: "Those passwords don't match.",
946
+ weakPassword: "Password must be at least 8 characters.",
947
+ genericError: "Something went wrong. Please try again."
948
+ };
949
+ /** Matches poly-auth's v5 reset floor (F040). */
950
+ const DEFAULT_MIN_LENGTH = 8;
951
+ /**
952
+ * The reset-completion form (poly-x v5 / F040). Reads the opaque token from the URL,
953
+ * collects a new password, and POSTs to the SDK's BFF (`@poly-x/next` `resetPassword`),
954
+ * which redeems the token single-use server-side. The token is never decoded in the
955
+ * browser — identity, validity, and expiry are all enforced by the backend.
956
+ *
957
+ * Reuses the `<SignIn/>` styling: `import "@poly-x/react/styles.css"` once, then theme
958
+ * via the `--polyx-*` custom properties. Target `.polyx-reset` for form-specific tweaks.
959
+ */
960
+ function ResetPassword(props) {
961
+ const action = props.action ?? "/api/polyx/reset-password";
962
+ const minLength = props.minLength ?? DEFAULT_MIN_LENGTH;
963
+ const signInPath = props.signInPath ?? "/sign-in";
964
+ const forgotPasswordPath = props.forgotPasswordPath ?? "/forgot-password";
965
+ const labels = {
966
+ ...DEFAULT_LABELS,
967
+ ...props.labels
968
+ };
969
+ const [token] = useState(() => props.token ?? (typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("token") ?? "" : ""));
970
+ const [newPassword, setNewPassword] = useState("");
971
+ const [confirmPassword, setConfirmPassword] = useState("");
972
+ const [submitting, setSubmitting] = useState(false);
973
+ const [error, setError] = useState(null);
974
+ const [step, setStep] = useState(token ? "form" : "invalid");
975
+ async function onSubmit(event) {
976
+ event.preventDefault();
977
+ if (newPassword !== confirmPassword) {
978
+ setError(labels.passwordMismatch);
979
+ return;
980
+ }
981
+ setSubmitting(true);
982
+ setError(null);
983
+ try {
984
+ switch ((await (await fetch(action, {
985
+ method: "POST",
986
+ headers: { "content-type": "application/json" },
987
+ body: JSON.stringify({
988
+ token,
989
+ newPassword,
990
+ confirmPassword
991
+ })
992
+ })).json().catch(() => ({}))).status) {
993
+ case "success":
994
+ setStep("success");
995
+ if (props.afterResetPath) window.location.assign(props.afterResetPath);
996
+ return;
997
+ case "weak_password":
998
+ setError(labels.weakPassword);
999
+ return;
1000
+ case "password_mismatch":
1001
+ setError(labels.passwordMismatch);
1002
+ return;
1003
+ case "invalid_or_expired":
1004
+ setStep("invalid");
1005
+ return;
1006
+ default:
1007
+ setError(labels.genericError);
1008
+ return;
1009
+ }
1010
+ } catch {
1011
+ setError(labels.genericError);
1012
+ } finally {
1013
+ setSubmitting(false);
1014
+ }
1015
+ }
1016
+ const rootClass = [
1017
+ "polyx-signin",
1018
+ "polyx-reset",
1019
+ props.className
1020
+ ].filter(Boolean).join(" ");
1021
+ const footer = props.showSecuredBy === false ? null : /* @__PURE__ */ jsxs("p", {
1022
+ className: "polyx-signin__footer",
1023
+ children: [/* @__PURE__ */ jsx("svg", {
1024
+ className: "polyx-signin__shield",
1025
+ viewBox: "0 0 16 16",
1026
+ fill: "currentColor",
1027
+ "aria-hidden": "true",
1028
+ focusable: "false",
1029
+ 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" })
1030
+ }), labels.securedBy]
1031
+ });
1032
+ const header = (heading, subheading) => /* @__PURE__ */ jsxs("div", {
1033
+ className: "polyx-signin__header",
1034
+ children: [
1035
+ props.logo ? /* @__PURE__ */ jsx("div", {
1036
+ className: "polyx-signin__logo",
1037
+ children: props.logo
1038
+ }) : null,
1039
+ /* @__PURE__ */ jsx("h1", {
1040
+ className: "polyx-signin__heading",
1041
+ children: heading
1042
+ }),
1043
+ /* @__PURE__ */ jsx("p", {
1044
+ className: "polyx-signin__subheading",
1045
+ children: subheading
1046
+ })
1047
+ ]
1048
+ });
1049
+ if (step === "success") return /* @__PURE__ */ jsx("div", {
1050
+ className: rootClass,
1051
+ "data-polyx-reset": "success",
1052
+ children: /* @__PURE__ */ jsx(RecoveryStatus, {
1053
+ icon: "check",
1054
+ heading: labels.successHeading,
1055
+ footer,
1056
+ actions: /* @__PURE__ */ jsx("a", {
1057
+ className: "polyx-signin__link",
1058
+ href: signInPath,
1059
+ children: labels.backToSignIn
1060
+ }),
1061
+ children: labels.successSubheading
1062
+ })
1063
+ });
1064
+ if (step === "invalid") return /* @__PURE__ */ jsx("div", {
1065
+ className: rootClass,
1066
+ "data-polyx-reset": "invalid",
1067
+ children: /* @__PURE__ */ jsx(RecoveryStatus, {
1068
+ icon: "warning",
1069
+ tone: "danger",
1070
+ heading: labels.invalidHeading,
1071
+ footer,
1072
+ actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("a", {
1073
+ className: "polyx-signin__link",
1074
+ href: forgotPasswordPath,
1075
+ children: labels.requestNewLink
1076
+ }), /* @__PURE__ */ jsx("a", {
1077
+ className: "polyx-signin__link",
1078
+ href: signInPath,
1079
+ children: labels.backToSignIn
1080
+ })] }),
1081
+ children: labels.invalidSubheading
1082
+ })
1083
+ });
1084
+ return /* @__PURE__ */ jsx("div", {
1085
+ className: rootClass,
1086
+ "data-polyx-reset": step,
1087
+ children: /* @__PURE__ */ jsxs("div", {
1088
+ className: "polyx-signin__card",
1089
+ children: [
1090
+ header(labels.heading, labels.subheading),
1091
+ /* @__PURE__ */ jsxs("form", {
1092
+ className: "polyx-signin__form",
1093
+ onSubmit,
1094
+ children: [
1095
+ /* @__PURE__ */ jsxs("label", {
1096
+ className: "polyx-signin__field",
1097
+ children: [/* @__PURE__ */ jsx("span", {
1098
+ className: "polyx-signin__label",
1099
+ children: labels.newPasswordLabel
1100
+ }), /* @__PURE__ */ jsx("input", {
1101
+ className: "polyx-signin__input",
1102
+ type: "password",
1103
+ name: "newPassword",
1104
+ autoComplete: "new-password",
1105
+ placeholder: labels.passwordPlaceholder,
1106
+ required: true,
1107
+ minLength,
1108
+ value: newPassword,
1109
+ disabled: submitting,
1110
+ onChange: (event) => setNewPassword(event.target.value)
1111
+ })]
1112
+ }),
1113
+ /* @__PURE__ */ jsxs("label", {
1114
+ className: "polyx-signin__field",
1115
+ children: [/* @__PURE__ */ jsx("span", {
1116
+ className: "polyx-signin__label",
1117
+ children: labels.confirmPasswordLabel
1118
+ }), /* @__PURE__ */ jsx("input", {
1119
+ className: "polyx-signin__input",
1120
+ type: "password",
1121
+ name: "confirmPassword",
1122
+ autoComplete: "new-password",
1123
+ placeholder: labels.passwordPlaceholder,
1124
+ required: true,
1125
+ minLength,
1126
+ value: confirmPassword,
1127
+ disabled: submitting,
1128
+ onChange: (event) => setConfirmPassword(event.target.value)
1129
+ })]
1130
+ }),
1131
+ error ? /* @__PURE__ */ jsx("p", {
1132
+ role: "alert",
1133
+ className: "polyx-signin__error",
1134
+ children: error
1135
+ }) : null,
1136
+ /* @__PURE__ */ jsx("button", {
1137
+ type: "submit",
1138
+ className: "polyx-signin__submit",
1139
+ disabled: submitting,
1140
+ children: submitting ? labels.submitting : labels.submit
1141
+ })
1142
+ ]
1143
+ }),
1144
+ footer
1145
+ ]
1146
+ })
1147
+ });
1148
+ }
1149
+ //#endregion
670
1150
  //#region src/components/auth-callback.tsx
671
1151
  /** Drop on your `/auth/callback` route: completes the sign-in exchange on mount. */
672
1152
  function AuthCallback({ children = null }) {
@@ -685,4 +1165,4 @@ function AuthCallback({ children = null }) {
685
1165
  const PACKAGE_NAME = "@poly-x/react";
686
1166
  const version = "0.0.0";
687
1167
  //#endregion
688
- export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, SessionStoragePkceStore, SignIn, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
1168
+ export { AuthCallback, BroadcastAuthChannel, BroadcastChannelSessionChannel, ForgotPassword, PACKAGE_NAME, PolyXContext, PolyXProvider, PopupCancelledError, Protected, ResetPassword, SessionStoragePkceStore, SignIn, WebLocksLock, WindowBrowserBridge, createLoginController, useAuth, useAuthCallback, useSession, useUser, version };
package/dist/styles.css CHANGED
@@ -339,6 +339,77 @@
339
339
  opacity: 0.85;
340
340
  }
341
341
 
342
+ /* --- Recovery confirmation states (ForgotPassword / ResetPassword) ------- */
343
+
344
+ .polyx-signin__header--center {
345
+ align-items: center;
346
+ text-align: center;
347
+ }
348
+
349
+ .polyx-signin__status-icon {
350
+ display: inline-flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ width: 3rem;
354
+ height: 3rem;
355
+ margin-bottom: 0.25rem;
356
+ border-radius: 999px;
357
+ }
358
+
359
+ .polyx-signin__status-icon svg {
360
+ width: 1.5rem;
361
+ height: 1.5rem;
362
+ }
363
+
364
+ .polyx-signin__status-icon--brand {
365
+ color: var(--polyx-brand);
366
+ background: color-mix(in srgb, var(--polyx-brand) 12%, transparent);
367
+ }
368
+
369
+ .polyx-signin__status-icon--danger {
370
+ color: var(--polyx-danger);
371
+ background: var(--polyx-danger-bg);
372
+ }
373
+
374
+ /* The entered email, emphasized inside the confirmation copy. */
375
+ .polyx-signin__email {
376
+ color: var(--polyx-fg);
377
+ font-weight: 600;
378
+ word-break: break-all;
379
+ }
380
+
381
+ /* Secondary text-link actions under a confirmation screen — quieter than the CTA. */
382
+ .polyx-signin__actions {
383
+ display: flex;
384
+ flex-direction: column;
385
+ align-items: center;
386
+ gap: 0.75rem;
387
+ margin-top: 1.5rem;
388
+ }
389
+
390
+ .polyx-signin__link {
391
+ padding: 0;
392
+ font: inherit;
393
+ font-size: 0.875rem;
394
+ font-weight: 500;
395
+ color: var(--polyx-brand);
396
+ background: none;
397
+ border: none;
398
+ cursor: pointer;
399
+ text-decoration: none;
400
+ }
401
+
402
+ .polyx-signin__link:hover {
403
+ color: var(--polyx-brand-hover);
404
+ text-decoration: underline;
405
+ }
406
+
407
+ .polyx-signin__link:focus-visible {
408
+ outline: none;
409
+ border-radius: 2px;
410
+ box-shadow: 0 0 0 3px var(--polyx-ring);
411
+ }
412
+
342
413
  /* --- Workspace picker ---------------------------------------------------- */
343
414
 
344
415
  .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.6",
3
+ "version": "0.1.0-alpha.7",
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.6"
30
+ "@poly-x/core": "0.1.0-alpha.7"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18",