@insforge/react 0.2.10 → 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,7 +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 { useState } from 'react';
5
+ import { createContext, useState, useEffect, useContext, useRef } from 'react';
6
+ import { createClient } from '@insforge/sdk';
6
7
 
7
8
  function AuthBranding() {
8
9
  return /* @__PURE__ */ jsxs("div", { className: "bg-[#FAFAFA] px-2 py-4 flex flex-row justify-center items-center gap-1", children: [
@@ -642,6 +643,221 @@ function AuthOAuthProviders({
642
643
  provider
643
644
  )) });
644
645
  }
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(
733
+ void 0
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
+ }
645
861
  function SignInForm({
646
862
  email,
647
863
  password,
@@ -668,7 +884,11 @@ function SignInForm({
668
884
  signUpText = "Don't have an account?",
669
885
  signUpLinkText = "Sign Up Now",
670
886
  signUpUrl = "/sign-up",
671
- dividerText = "or"
887
+ dividerText = "or",
888
+ // Email verification step props
889
+ showVerificationStep = false,
890
+ onVerifyCode,
891
+ verificationDescription
672
892
  }) {
673
893
  return /* @__PURE__ */ jsxs(
674
894
  AuthContainer,
@@ -681,8 +901,8 @@ function SignInForm({
681
901
  /* @__PURE__ */ jsx(
682
902
  AuthHeader,
683
903
  {
684
- title,
685
- subtitle,
904
+ title: showVerificationStep ? "Verify Your Email" : title,
905
+ subtitle: showVerificationStep ? "" : subtitle,
686
906
  appearance: {
687
907
  containerClassName: appearance.header?.container,
688
908
  titleClassName: appearance.header?.title,
@@ -697,7 +917,14 @@ function SignInForm({
697
917
  className: appearance.errorBanner
698
918
  }
699
919
  ),
700
- /* @__PURE__ */ jsxs(
920
+ showVerificationStep ? /* @__PURE__ */ jsx(
921
+ AuthEmailVerificationStep,
922
+ {
923
+ email,
924
+ description: verificationDescription,
925
+ onVerifyCode
926
+ }
927
+ ) : /* @__PURE__ */ jsxs(
701
928
  "form",
702
929
  {
703
930
  onSubmit,
@@ -756,39 +983,41 @@ function SignInForm({
756
983
  ]
757
984
  }
758
985
  ),
759
- /* @__PURE__ */ jsx(
760
- AuthLink,
761
- {
762
- text: signUpText,
763
- linkText: signUpLinkText,
764
- href: signUpUrl,
765
- appearance: {
766
- containerClassName: appearance.link?.container,
767
- linkClassName: appearance.link?.link
768
- }
769
- }
770
- ),
771
- availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxs(Fragment, { children: [
772
- /* @__PURE__ */ jsx(
773
- AuthDivider,
774
- {
775
- text: dividerText,
776
- className: appearance.divider
777
- }
778
- ),
986
+ !showVerificationStep && /* @__PURE__ */ jsxs(Fragment, { children: [
779
987
  /* @__PURE__ */ jsx(
780
- AuthOAuthProviders,
988
+ AuthLink,
781
989
  {
782
- providers: availableProviders,
783
- onClick: onOAuthClick,
784
- disabled: loading || oauthLoading !== null,
785
- loading: oauthLoading,
990
+ text: signUpText,
991
+ linkText: signUpLinkText,
992
+ href: signUpUrl,
786
993
  appearance: {
787
- containerClassName: appearance.oauth?.container,
788
- buttonClassName: appearance.oauth?.button
994
+ containerClassName: appearance.link?.container,
995
+ linkClassName: appearance.link?.link
789
996
  }
790
997
  }
791
- )
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
+ ] })
792
1021
  ] })
793
1022
  ]
794
1023
  }
@@ -818,7 +1047,11 @@ function SignUpForm({
818
1047
  signInText = "Already have an account?",
819
1048
  signInLinkText = "Login Now",
820
1049
  signInUrl = "/sign-in",
821
- dividerText = "or"
1050
+ dividerText = "or",
1051
+ // Email verification step props
1052
+ showVerificationStep = false,
1053
+ onVerifyCode,
1054
+ verificationDescription
822
1055
  }) {
823
1056
  return /* @__PURE__ */ jsxs(
824
1057
  AuthContainer,
@@ -831,8 +1064,8 @@ function SignUpForm({
831
1064
  /* @__PURE__ */ jsx(
832
1065
  AuthHeader,
833
1066
  {
834
- title,
835
- subtitle,
1067
+ title: showVerificationStep ? "Verify Your Email" : title,
1068
+ subtitle: showVerificationStep ? "" : subtitle,
836
1069
  appearance: {
837
1070
  containerClassName: appearance.header?.container,
838
1071
  titleClassName: appearance.header?.title,
@@ -847,7 +1080,14 @@ function SignUpForm({
847
1080
  className: appearance.errorBanner
848
1081
  }
849
1082
  ),
850
- /* @__PURE__ */ jsxs(
1083
+ showVerificationStep ? /* @__PURE__ */ jsx(
1084
+ AuthEmailVerificationStep,
1085
+ {
1086
+ email,
1087
+ description: verificationDescription,
1088
+ onVerifyCode
1089
+ }
1090
+ ) : /* @__PURE__ */ jsxs(
851
1091
  "form",
852
1092
  {
853
1093
  onSubmit,
@@ -904,39 +1144,41 @@ function SignUpForm({
904
1144
  ]
905
1145
  }
906
1146
  ),
907
- /* @__PURE__ */ jsx(
908
- AuthLink,
909
- {
910
- text: signInText,
911
- linkText: signInLinkText,
912
- href: signInUrl,
913
- appearance: {
914
- containerClassName: appearance.link?.container,
915
- linkClassName: appearance.link?.link
916
- }
917
- }
918
- ),
919
- availableProviders.length > 0 && onOAuthClick && /* @__PURE__ */ jsxs(Fragment, { children: [
1147
+ !showVerificationStep && /* @__PURE__ */ jsxs(Fragment, { children: [
920
1148
  /* @__PURE__ */ jsx(
921
- AuthDivider,
1149
+ AuthLink,
922
1150
  {
923
- text: dividerText,
924
- className: appearance.divider
925
- }
926
- ),
927
- /* @__PURE__ */ jsx(
928
- AuthOAuthProviders,
929
- {
930
- providers: availableProviders,
931
- onClick: onOAuthClick,
932
- disabled: loading || oauthLoading !== null,
933
- loading: oauthLoading,
1151
+ text: signInText,
1152
+ linkText: signInLinkText,
1153
+ href: signInUrl,
934
1154
  appearance: {
935
- containerClassName: appearance.oauth?.container,
936
- buttonClassName: appearance.oauth?.button
1155
+ containerClassName: appearance.link?.container,
1156
+ linkClassName: appearance.link?.link
937
1157
  }
938
1158
  }
939
- )
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
+ ] })
940
1182
  ] })
941
1183
  ]
942
1184
  }
@@ -1162,13 +1404,18 @@ function ResetPasswordForm({
1162
1404
  ]
1163
1405
  }
1164
1406
  ),
1165
- /* @__PURE__ */ jsxs("p", { className: appearance.backToSignIn || "text-center text-sm text-gray-600 dark:text-gray-400", children: [
1166
- backToSignInText && /* @__PURE__ */ jsxs("span", { children: [
1167
- backToSignInText,
1168
- " "
1169
- ] }),
1170
- /* @__PURE__ */ jsx("a", { href: backToSignInUrl, className: "text-black dark:text-white font-medium", children: "Back to Sign In" })
1171
- ] })
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
+ )
1172
1419
  ]
1173
1420
  }
1174
1421
  );