@insforge/react 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/forms.mjs CHANGED
@@ -2,8 +2,8 @@ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
2
  import { clsx } from 'clsx';
3
3
  import { twMerge } from 'tailwind-merge';
4
4
  import { AlertTriangle, EyeOff, Eye, Loader2, CircleCheck, Check } from 'lucide-react';
5
- import { createContext, useState } from 'react';
6
- import '@insforge/sdk';
5
+ import { createContext, useState, useEffect, useContext, useRef } from 'react';
6
+ import { createClient } from '@insforge/sdk';
7
7
 
8
8
  function AuthBranding() {
9
9
  return /* @__PURE__ */ jsxs("div", { className: "bg-[#FAFAFA] px-2 py-4 flex flex-row justify-center items-center gap-1", children: [
@@ -643,9 +643,221 @@ function AuthOAuthProviders({
643
643
  provider
644
644
  )) });
645
645
  }
646
- createContext(
646
+ function AuthVerificationCodeInput({
647
+ length = 6,
648
+ value,
649
+ email,
650
+ onChange,
651
+ disabled = false,
652
+ onComplete,
653
+ appearance = {}
654
+ }) {
655
+ const inputRefs = useRef([]);
656
+ const handleChange = (index, digit) => {
657
+ if (digit.length > 1) return;
658
+ if (digit && !/^\d$/.test(digit)) return;
659
+ const newValue = value.split("");
660
+ newValue[index] = digit;
661
+ const updatedValue = newValue.join("");
662
+ onChange(updatedValue);
663
+ if (digit && index < length - 1) {
664
+ inputRefs.current[index + 1]?.focus();
665
+ }
666
+ if (digit && index === length - 1 && updatedValue.length === length && onComplete) {
667
+ onComplete(updatedValue);
668
+ }
669
+ };
670
+ const handleKeyDown = (index, e) => {
671
+ if (e.key === "Backspace") {
672
+ if (!value[index] && index > 0) {
673
+ inputRefs.current[index - 1]?.focus();
674
+ } else {
675
+ handleChange(index, "");
676
+ }
677
+ } else if (e.key === "ArrowLeft" && index > 0) {
678
+ inputRefs.current[index - 1]?.focus();
679
+ } else if (e.key === "ArrowRight" && index < length - 1) {
680
+ inputRefs.current[index + 1]?.focus();
681
+ }
682
+ };
683
+ const handlePaste = (e) => {
684
+ e.preventDefault();
685
+ const pastedData = e.clipboardData.getData("text/plain").trim();
686
+ if (/^\d+$/.test(pastedData) && pastedData.length === length) {
687
+ onChange(pastedData);
688
+ inputRefs.current[length - 1]?.focus();
689
+ if (onComplete) {
690
+ onComplete(pastedData);
691
+ }
692
+ }
693
+ };
694
+ return /* @__PURE__ */ jsxs("div", { className: cn(
695
+ "flex flex-col justify-center items-center gap-6",
696
+ appearance.containerClassName
697
+ ), children: [
698
+ /* @__PURE__ */ jsxs("p", { className: "text-sm font-normal text-[#525252] leading-5", children: [
699
+ "We've sent a verification code to your inbox at",
700
+ " ",
701
+ /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold text-black leading-5", children: email }),
702
+ ". Enter it below to proceed."
703
+ ] }),
704
+ /* @__PURE__ */ jsx("div", { className: "flex flex-row gap-3 justify-center items-center", children: Array.from({ length }).map((_, index) => /* @__PURE__ */ jsx(
705
+ "input",
706
+ {
707
+ ref: (el) => {
708
+ inputRefs.current[index] = el;
709
+ },
710
+ type: "text",
711
+ inputMode: "numeric",
712
+ maxLength: 1,
713
+ value: value[index] || "",
714
+ onChange: (e) => handleChange(index, e.target.value),
715
+ onKeyDown: (e) => handleKeyDown(index, e),
716
+ onPaste: handlePaste,
717
+ disabled,
718
+ className: cn(
719
+ "w-full h-12 px-3 py-2 rounded border border-[#E0E0E0] bg-white",
720
+ "text-center text-base font-semibold leading-5 text-black",
721
+ "transition-all duration-200 outline-none",
722
+ "focus:border-black focus:shadow-[0_0_0_2px_rgba(0,0,0,0.1)]",
723
+ "disabled:bg-[#F5F5F5] disabled:cursor-not-allowed disabled:opacity-60",
724
+ appearance.inputClassName
725
+ ),
726
+ autoComplete: "one-time-code"
727
+ },
728
+ index
729
+ )) })
730
+ ] });
731
+ }
732
+ var InsforgeContext = createContext(
647
733
  void 0
648
734
  );
735
+ function useInsforge() {
736
+ const context = useContext(InsforgeContext);
737
+ if (!context) {
738
+ throw new Error("useInsforge must be used within InsforgeProvider");
739
+ }
740
+ return context;
741
+ }
742
+ function AuthEmailVerificationStep({
743
+ email,
744
+ description,
745
+ method = "code",
746
+ onVerifyCode
747
+ }) {
748
+ const { baseUrl } = useInsforge();
749
+ const [insforge] = useState(() => createClient({ baseUrl }));
750
+ const [resendDisabled, setResendDisabled] = useState(true);
751
+ const [resendCountdown, setResendCountdown] = useState(60);
752
+ const [isSending, setIsSending] = useState(false);
753
+ const [verificationCode, setVerificationCode] = useState("");
754
+ const [isVerifying, setIsVerifying] = useState(false);
755
+ const [verificationError, setVerificationError] = useState("");
756
+ const defaultDescription = method === "code" ? "We've sent a 6-digit verification code to {email}. Please enter it below to verify your account. The code will expire in 10 minutes." : "We've sent a verification link to {email}. Please check your email and click the link to verify your account. The link will expire in 10 minutes.";
757
+ useEffect(() => {
758
+ const sendInitialEmail = async () => {
759
+ try {
760
+ if (method === "code") {
761
+ await insforge.auth.sendVerificationCode({ email });
762
+ } else {
763
+ await insforge.auth.sendVerificationLink({ email });
764
+ }
765
+ } catch {
766
+ }
767
+ };
768
+ void sendInitialEmail();
769
+ }, [email, method, insforge.auth]);
770
+ useEffect(() => {
771
+ if (resendCountdown > 0) {
772
+ const timer = setInterval(() => {
773
+ setResendCountdown((prev) => {
774
+ if (prev <= 1) {
775
+ setResendDisabled(false);
776
+ return 0;
777
+ }
778
+ return prev - 1;
779
+ });
780
+ }, 1e3);
781
+ return () => clearInterval(timer);
782
+ }
783
+ }, [resendCountdown]);
784
+ const handleResend = async () => {
785
+ setResendDisabled(true);
786
+ setResendCountdown(60);
787
+ setIsSending(true);
788
+ setVerificationError("");
789
+ try {
790
+ if (method === "code") {
791
+ await insforge.auth.sendVerificationCode({ email });
792
+ } else {
793
+ await insforge.auth.sendVerificationLink({ email });
794
+ }
795
+ } catch {
796
+ setResendDisabled(false);
797
+ setResendCountdown(0);
798
+ } finally {
799
+ setIsSending(false);
800
+ }
801
+ };
802
+ const handleVerifyCode = async (code) => {
803
+ if (!onVerifyCode) {
804
+ return;
805
+ }
806
+ setIsVerifying(true);
807
+ setVerificationError("");
808
+ try {
809
+ await onVerifyCode(code);
810
+ } catch (error) {
811
+ setVerificationError(
812
+ error instanceof Error ? error.message : "Invalid verification code. Please try again."
813
+ );
814
+ setVerificationCode("");
815
+ } finally {
816
+ setIsVerifying(false);
817
+ }
818
+ };
819
+ const displayDescription = description || defaultDescription;
820
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6 items-stretch", children: [
821
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-neutral-600 dark:text-neutral-400 leading-relaxed", children: displayDescription.split("{email}").map((part, index, array) => /* @__PURE__ */ jsxs("span", { children: [
822
+ part,
823
+ index < array.length - 1 && /* @__PURE__ */ jsx("span", { className: "font-medium text-black dark:text-white", children: email })
824
+ ] }, index)) }),
825
+ verificationError && /* @__PURE__ */ jsx("div", { className: "pl-3 py-2 pr-2 bg-red-50 border-2 border-red-600 rounded", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
826
+ /* @__PURE__ */ jsx("svg", { className: "w-6 h-6 text-red-500 shrink-0", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: "2", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" }) }),
827
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-red-600 flex-1", children: verificationError })
828
+ ] }) }),
829
+ method === "code" && /* @__PURE__ */ jsxs("div", { className: "w-full bg-neutral-100 dark:bg-neutral-800 rounded-lg px-4 pt-4 pb-6 flex flex-col gap-4", children: [
830
+ /* @__PURE__ */ jsx(
831
+ AuthVerificationCodeInput,
832
+ {
833
+ value: verificationCode,
834
+ onChange: setVerificationCode,
835
+ email,
836
+ disabled: isVerifying,
837
+ onComplete: (code) => {
838
+ void handleVerifyCode(code);
839
+ }
840
+ }
841
+ ),
842
+ isVerifying && /* @__PURE__ */ jsx("p", { className: "text-sm text-neutral-600 dark:text-neutral-400 text-center", children: "Verifying..." })
843
+ ] }),
844
+ /* @__PURE__ */ jsxs("div", { className: "w-full text-sm text-center text-neutral-600 dark:text-neutral-400", children: [
845
+ "Didn't receive the email?",
846
+ " ",
847
+ /* @__PURE__ */ jsx(
848
+ "button",
849
+ {
850
+ onClick: () => {
851
+ void handleResend();
852
+ },
853
+ disabled: resendDisabled || isSending,
854
+ className: "text-black dark:text-white font-medium transition-colors disabled:cursor-not-allowed cursor-pointer hover:underline disabled:no-underline disabled:opacity-50",
855
+ children: isSending ? "Sending..." : resendDisabled ? `Retry in (${resendCountdown}s)` : "Click to resend"
856
+ }
857
+ )
858
+ ] })
859
+ ] });
860
+ }
649
861
  function SignInForm({
650
862
  email,
651
863
  password,
@@ -672,7 +884,11 @@ function SignInForm({
672
884
  signUpText = "Don't have an account?",
673
885
  signUpLinkText = "Sign Up Now",
674
886
  signUpUrl = "/sign-up",
675
- dividerText = "or"
887
+ dividerText = "or",
888
+ // Email verification step props
889
+ showVerificationStep = false,
890
+ onVerifyCode,
891
+ verificationDescription
676
892
  }) {
677
893
  return /* @__PURE__ */ jsxs(
678
894
  AuthContainer,
@@ -685,8 +901,8 @@ function SignInForm({
685
901
  /* @__PURE__ */ jsx(
686
902
  AuthHeader,
687
903
  {
688
- title,
689
- subtitle,
904
+ title: showVerificationStep ? "Verify Your Email" : title,
905
+ subtitle: showVerificationStep ? "" : subtitle,
690
906
  appearance: {
691
907
  containerClassName: appearance.header?.container,
692
908
  titleClassName: appearance.header?.title,
@@ -701,7 +917,14 @@ function SignInForm({
701
917
  className: appearance.errorBanner
702
918
  }
703
919
  ),
704
- /* @__PURE__ */ jsxs(
920
+ showVerificationStep ? /* @__PURE__ */ jsx(
921
+ AuthEmailVerificationStep,
922
+ {
923
+ email,
924
+ description: verificationDescription,
925
+ onVerifyCode
926
+ }
927
+ ) : /* @__PURE__ */ jsxs(
705
928
  "form",
706
929
  {
707
930
  onSubmit,
@@ -760,39 +983,41 @@ function SignInForm({
760
983
  ]
761
984
  }
762
985
  ),
763
- /* @__PURE__ */ jsx(
764
- AuthLink,
765
- {
766
- text: signUpText,
767
- linkText: signUpLinkText,
768
- href: signUpUrl,
769
- appearance: {
770
- containerClassName: appearance.link?.container,
771
- linkClassName: appearance.link?.link
772
- }
773
- }
774
- ),
775
- availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxs(Fragment, { children: [
776
- /* @__PURE__ */ jsx(
777
- AuthDivider,
778
- {
779
- text: dividerText,
780
- className: appearance.divider
781
- }
782
- ),
986
+ !showVerificationStep && /* @__PURE__ */ jsxs(Fragment, { children: [
783
987
  /* @__PURE__ */ jsx(
784
- AuthOAuthProviders,
988
+ AuthLink,
785
989
  {
786
- providers: availableProviders,
787
- onClick: onOAuthClick,
788
- disabled: loading || oauthLoading !== null,
789
- loading: oauthLoading,
990
+ text: signUpText,
991
+ linkText: signUpLinkText,
992
+ href: signUpUrl,
790
993
  appearance: {
791
- containerClassName: appearance.oauth?.container,
792
- buttonClassName: appearance.oauth?.button
994
+ containerClassName: appearance.link?.container,
995
+ linkClassName: appearance.link?.link
793
996
  }
794
997
  }
795
- )
998
+ ),
999
+ availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxs(Fragment, { children: [
1000
+ /* @__PURE__ */ jsx(
1001
+ AuthDivider,
1002
+ {
1003
+ text: dividerText,
1004
+ className: appearance.divider
1005
+ }
1006
+ ),
1007
+ /* @__PURE__ */ jsx(
1008
+ AuthOAuthProviders,
1009
+ {
1010
+ providers: availableProviders,
1011
+ onClick: onOAuthClick,
1012
+ disabled: loading || oauthLoading !== null,
1013
+ loading: oauthLoading,
1014
+ appearance: {
1015
+ containerClassName: appearance.oauth?.container,
1016
+ buttonClassName: appearance.oauth?.button
1017
+ }
1018
+ }
1019
+ )
1020
+ ] })
796
1021
  ] })
797
1022
  ]
798
1023
  }
@@ -822,7 +1047,11 @@ function SignUpForm({
822
1047
  signInText = "Already have an account?",
823
1048
  signInLinkText = "Login Now",
824
1049
  signInUrl = "/sign-in",
825
- dividerText = "or"
1050
+ dividerText = "or",
1051
+ // Email verification step props
1052
+ showVerificationStep = false,
1053
+ onVerifyCode,
1054
+ verificationDescription
826
1055
  }) {
827
1056
  return /* @__PURE__ */ jsxs(
828
1057
  AuthContainer,
@@ -835,8 +1064,8 @@ function SignUpForm({
835
1064
  /* @__PURE__ */ jsx(
836
1065
  AuthHeader,
837
1066
  {
838
- title,
839
- subtitle,
1067
+ title: showVerificationStep ? "Verify Your Email" : title,
1068
+ subtitle: showVerificationStep ? "" : subtitle,
840
1069
  appearance: {
841
1070
  containerClassName: appearance.header?.container,
842
1071
  titleClassName: appearance.header?.title,
@@ -851,7 +1080,14 @@ function SignUpForm({
851
1080
  className: appearance.errorBanner
852
1081
  }
853
1082
  ),
854
- /* @__PURE__ */ jsxs(
1083
+ showVerificationStep ? /* @__PURE__ */ jsx(
1084
+ AuthEmailVerificationStep,
1085
+ {
1086
+ email,
1087
+ description: verificationDescription,
1088
+ onVerifyCode
1089
+ }
1090
+ ) : /* @__PURE__ */ jsxs(
855
1091
  "form",
856
1092
  {
857
1093
  onSubmit,
@@ -908,39 +1144,41 @@ function SignUpForm({
908
1144
  ]
909
1145
  }
910
1146
  ),
911
- /* @__PURE__ */ jsx(
912
- AuthLink,
913
- {
914
- text: signInText,
915
- linkText: signInLinkText,
916
- href: signInUrl,
917
- appearance: {
918
- containerClassName: appearance.link?.container,
919
- linkClassName: appearance.link?.link
920
- }
921
- }
922
- ),
923
- availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxs(Fragment, { children: [
1147
+ !showVerificationStep && /* @__PURE__ */ jsxs(Fragment, { children: [
924
1148
  /* @__PURE__ */ jsx(
925
- AuthDivider,
1149
+ AuthLink,
926
1150
  {
927
- text: dividerText,
928
- className: appearance.divider
929
- }
930
- ),
931
- /* @__PURE__ */ jsx(
932
- AuthOAuthProviders,
933
- {
934
- providers: availableProviders,
935
- onClick: onOAuthClick,
936
- disabled: loading || oauthLoading !== null,
937
- loading: oauthLoading,
1151
+ text: signInText,
1152
+ linkText: signInLinkText,
1153
+ href: signInUrl,
938
1154
  appearance: {
939
- containerClassName: appearance.oauth?.container,
940
- buttonClassName: appearance.oauth?.button
1155
+ containerClassName: appearance.link?.container,
1156
+ linkClassName: appearance.link?.link
941
1157
  }
942
1158
  }
943
- )
1159
+ ),
1160
+ availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxs(Fragment, { children: [
1161
+ /* @__PURE__ */ jsx(
1162
+ AuthDivider,
1163
+ {
1164
+ text: dividerText,
1165
+ className: appearance.divider
1166
+ }
1167
+ ),
1168
+ /* @__PURE__ */ jsx(
1169
+ AuthOAuthProviders,
1170
+ {
1171
+ providers: availableProviders,
1172
+ onClick: onOAuthClick,
1173
+ disabled: loading || oauthLoading !== null,
1174
+ loading: oauthLoading,
1175
+ appearance: {
1176
+ containerClassName: appearance.oauth?.container,
1177
+ buttonClassName: appearance.oauth?.button
1178
+ }
1179
+ }
1180
+ )
1181
+ ] })
944
1182
  ] })
945
1183
  ]
946
1184
  }
@@ -1166,13 +1404,18 @@ function ResetPasswordForm({
1166
1404
  ]
1167
1405
  }
1168
1406
  ),
1169
- /* @__PURE__ */ jsxs("p", { className: appearance.backToSignIn || "text-center text-sm text-gray-600 dark:text-gray-400", children: [
1170
- backToSignInText && /* @__PURE__ */ jsxs("span", { children: [
1171
- backToSignInText,
1172
- " "
1173
- ] }),
1174
- /* @__PURE__ */ jsx("a", { href: backToSignInUrl, className: "text-black dark:text-white font-medium", children: "Back to Sign In" })
1175
- ] })
1407
+ /* @__PURE__ */ jsx(
1408
+ AuthLink,
1409
+ {
1410
+ text: backToSignInText,
1411
+ linkText: "Back to Sign In",
1412
+ href: backToSignInUrl,
1413
+ appearance: {
1414
+ containerClassName: appearance.link?.container,
1415
+ linkClassName: appearance.link?.link
1416
+ }
1417
+ }
1418
+ )
1176
1419
  ]
1177
1420
  }
1178
1421
  );