@tern-secure/nextjs 3.4.3 → 4.1.0

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.
Files changed (96) hide show
  1. package/dist/cjs/app-router/client/TernSecureProvider.js +17 -2
  2. package/dist/cjs/app-router/client/TernSecureProvider.js.map +1 -1
  3. package/dist/cjs/app-router/client/actions.js +55 -55
  4. package/dist/cjs/app-router/client/actions.js.map +1 -1
  5. package/dist/cjs/app-router/route-handler/internal-route.js +22 -3
  6. package/dist/cjs/app-router/route-handler/internal-route.js.map +1 -1
  7. package/dist/cjs/boundary/TernSecureClientProvider.js +167 -34
  8. package/dist/cjs/boundary/TernSecureClientProvider.js.map +1 -1
  9. package/dist/cjs/boundary/TernSecureCtx.js.map +1 -1
  10. package/dist/cjs/boundary/hooks/useAuth.js +15 -2
  11. package/dist/cjs/boundary/hooks/useAuth.js.map +1 -1
  12. package/dist/cjs/components/sign-in.js +158 -35
  13. package/dist/cjs/components/sign-in.js.map +1 -1
  14. package/dist/cjs/components/sign-out-button.js +84 -0
  15. package/dist/cjs/components/sign-out-button.js.map +1 -0
  16. package/dist/cjs/components/sign-out.js +39 -9
  17. package/dist/cjs/components/sign-out.js.map +1 -1
  18. package/dist/cjs/components/sign-up.js +10 -5
  19. package/dist/cjs/components/sign-up.js.map +1 -1
  20. package/dist/cjs/errors.js +233 -5
  21. package/dist/cjs/errors.js.map +1 -1
  22. package/dist/cjs/index.js +3 -3
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/types.js +14 -0
  25. package/dist/cjs/types.js.map +1 -1
  26. package/dist/cjs/utils/construct.js +50 -18
  27. package/dist/cjs/utils/construct.js.map +1 -1
  28. package/dist/cjs/utils/redirect.js +57 -0
  29. package/dist/cjs/utils/redirect.js.map +1 -0
  30. package/dist/esm/app-router/client/TernSecureProvider.js +17 -2
  31. package/dist/esm/app-router/client/TernSecureProvider.js.map +1 -1
  32. package/dist/esm/app-router/client/actions.js +64 -56
  33. package/dist/esm/app-router/client/actions.js.map +1 -1
  34. package/dist/esm/app-router/route-handler/internal-route.js +18 -2
  35. package/dist/esm/app-router/route-handler/internal-route.js.map +1 -1
  36. package/dist/esm/boundary/TernSecureClientProvider.js +168 -35
  37. package/dist/esm/boundary/TernSecureClientProvider.js.map +1 -1
  38. package/dist/esm/boundary/TernSecureCtx.js.map +1 -1
  39. package/dist/esm/boundary/hooks/useAuth.js +15 -2
  40. package/dist/esm/boundary/hooks/useAuth.js.map +1 -1
  41. package/dist/esm/components/sign-in.js +160 -37
  42. package/dist/esm/components/sign-in.js.map +1 -1
  43. package/dist/esm/components/sign-out-button.js +60 -0
  44. package/dist/esm/components/sign-out-button.js.map +1 -0
  45. package/dist/esm/components/sign-out.js +30 -10
  46. package/dist/esm/components/sign-out.js.map +1 -1
  47. package/dist/esm/components/sign-up.js +10 -5
  48. package/dist/esm/components/sign-up.js.map +1 -1
  49. package/dist/esm/errors.js +229 -4
  50. package/dist/esm/errors.js.map +1 -1
  51. package/dist/esm/index.js +2 -2
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/types.js +6 -0
  54. package/dist/esm/types.js.map +1 -1
  55. package/dist/esm/utils/construct.js +46 -17
  56. package/dist/esm/utils/construct.js.map +1 -1
  57. package/dist/esm/utils/redirect.js +32 -0
  58. package/dist/esm/utils/redirect.js.map +1 -0
  59. package/dist/types/app-router/client/TernSecureProvider.d.ts +14 -3
  60. package/dist/types/app-router/client/TernSecureProvider.d.ts.map +1 -1
  61. package/dist/types/app-router/client/actions.d.ts +24 -18
  62. package/dist/types/app-router/client/actions.d.ts.map +1 -1
  63. package/dist/types/app-router/route-handler/internal-route.d.ts +8 -1
  64. package/dist/types/app-router/route-handler/internal-route.d.ts.map +1 -1
  65. package/dist/types/boundary/TernSecureClientProvider.d.ts +17 -1
  66. package/dist/types/boundary/TernSecureClientProvider.d.ts.map +1 -1
  67. package/dist/types/boundary/TernSecureCtx.d.ts +3 -8
  68. package/dist/types/boundary/TernSecureCtx.d.ts.map +1 -1
  69. package/dist/types/boundary/hooks/useAuth.d.ts +6 -1
  70. package/dist/types/boundary/hooks/useAuth.d.ts.map +1 -1
  71. package/dist/types/components/sign-in.d.ts.map +1 -1
  72. package/dist/types/components/sign-out-button.d.ts +14 -0
  73. package/dist/types/components/sign-out-button.d.ts.map +1 -0
  74. package/dist/types/components/sign-out.d.ts +7 -5
  75. package/dist/types/components/sign-out.d.ts.map +1 -1
  76. package/dist/types/components/sign-up.d.ts +4 -0
  77. package/dist/types/components/sign-up.d.ts.map +1 -1
  78. package/dist/types/components/ui/alert.d.ts +1 -1
  79. package/dist/types/components/ui/button.d.ts +1 -1
  80. package/dist/types/errors.d.ts +37 -2
  81. package/dist/types/errors.d.ts.map +1 -1
  82. package/dist/types/index.d.ts +1 -1
  83. package/dist/types/index.d.ts.map +1 -1
  84. package/dist/types/types.d.ts +45 -0
  85. package/dist/types/types.d.ts.map +1 -1
  86. package/dist/types/utils/construct.d.ts +20 -4
  87. package/dist/types/utils/construct.d.ts.map +1 -1
  88. package/dist/types/utils/redirect.d.ts +9 -0
  89. package/dist/types/utils/redirect.d.ts.map +1 -0
  90. package/package.json +6 -6
  91. package/dist/cjs/boundary/hooks/useUser.js +0 -44
  92. package/dist/cjs/boundary/hooks/useUser.js.map +0 -1
  93. package/dist/esm/boundary/hooks/useUser.js +0 -20
  94. package/dist/esm/boundary/hooks/useUser.js.map +0 -1
  95. package/dist/types/boundary/hooks/useUser.d.ts +0 -7
  96. package/dist/types/boundary/hooks/useUser.d.ts.map +0 -1
@@ -4,23 +4,80 @@ import { useState, useEffect, useMemo, useCallback } from "react";
4
4
  import { ternSecureAuth } from "../utils/client-init";
5
5
  import { onAuthStateChanged } from "firebase/auth";
6
6
  import { TernSecureCtx } from "./TernSecureCtx";
7
- import { useRouter } from "next/navigation";
7
+ import { useRouter, usePathname } from "next/navigation";
8
+ import { isBaseAuthRoute, isInternalRoute, isAuthRoute } from "../app-router/route-handler/internal-route";
9
+ import { hasRedirectLoop } from "../utils/construct";
8
10
  function TernSecureClientProvider({
9
11
  children,
10
- loginPath = "/sign-in",
11
- loadingComponent
12
+ loginPath = process.env.NEXT_PUBLIC_SIGN_IN_PATH || "/sign-in",
13
+ signUpPath = process.env.NEXT_PUBLIC_SIGN_UP_PATH || "/sign-up",
14
+ loadingComponent,
15
+ requiresVerification
12
16
  }) {
13
17
  const auth = useMemo(() => ternSecureAuth, []);
14
18
  const router = useRouter();
19
+ const pathname = usePathname();
20
+ const [isRedirecting, setIsRedirecting] = useState(false);
15
21
  const [authState, setAuthState] = useState(() => ({
16
22
  userId: null,
17
23
  isLoaded: false,
18
24
  error: null,
19
25
  isValid: false,
26
+ isVerified: false,
27
+ isAuthenticated: false,
20
28
  token: null,
21
- email: null
29
+ email: null,
30
+ status: "loading",
31
+ requiresVerification
22
32
  }));
33
+ const constructUrlWithRedirect = useCallback(
34
+ (loginPath2, currentPath, loginPathParam, signUpPathParam) => {
35
+ const baseUrl = window.location.origin;
36
+ const signInUrl = new URL(loginPath2, baseUrl);
37
+ if (!currentPath.includes(loginPathParam) && !currentPath.includes(signUpPathParam)) {
38
+ signInUrl.searchParams.set("redirect", currentPath);
39
+ }
40
+ return signInUrl.toString();
41
+ },
42
+ []
43
+ );
44
+ const shouldRedirect = useCallback(
45
+ (pathname2, isVerified) => {
46
+ const searchParams = new URLSearchParams(window.location.search);
47
+ if (isBaseAuthRoute(pathname2) && !searchParams.has("redirect")) {
48
+ return false;
49
+ }
50
+ if (isInternalRoute(pathname2)) {
51
+ return false;
52
+ }
53
+ if (isAuthRoute(pathname2) && (!requiresVerification || isVerified)) {
54
+ return false;
55
+ }
56
+ return true;
57
+ },
58
+ [requiresVerification]
59
+ );
60
+ const redirectToLogin = useCallback(
61
+ (currentPath) => {
62
+ const path = currentPath || pathname || "/";
63
+ if (isInternalRoute(path)) {
64
+ return;
65
+ }
66
+ if (hasRedirectLoop(path, loginPath)) {
67
+ return;
68
+ }
69
+ setIsRedirecting(true);
70
+ const loginUrl = constructUrlWithRedirect(loginPath, path, loginPath, signUpPath);
71
+ if (process.env.NODE_ENV === "production") {
72
+ window.location.href = loginUrl;
73
+ } else {
74
+ router.push(loginUrl);
75
+ }
76
+ },
77
+ [router, loginPath, signUpPath, pathname, constructUrlWithRedirect]
78
+ );
23
79
  const handleSignOut = useCallback(async (error) => {
80
+ const currentPath = window.location.pathname;
24
81
  await auth.signOut();
25
82
  setAuthState({
26
83
  isLoaded: true,
@@ -28,50 +85,126 @@ function TernSecureClientProvider({
28
85
  error: error || null,
29
86
  isValid: false,
30
87
  token: null,
31
- email: null
88
+ email: null,
89
+ isVerified: false,
90
+ isAuthenticated: false,
91
+ status: "unauthenticated",
92
+ requiresVerification
32
93
  });
33
- router.push(loginPath);
34
- }, [auth, router, loginPath]);
94
+ redirectToLogin(currentPath);
95
+ }, [auth, redirectToLogin, requiresVerification]);
35
96
  const setEmail = useCallback((email) => {
36
97
  setAuthState((prev) => ({
37
98
  ...prev,
38
99
  email
39
100
  }));
40
101
  }, []);
102
+ const getAuthError = useCallback(() => {
103
+ if (authState.error) {
104
+ const error = authState.error;
105
+ return {
106
+ success: false,
107
+ message: error.message,
108
+ error: error.code,
109
+ user: null
110
+ };
111
+ }
112
+ if (authState.requiresVerification && authState.isValid && !authState.isVerified) {
113
+ return {
114
+ success: false,
115
+ message: "Email verification required",
116
+ error: "EMAIL_NOT_VERIFIED",
117
+ user: null
118
+ };
119
+ }
120
+ if (!authState.isAuthenticated && authState.status !== "loading") {
121
+ return {
122
+ success: false,
123
+ message: "User is not authenticated",
124
+ error: "AUTHENTICATED",
125
+ user: null
126
+ };
127
+ }
128
+ return {
129
+ success: true,
130
+ user: ternSecureAuth.currentUser
131
+ };
132
+ }, [
133
+ authState.error,
134
+ authState.isValid,
135
+ authState.isVerified,
136
+ authState.isAuthenticated,
137
+ authState.status,
138
+ authState.requiresVerification
139
+ ]);
41
140
  useEffect(() => {
42
- const unsubscribe = onAuthStateChanged(auth, async (user) => {
43
- if (user) {
44
- setAuthState({
45
- isLoaded: true,
46
- userId: user.uid,
47
- isValid: true,
48
- token: user.getIdToken(),
49
- error: null,
50
- email: user.email
51
- });
52
- } else {
53
- setAuthState({
54
- isLoaded: true,
55
- userId: null,
56
- isValid: false,
57
- token: null,
58
- error: new Error("User is not authenticated"),
59
- email: null
60
- });
61
- if (!window.location.pathname.includes("/sign-up")) {
62
- router.push(loginPath);
141
+ let mounted = true;
142
+ let initialLoad = true;
143
+ const unsubscribe = onAuthStateChanged(
144
+ auth,
145
+ async (user) => {
146
+ if (!mounted) return;
147
+ try {
148
+ if (user) {
149
+ const isValid = !!user.uid;
150
+ const isVerified = user.emailVerified;
151
+ const isAuthenticated = isValid && (!requiresVerification || isVerified);
152
+ setAuthState({
153
+ isLoaded: true,
154
+ userId: user.uid,
155
+ isValid,
156
+ isVerified,
157
+ isAuthenticated: isValid && isVerified,
158
+ token: user.getIdToken(),
159
+ error: null,
160
+ email: user.email,
161
+ status: isAuthenticated ? "authenticated" : "unverified",
162
+ requiresVerification
163
+ });
164
+ if (requiresVerification && !isVerified && shouldRedirect(pathname || "", isVerified)) {
165
+ if (initialLoad || !isRedirecting) {
166
+ redirectToLogin(pathname);
167
+ }
168
+ }
169
+ } else {
170
+ setAuthState({
171
+ isLoaded: true,
172
+ userId: null,
173
+ isValid: false,
174
+ isVerified: false,
175
+ isAuthenticated: false,
176
+ token: null,
177
+ error: null,
178
+ email: null,
179
+ status: "unauthenticated",
180
+ requiresVerification
181
+ });
182
+ if (shouldRedirect(pathname || "", false) && initialLoad) {
183
+ redirectToLogin();
184
+ }
185
+ }
186
+ } catch (error) {
187
+ console.error("Auth state change error:", error);
188
+ if (mounted) {
189
+ handleSignOut(error instanceof Error ? error : new Error("Authentication error occurred"));
190
+ }
191
+ } finally {
192
+ initialLoad = false;
63
193
  }
64
194
  }
65
- }, (error) => {
66
- handleSignOut(error instanceof Error ? error : new Error("Authentication error occurred"));
67
- });
68
- return () => unsubscribe();
69
- }, [auth, handleSignOut, router, loginPath]);
195
+ );
196
+ return () => {
197
+ mounted = false;
198
+ unsubscribe();
199
+ };
200
+ }, [auth, handleSignOut, redirectToLogin, requiresVerification, pathname, isRedirecting, shouldRedirect]);
70
201
  const contextValue = useMemo(() => ({
71
202
  ...authState,
72
203
  signOut: handleSignOut,
73
- setEmail
74
- }), [authState, auth, handleSignOut, setEmail]);
204
+ setEmail,
205
+ getAuthError,
206
+ redirectToLogin
207
+ }), [authState, handleSignOut, setEmail, getAuthError, redirectToLogin]);
75
208
  if (!authState.isLoaded) {
76
209
  return /* @__PURE__ */ jsx(TernSecureCtx.Provider, { value: contextValue, children: loadingComponent || /* @__PURE__ */ jsx("div", { "aria-live": "polite", "aria-busy": "true", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Loading authentication state..." }) }) });
77
210
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/boundary/TernSecureClientProvider.tsx"],"sourcesContent":["\"use client\"\n\nimport React, { useState, useEffect, useMemo, useCallback } from 'react'\nimport { ternSecureAuth } from '../utils/client-init'\nimport { onAuthStateChanged, User } from \"firebase/auth\"\nimport { TernSecureCtx, TernSecureCtxValue, TernSecureState } from './TernSecureCtx'\nimport { useRouter } from 'next/navigation'\n\ninterface TernSecureClientProviderProps {\n children: React.ReactNode;\n onUserChanged?: (user: User | null) => Promise<void>;\n loginPath?: string;\n loadingComponent?: React.ReactNode;\n}\n\nexport function TernSecureClientProvider({ \n children, \n loginPath = '/sign-in',\n loadingComponent\n}: TernSecureClientProviderProps) {\n const auth = useMemo(() => ternSecureAuth, []);\n const router = useRouter();\n const [authState, setAuthState] = useState<TernSecureState>(() => ({\n userId: null,\n isLoaded: false,\n error: null,\n isValid: false,\n token: null,\n email: null\n }));\n\n const handleSignOut = useCallback(async (error?: Error) => {\n await auth.signOut();\n setAuthState({\n isLoaded: true,\n userId: null,\n error: error || null,\n isValid: false,\n token: null,\n email: null\n });\n router.push(loginPath);\n }, [auth, router, loginPath]);\n\n const setEmail = useCallback((email: string) => {\n setAuthState((prev) => ({\n ...prev,\n email,\n }))\n }, [])\n\nuseEffect(() => {\n const unsubscribe = onAuthStateChanged(auth, async (user: User | null) => {\n if (user) {\n setAuthState({\n isLoaded: true,\n userId: user.uid,\n isValid: true,\n token: user.getIdToken(),\n error: null,\n email: user.email,\n })\n } else {\n setAuthState({\n isLoaded: true,\n userId: null,\n isValid: false,\n token: null,\n error: new Error('User is not authenticated'),\n email: null\n })\n if (!window.location.pathname.includes(\"/sign-up\")) {\n router.push(loginPath)\n }\n }\n }, (error) => {\n handleSignOut(error instanceof Error ? error : new Error('Authentication error occurred'));\n })\n \n return () => unsubscribe()\n }, [auth, handleSignOut, router, loginPath])\n\n const contextValue: TernSecureCtxValue = useMemo(() => ({\n ...authState,\n signOut: handleSignOut,\n setEmail\n }), [authState, auth, handleSignOut, setEmail]);\n\n if (!authState.isLoaded) {\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {loadingComponent || (\n <div aria-live=\"polite\" aria-busy=\"true\">\n <span className=\"sr-only\">Loading authentication state...</span>\n </div>\n )}\n </TernSecureCtx.Provider>\n );\n }\n\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {children}\n </TernSecureCtx.Provider>\n )\n}"],"mappings":";AA6FY;AA3FZ,SAAgB,UAAU,WAAW,SAAS,mBAAmB;AACjE,SAAS,sBAAsB;AAC/B,SAAS,0BAAgC;AACzC,SAAS,qBAA0D;AACnE,SAAS,iBAAiB;AASnB,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAAkC;AAChC,QAAM,OAAO,QAAQ,MAAM,gBAAgB,CAAC,CAAC;AAC7C,QAAM,SAAS,UAAU;AACzB,QAAM,CAAC,WAAW,YAAY,IAAI,SAA0B,OAAO;AAAA,IACjE,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,OAAO;AAAA,IACP,SAAS;AAAA,IACT,OAAO;AAAA,IACP,OAAO;AAAA,EACT,EAAE;AAEF,QAAM,gBAAgB,YAAY,OAAO,UAAkB;AACzD,UAAM,KAAK,QAAQ;AACnB,iBAAa;AAAA,MACX,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,OAAO,SAAS;AAAA,MAChB,SAAS;AAAA,MACT,OAAO;AAAA,MACP,OAAO;AAAA,IACT,CAAC;AACD,WAAO,KAAK,SAAS;AAAA,EACvB,GAAG,CAAC,MAAM,QAAQ,SAAS,CAAC;AAE5B,QAAM,WAAW,YAAY,CAAC,UAAkB;AAC9C,iBAAa,CAAC,UAAU;AAAA,MACtB,GAAG;AAAA,MACH;AAAA,IACF,EAAE;AAAA,EACJ,GAAG,CAAC,CAAC;AAEP,YAAU,MAAM;AACZ,UAAM,cAAc,mBAAmB,MAAM,OAAO,SAAsB;AACxE,UAAI,MAAM;AACR,qBAAa;AAAA,UACX,UAAU;AAAA,UACV,QAAQ,KAAK;AAAA,UACb,SAAS;AAAA,UACT,OAAO,KAAK,WAAW;AAAA,UACvB,OAAO;AAAA,UACP,OAAO,KAAK;AAAA,QACd,CAAC;AAAA,MACH,OAAO;AACL,qBAAa;AAAA,UACX,UAAU;AAAA,UACV,QAAQ;AAAA,UACR,SAAS;AAAA,UACT,OAAO;AAAA,UACP,OAAO,IAAI,MAAM,2BAA2B;AAAA,UAC5C,OAAO;AAAA,QACT,CAAC;AACD,YAAI,CAAC,OAAO,SAAS,SAAS,SAAS,UAAU,GAAG;AAClD,iBAAO,KAAK,SAAS;AAAA,QACvB;AAAA,MACF;AAAA,IACF,GAAG,CAAC,UAAU;AACZ,oBAAc,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,+BAA+B,CAAC;AAAA,IAC3F,CAAC;AAED,WAAO,MAAM,YAAY;AAAA,EAC3B,GAAG,CAAC,MAAM,eAAe,QAAQ,SAAS,CAAC;AAE3C,QAAM,eAAmC,QAAQ,OAAO;AAAA,IACtD,GAAG;AAAA,IACH,SAAS;AAAA,IACT;AAAA,EACF,IAAI,CAAC,WAAW,MAAM,eAAe,QAAQ,CAAC;AAE9C,MAAI,CAAC,UAAU,UAAU;AACvB,WACE,oBAAC,cAAc,UAAd,EAAuB,OAAO,cAC5B,8BACC,oBAAC,SAAI,aAAU,UAAS,aAAU,QAChC,8BAAC,UAAK,WAAU,WAAU,6CAA+B,GAC3D,GAEJ;AAAA,EAEJ;AAEA,SACI,oBAAC,cAAc,UAAd,EAAuB,OAAO,cAC7B,UACF;AAEN;","names":[]}
1
+ {"version":3,"sources":["../../../src/boundary/TernSecureClientProvider.tsx"],"sourcesContent":["\"use client\"\n\nimport React, { useState, useEffect, useMemo, useCallback } from 'react'\nimport { ternSecureAuth } from '../utils/client-init'\nimport { onAuthStateChanged, User } from \"firebase/auth\"\nimport { TernSecureCtx, TernSecureCtxValue } from './TernSecureCtx'\nimport type { TernSecureState, AuthError, SignInResponse } from \"../types\"\nimport type { ERRORS } from '../errors'\nimport { useRouter, usePathname } from 'next/navigation'\nimport { isBaseAuthRoute, isInternalRoute, isAuthRoute} from '../app-router/route-handler/internal-route'\nimport { hasRedirectLoop } from '../utils/construct'\n\n\n\n/**\n * @internal\n * Internal provider props - not meant for direct usage\n */\ninterface TernSecureClientProviderProps {\n children: React.ReactNode\n /** Callback when user state changes */\n onUserChanged?: (user: User | null) => Promise<void>\n /** Login page path */\n loginPath?: string\n /** Signup page path */\n signUpPath?: string\n /** Custom loading component */\n loadingComponent?: React.ReactNode\n /** Whether email verification is required */\n requiresVerification: boolean\n}\n\n/**\n * @internal\n * Internal provider component that handles authentication state\n * This is wrapped by the public TernSecureProvider\n */\n\nexport function TernSecureClientProvider({ \n children, \n loginPath = process.env.NEXT_PUBLIC_SIGN_IN_PATH || '/sign-in',\n signUpPath = process.env.NEXT_PUBLIC_SIGN_UP_PATH || '/sign-up',\n loadingComponent,\n requiresVerification,\n}: TernSecureClientProviderProps) {\n const auth = useMemo(() => ternSecureAuth, []);\n const router = useRouter();\n const pathname = usePathname() // Get current pathname\n const [isRedirecting, setIsRedirecting] = useState(false)\n\n const [authState, setAuthState] = useState<TernSecureState>(() => ({\n userId: null,\n isLoaded: false,\n error: null,\n isValid: false,\n isVerified: false,\n isAuthenticated: false,\n token: null,\n email: null,\n status: \"loading\",\n requiresVerification,\n }));\n\n const constructUrlWithRedirect = useCallback(\n (loginPath: string, currentPath: string, loginPathParam: string, signUpPathParam: string): string => {\n const baseUrl = window.location.origin\n const signInUrl = new URL(loginPath, baseUrl)\n\n // Only add redirect if not already on login or signup page\n if (!currentPath.includes(loginPathParam) && !currentPath.includes(signUpPathParam)) {\n signInUrl.searchParams.set(\"redirect\", currentPath)\n }\n return signInUrl.toString()\n },\n [],\n )\n\n\n const shouldRedirect = useCallback(\n (pathname: string, isVerified: boolean) => {\n // Get current search params\n const searchParams = new URLSearchParams(window.location.search)\n\n // Don't redirect if we're on the base sign-in page with no redirect param\n if (isBaseAuthRoute(pathname) && !searchParams.has(\"redirect\")) {\n return false\n }\n\n // Don't redirect if we're on an internal route\n if (isInternalRoute(pathname)) {\n return false\n }\n\n // Don't redirect if we're in auth routes (except when handling verification)\n if (isAuthRoute(pathname) && (!requiresVerification || isVerified)) {\n return false\n }\n\n return true\n },\n [requiresVerification],\n )\n\n const redirectToLogin = useCallback(\n (currentPath?: string) => {\n const path = currentPath || pathname || \"/\"\n\n\n if (isInternalRoute(path)) { // Don't redirect if we're already on an internal route\n return\n }\n\n // Check for redirect loops\n if (hasRedirectLoop(path, loginPath)) {\n return\n }\n\n setIsRedirecting(true)\n\n const loginUrl = constructUrlWithRedirect(loginPath, path, loginPath, signUpPath)\n\n if (process.env.NODE_ENV === \"production\") {\n window.location.href = loginUrl\n } else {\n // Use router.push for development\n router.push(loginUrl)\n }\n }, \n [router, loginPath, signUpPath, pathname, constructUrlWithRedirect]\n)\n\n const handleSignOut = useCallback(async (error?: Error) => {\n const currentPath = window.location.pathname\n await auth.signOut();\n setAuthState({\n isLoaded: true,\n userId: null,\n error: error || null,\n isValid: false,\n token: null,\n email: null,\n isVerified: false,\n isAuthenticated: false,\n status: \"unauthenticated\",\n requiresVerification,\n })\n redirectToLogin(currentPath)\n }, [auth, redirectToLogin, requiresVerification])\n\n const setEmail = useCallback((email: string) => {\n setAuthState((prev) => ({\n ...prev,\n email,\n }))\n }, [])\n\n const getAuthError = useCallback((): SignInResponse => {\n if (authState.error) {\n const error = authState.error as AuthError;\n return {\n success: false,\n message: error.message,\n error: error.code as keyof typeof ERRORS,\n user: null,\n }\n }\n\n if (authState.requiresVerification && authState.isValid && !authState.isVerified) {\n return {\n success: false,\n message: 'Email verification required',\n error: 'EMAIL_NOT_VERIFIED',\n user: null,\n }\n }\n\n if (!authState.isAuthenticated && authState.status !== \"loading\") {\n return {\n success: false,\n message: 'User is not authenticated',\n error: 'AUTHENTICATED',\n user: null,\n }\n }\n\n return {\n success: true,\n user: ternSecureAuth.currentUser,\n }\n }, [\n authState.error,\n authState.isValid,\n authState.isVerified,\n authState.isAuthenticated,\n authState.status,\n authState.requiresVerification,\n ])\n\nuseEffect(() => {\n let mounted = true\n let initialLoad = true\n\n const unsubscribe = onAuthStateChanged(\n auth,\n async (user: User | null) => {\n if (!mounted) return\n try {\n if (user) {\n const isValid = !!user.uid;\n const isVerified = user.emailVerified;\n const isAuthenticated = isValid && (!requiresVerification || isVerified) // Consider user authenticated if verification is not required or if email is verified\n\n setAuthState({\n isLoaded: true,\n userId: user.uid,\n isValid,\n isVerified,\n isAuthenticated: isValid && isVerified,\n token: user.getIdToken(),\n error: null,\n email: user.email,\n status: isAuthenticated ? \"authenticated\" : \"unverified\",\n requiresVerification,\n })\n \n if (requiresVerification && !isVerified && shouldRedirect(pathname || \"\", isVerified)) {\n if(initialLoad || !isRedirecting) {\n redirectToLogin(pathname)\n }\n }\n } else {\n setAuthState({\n isLoaded: true,\n userId: null,\n isValid: false,\n isVerified: false,\n isAuthenticated: false,\n token: null,\n error: null,\n email: null,\n status: \"unauthenticated\",\n requiresVerification,\n })\n \n if (shouldRedirect(pathname || \"\", false) && initialLoad) {\n redirectToLogin()\n }\n }\n } catch (error){\n console.error(\"Auth state change error:\", error)\n if (mounted) {\n handleSignOut(error instanceof Error ? error : new Error(\"Authentication error occurred\"))\n }\n } finally {\n initialLoad = false\n }\n })\n \n return () => {\n mounted = false\n unsubscribe()\n }\n }, [auth, handleSignOut, redirectToLogin, requiresVerification, pathname, isRedirecting, shouldRedirect])\n\n const contextValue: TernSecureCtxValue = useMemo(() => ({\n ...authState,\n signOut: handleSignOut,\n setEmail,\n getAuthError,\n redirectToLogin,\n }), [authState, handleSignOut, setEmail, getAuthError, redirectToLogin]);\n\n if (!authState.isLoaded) {\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {loadingComponent || (\n <div aria-live=\"polite\" aria-busy=\"true\">\n <span className=\"sr-only\">Loading authentication state...</span>\n </div>\n )}\n </TernSecureCtx.Provider>\n );\n }\n\n return (\n <TernSecureCtx.Provider value={contextValue}>\n {children}\n </TernSecureCtx.Provider>\n )\n}"],"mappings":"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","names":["loginPath","pathname"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/boundary/TernSecureCtx.tsx"],"sourcesContent":["\"use client\"\n\nimport { createContext, useContext } from 'react'\nimport { ternSecureAuth } from '../utils/client-init';\nimport { User } from 'firebase/auth';\n\nexport const TernSecureUser = (): User | null => {\n return ternSecureAuth.currentUser;\n}\n\nexport interface TernSecureState {\n userId: string | null\n isLoaded: boolean\n error: Error | null\n isValid: boolean\n token: any | null\n email: string | null\n}\n\nexport interface TernSecureCtxValue extends TernSecureState {\n signOut: () => Promise<void>\n setEmail: (email: string) => void\n}\n\nexport const TernSecureCtx = createContext<TernSecureCtxValue | null>(null)\n\nTernSecureCtx.displayName = 'TernSecureCtx'\n\nexport const useTernSecure = (hookName: string) => {\n const context = useContext(TernSecureCtx)\n \n if (!context) {\n throw new Error(\n `${hookName} must be used within TernSecureProvider`\n )\n }\n\n return context\n}\n\n"],"mappings":";AAEA,SAAS,eAAe,kBAAkB;AAC1C,SAAS,sBAAsB;AAGxB,MAAM,iBAAiB,MAAmB;AAC/C,SAAO,eAAe;AACxB;AAgBO,MAAM,gBAAgB,cAAyC,IAAI;AAE1E,cAAc,cAAc;AAErB,MAAM,gBAAgB,CAAC,aAAqB;AACjD,QAAM,UAAU,WAAW,aAAa;AAExC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,GAAG,QAAQ;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
1
+ {"version":3,"sources":["../../../src/boundary/TernSecureCtx.tsx"],"sourcesContent":["\"use client\"\n\nimport { createContext, useContext } from 'react'\nimport { ternSecureAuth } from '../utils/client-init';\nimport { User } from 'firebase/auth';\nimport type { TernSecureState, SignInResponse } from '../types';\n\nexport const TernSecureUser = (): User | null => {\n return ternSecureAuth.currentUser;\n}\n\nexport interface TernSecureCtxValue extends TernSecureState {\n signOut: () => Promise<void>\n setEmail: (email: string) => void\n getAuthError: () => SignInResponse\n redirectToLogin: () => void\n}\n\nexport const TernSecureCtx = createContext<TernSecureCtxValue | null>(null)\n\nTernSecureCtx.displayName = 'TernSecureCtx'\n\nexport const useTernSecure = (hookName: string) => {\n const context = useContext(TernSecureCtx)\n \n if (!context) {\n throw new Error(\n `${hookName} must be used within TernSecureProvider`\n )\n }\n\n return context\n}\n\n"],"mappings":";AAEA,SAAS,eAAe,kBAAkB;AAC1C,SAAS,sBAAsB;AAIxB,MAAM,iBAAiB,MAAmB;AAC/C,SAAO,eAAe;AACxB;AASO,MAAM,gBAAgB,cAAyC,IAAI;AAE1E,cAAc,cAAc;AAErB,MAAM,gBAAgB,CAAC,aAAqB;AACjD,QAAM,UAAU,WAAW,aAAa;AAExC,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,GAAG,QAAQ;AAAA,IACb;AAAA,EACF;AAEA,SAAO;AACT;","names":[]}
@@ -7,17 +7,30 @@ function useAuth() {
7
7
  isLoaded,
8
8
  error,
9
9
  isValid,
10
+ isVerified,
11
+ isAuthenticated,
10
12
  token,
13
+ getAuthError,
14
+ status,
15
+ requiresVerification,
11
16
  signOut
12
17
  } = useTernSecure("useAuth");
13
18
  const user = TernSecureUser();
19
+ const authResponse = getAuthError();
14
20
  return {
15
21
  user,
16
22
  userId,
17
23
  isLoaded,
18
- error,
19
- isAuthenticated: isValid,
24
+ error: authResponse.success ? null : authResponse,
25
+ isValid,
26
+ // User is signed in
27
+ isVerified,
28
+ // Email is verified
29
+ isAuthenticated,
30
+ // User is both signed in and verified
20
31
  token,
32
+ status,
33
+ requiresVerification,
21
34
  signOut
22
35
  };
23
36
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/boundary/hooks/useAuth.ts"],"sourcesContent":["\"use client\"\n\nimport { useTernSecure } from '../TernSecureCtx'\nimport { User } from 'firebase/auth'\nimport { TernSecureUser } from '../TernSecureCtx'\n\nexport function useAuth() {\n const {\n userId,\n isLoaded,\n error,\n isValid,\n token,\n signOut\n } = useTernSecure('useAuth')\n\n const user: User | null = TernSecureUser()\n\n return {\n user,\n userId,\n isLoaded,\n error,\n isAuthenticated: isValid,\n token,\n signOut\n }\n}\n"],"mappings":";AAEA,SAAS,qBAAqB;AAE9B,SAAS,sBAAsB;AAExB,SAAS,UAAU;AACxB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,cAAc,SAAS;AAE3B,QAAM,OAAoB,eAAe;AAEzC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../../../src/boundary/hooks/useAuth.ts"],"sourcesContent":["\"use client\"\n\nimport { useTernSecure } from '../TernSecureCtx'\nimport { User } from 'firebase/auth'\nimport { TernSecureUser } from '../TernSecureCtx'\nimport type { SignInResponse } from '../../types'\n\n\nexport function useAuth() {\n const {\n userId,\n isLoaded,\n error,\n isValid,\n isVerified,\n isAuthenticated,\n token,\n getAuthError,\n status,\n requiresVerification,\n signOut\n } = useTernSecure('useAuth')\n\n const user: User | null = TernSecureUser()\n const authResponse: SignInResponse = getAuthError()\n\n\n return {\n user,\n userId,\n isLoaded,\n error: authResponse.success ? null : authResponse,\n isValid, // User is signed in\n isVerified, // Email is verified\n isAuthenticated, // User is both signed in and verified\n token,\n status,\n requiresVerification,\n signOut\n }\n}\n"],"mappings":";AAEA,SAAS,qBAAqB;AAE9B,SAAS,sBAAsB;AAIxB,SAAS,UAAU;AACxB,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAI,cAAc,SAAS;AAE3B,QAAM,OAAoB,eAAe;AACzC,QAAM,eAA+B,aAAa;AAGlD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,aAAa,UAAU,OAAO;AAAA,IACrC;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useCallback, useEffect } from "react";
4
- import { useSearchParams } from "next/navigation";
4
+ import { useSearchParams, useRouter, usePathname } from "next/navigation";
5
5
  import { signInWithEmail, signInWithRedirectGoogle, signInWithMicrosoft } from "../app-router/client/actions";
6
6
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./ui/card";
7
7
  import { Input } from "./ui/input";
@@ -10,12 +10,15 @@ import { Button } from "./ui/button";
10
10
  import { Alert, AlertDescription } from "./ui/alert";
11
11
  import { Separator } from "./ui/separator";
12
12
  import { cn } from "../lib/utils";
13
- import { Loader2 } from "lucide-react";
13
+ import { Loader2, Eye, EyeOff } from "lucide-react";
14
14
  import { getRedirectResult } from "firebase/auth";
15
15
  import { ternSecureAuth } from "../utils/client-init";
16
16
  import { createSessionCookie } from "../app-router/server/sessionTernSecure";
17
17
  import { AuthBackground } from "./background";
18
18
  import { getValidRedirectUrl } from "../utils/construct";
19
+ import { handleInternalRoute } from "../app-router/route-handler/internal-route";
20
+ import { useAuth } from "../boundary/hooks/useAuth";
21
+ import { getErrorAlertVariant } from "../errors";
19
22
  const authDomain = process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN;
20
23
  const appName = process.env.NEXT_PUBLIC_FIREBASE_APP_NAME || "TernSecure";
21
24
  function SignIn({
@@ -27,11 +30,65 @@ function SignIn({
27
30
  }) {
28
31
  const [loading, setLoading] = useState(false);
29
32
  const [checkingRedirect, setCheckingRedirect] = useState(true);
33
+ const [formError, setFormError] = useState(null);
30
34
  const [error, setError] = useState("");
31
35
  const [email, setEmail] = useState("");
32
36
  const [password, setPassword] = useState("");
37
+ const [showPassword, setShowPassword] = useState(false);
38
+ const [passwordFocused, setPasswordFocused] = useState(false);
39
+ const [authResponse, setAuthResponse] = useState(null);
40
+ const [authErrorMessage, setAuthErrorMessage] = useState(null);
33
41
  const searchParams = useSearchParams();
34
42
  const isRedirectSignIn = searchParams.get("signInRedirect") === "true";
43
+ const router = useRouter();
44
+ const pathname = usePathname();
45
+ const InternalComponent = handleInternalRoute(pathname || "");
46
+ const { requiresVerification, error: authError, status } = useAuth();
47
+ const validRedirectUrl = getValidRedirectUrl(searchParams, redirectUrl);
48
+ if (InternalComponent) {
49
+ return /* @__PURE__ */ jsx(InternalComponent, {});
50
+ }
51
+ useEffect(() => {
52
+ if (authError && status !== "loading" && status !== "unauthenticated") {
53
+ const message = authError.message || "Authentication failed";
54
+ setAuthErrorMessage(message);
55
+ if (!authResponse || authResponse.message !== message) {
56
+ setAuthResponse(authError);
57
+ }
58
+ } else {
59
+ setAuthErrorMessage(null);
60
+ }
61
+ }, [authError, status, authResponse]);
62
+ const handleSuccessfulAuth = useCallback(
63
+ async (user) => {
64
+ try {
65
+ const idToken = await user.getIdToken();
66
+ const sessionResult = await createSessionCookie(idToken);
67
+ if (!sessionResult.success) {
68
+ setFormError({
69
+ success: false,
70
+ message: sessionResult.message || "Failed to create session",
71
+ error: "INTERNAL_ERROR",
72
+ user: null
73
+ });
74
+ }
75
+ onSuccess == null ? void 0 : onSuccess();
76
+ if (process.env.NODE_ENV === "production") {
77
+ window.location.href = validRedirectUrl;
78
+ } else {
79
+ router.push(validRedirectUrl);
80
+ }
81
+ } catch (err) {
82
+ setFormError({
83
+ success: false,
84
+ message: "Failed to complete authentication",
85
+ error: "INTERNAL_ERROR",
86
+ user: null
87
+ });
88
+ }
89
+ },
90
+ [validRedirectUrl, router, onSuccess]
91
+ );
35
92
  const handleRedirectResult = useCallback(async () => {
36
93
  if (!isRedirectSignIn) return false;
37
94
  setCheckingRedirect(true);
@@ -52,15 +109,16 @@ function SignIn({
52
109
  const storedRedirectUrl = sessionStorage.getItem("auth_return_url");
53
110
  sessionStorage.removeItem("auth_redirect_url");
54
111
  onSuccess == null ? void 0 : onSuccess();
55
- window.location.href = storedRedirectUrl || getValidRedirectUrl(redirectUrl, searchParams);
112
+ window.location.href = storedRedirectUrl || getValidRedirectUrl(searchParams, redirectUrl);
56
113
  return true;
57
114
  }
58
115
  setCheckingRedirect(false);
59
116
  } catch (err) {
60
- console.error("Redirect result error:", err);
61
- const errorMessage = err instanceof Error ? err.message : "Authentication failed";
62
- setError(errorMessage);
63
- onError == null ? void 0 : onError(err instanceof Error ? err : new Error(errorMessage));
117
+ const errorMessage = err;
118
+ setFormError(errorMessage);
119
+ if (onError && err instanceof Error) {
120
+ onError(err);
121
+ }
64
122
  sessionStorage.removeItem("auth_redirect_url");
65
123
  return false;
66
124
  }
@@ -69,21 +127,42 @@ function SignIn({
69
127
  if (isRedirectSignIn) {
70
128
  handleRedirectResult();
71
129
  }
72
- ;
73
130
  }, [handleRedirectResult, isRedirectSignIn]);
74
131
  const handleSubmit = async (e) => {
75
132
  e.preventDefault();
76
133
  setLoading(true);
134
+ setFormError(null);
135
+ setAuthResponse(null);
77
136
  try {
78
- const user = await signInWithEmail(email, password);
79
- if (user.success) {
80
- onSuccess == null ? void 0 : onSuccess();
81
- window.location.href = getValidRedirectUrl(redirectUrl, searchParams);
137
+ const response = await signInWithEmail(email, password);
138
+ setAuthResponse(response);
139
+ if (!response.success) {
140
+ setFormError({
141
+ success: false,
142
+ message: response.message,
143
+ error: response.error,
144
+ user: null
145
+ });
146
+ return;
147
+ }
148
+ if (response.user) {
149
+ if (requiresVerification && !response.user.emailVerified) {
150
+ setFormError({
151
+ success: false,
152
+ message: "Email verification required",
153
+ error: "REQUIRES_VERIFICATION",
154
+ user: response.user
155
+ });
156
+ return;
157
+ }
158
+ await handleSuccessfulAuth(response.user);
82
159
  }
83
160
  } catch (err) {
84
- const errorMessage = err instanceof Error ? err.message : "Failed to sign in";
85
- setError(errorMessage);
86
- onError == null ? void 0 : onError(err instanceof Error ? err : new Error("Failed to sign in"));
161
+ const errorMessage = err;
162
+ setFormError(errorMessage);
163
+ if (onError && err instanceof Error) {
164
+ onError(err);
165
+ }
87
166
  } finally {
88
167
  setLoading(false);
89
168
  }
@@ -91,8 +170,8 @@ function SignIn({
91
170
  const handleSocialSignIn = async (provider) => {
92
171
  setLoading(true);
93
172
  try {
94
- const validRedirectUrl = getValidRedirectUrl(redirectUrl, searchParams);
95
- sessionStorage.setItem("auth_redirect_url", validRedirectUrl);
173
+ const validRedirectUrl2 = getValidRedirectUrl(searchParams, redirectUrl);
174
+ sessionStorage.setItem("auth_redirect_url", validRedirectUrl2);
96
175
  const currentUrl = new URL(window.location.href);
97
176
  currentUrl.searchParams.set("signInRedirect", "true");
98
177
  window.history.replaceState({}, "", currentUrl.toString());
@@ -101,29 +180,50 @@ function SignIn({
101
180
  throw new Error(result.error);
102
181
  }
103
182
  } catch (err) {
104
- const errorMessage = err instanceof Error ? err.message : `Failed to sign in with ${provider}`;
105
- setError(errorMessage);
106
- onError == null ? void 0 : onError(err instanceof Error ? err : new Error(`Failed to sign in with ${provider}`));
183
+ const errorMessage = err;
184
+ setFormError(errorMessage);
185
+ if (onError && err instanceof Error) {
186
+ onError(err);
187
+ }
107
188
  setLoading(false);
108
189
  sessionStorage.removeItem("auth_redirect_url");
109
190
  }
110
191
  };
192
+ const handleVerificationRedirect = (e) => {
193
+ e.preventDefault();
194
+ router.push("/sign-in/verify");
195
+ };
111
196
  if (checkingRedirect && isRedirectSignIn) {
112
197
  return /* @__PURE__ */ jsx("div", { className: "flex min-h-screen items-center justify-center", children: /* @__PURE__ */ jsx("div", { className: "text-center space-y-4", children: /* @__PURE__ */ jsx("div", { className: "animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto" }) }) });
113
198
  }
199
+ const activeError = formError || authResponse;
200
+ const showEmailVerificationButton = (activeError == null ? void 0 : activeError.error) === "EMAIL_NOT_VERIFIED" || (activeError == null ? void 0 : activeError.error) === "REQUIRES_VERIFICATION";
114
201
  return /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-center", children: [
115
202
  /* @__PURE__ */ jsx(AuthBackground, {}),
116
203
  /* @__PURE__ */ jsxs(Card, { className: cn("w-full max-w-md mx-auto mt-8", className, customStyles.card), children: [
117
204
  /* @__PURE__ */ jsxs(CardHeader, { className: "space-y-1 text-center", children: [
118
205
  /* @__PURE__ */ jsxs(CardTitle, { className: cn("font-bold", customStyles.title), children: [
119
206
  "Sign in to ",
120
- `${appName}`
207
+ `${appName}`,
208
+ " "
121
209
  ] }),
122
210
  /* @__PURE__ */ jsx(CardDescription, { className: cn("text-muted-foreground", customStyles.description), children: "Please sign in to continue" })
123
211
  ] }),
124
212
  /* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
125
213
  /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [
126
- error && /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }),
214
+ activeError && /* @__PURE__ */ jsx(Alert, { variant: getErrorAlertVariant(activeError), className: "animate-in fade-in-50", children: /* @__PURE__ */ jsxs(AlertDescription, { children: [
215
+ /* @__PURE__ */ jsx("span", { children: activeError.message }),
216
+ showEmailVerificationButton && /* @__PURE__ */ jsx(
217
+ Button,
218
+ {
219
+ type: "button",
220
+ variant: "link",
221
+ className: "p-0 h-auto font-normal text-sm hover:underline",
222
+ onClick: handleVerificationRedirect,
223
+ children: "Request new verification email \u2192"
224
+ }
225
+ )
226
+ ] }) }),
127
227
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
128
228
  /* @__PURE__ */ jsx(Label, { htmlFor: "email", className: cn(customStyles.label), children: "Email" }),
129
229
  /* @__PURE__ */ jsx(
@@ -136,24 +236,47 @@ function SignIn({
136
236
  onChange: (e) => setEmail(e.target.value),
137
237
  disabled: loading,
138
238
  className: cn(customStyles.input),
139
- required: true
239
+ required: true,
240
+ "aria-invalid": (activeError == null ? void 0 : activeError.error) === "INVALID_EMAIL",
241
+ "aria-describedby": activeError ? "error-message" : void 0
140
242
  }
141
243
  )
142
244
  ] }),
143
245
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
144
246
  /* @__PURE__ */ jsx(Label, { htmlFor: "password", className: cn(customStyles.label), children: "Password" }),
145
- /* @__PURE__ */ jsx(
146
- Input,
147
- {
148
- id: "password",
149
- type: "password",
150
- value: password,
151
- onChange: (e) => setPassword(e.target.value),
152
- disabled: loading,
153
- className: cn(customStyles.input),
154
- required: true
155
- }
156
- )
247
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
248
+ /* @__PURE__ */ jsx(
249
+ Input,
250
+ {
251
+ id: "password",
252
+ name: "password",
253
+ type: showPassword ? "text" : "password",
254
+ value: password,
255
+ onChange: (e) => setPassword(e.target.value),
256
+ onFocus: () => setPasswordFocused(true),
257
+ onBlur: () => setPasswordFocused(false),
258
+ disabled: loading,
259
+ className: cn(customStyles.input),
260
+ required: true,
261
+ "aria-invalid": (activeError == null ? void 0 : activeError.error) === "INVALID_CREDENTIALS",
262
+ "aria-describedby": activeError ? "error-message" : void 0
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsxs(
266
+ Button,
267
+ {
268
+ type: "button",
269
+ variant: "ghost",
270
+ size: "icon",
271
+ className: "absolute right-2 top-1/2 -translate-y-1/2 h-8 w-8 hover:bg-transparent",
272
+ onClick: () => setShowPassword(!showPassword),
273
+ children: [
274
+ showPassword ? /* @__PURE__ */ jsx(EyeOff, { className: "h-4 w-4 text-muted-foreground hover:text-foreground" }) : /* @__PURE__ */ jsx(Eye, { className: "h-4 w-4 text-muted-foreground hover:text-foreground" }),
275
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: showPassword ? "Hide password" : "Show password" })
276
+ ]
277
+ }
278
+ )
279
+ ] })
157
280
  ] }),
158
281
  /* @__PURE__ */ jsx(Button, { type: "submit", disabled: loading, className: cn("w-full", customStyles.button), children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
159
282
  /* @__PURE__ */ jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }),
@@ -161,8 +284,8 @@ function SignIn({
161
284
  ] }) : "Sign in" })
162
285
  ] }),
163
286
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
164
- /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center", children: /* @__PURE__ */ jsx(Separator, { className: cn(customStyles.separator) }) }),
165
- /* @__PURE__ */ jsx("div", { className: "relative flex justify-center text-xs uppercase", children: /* @__PURE__ */ jsx("span", { className: "bg-background px-2 text-muted-foreground", children: "Or continue with" }) })
287
+ /* @__PURE__ */ jsx(Separator, { className: cn(customStyles.separator) }),
288
+ /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "bg-background px-2 text-muted-foreground text-sm", children: "Or continue with" }) })
166
289
  ] }),
167
290
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
168
291
  /* @__PURE__ */ jsxs(