@mesob/auth-react 0.0.3 → 0.0.5

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 (62) hide show
  1. package/dist/components/auth/auth-card.js +11 -0
  2. package/dist/components/auth/auth-card.js.map +1 -0
  3. package/dist/components/{auth-page-layout.d.ts → auth/auth-page-layout.d.ts} +2 -1
  4. package/dist/components/{auth-page-layout.js → auth/auth-page-layout.js} +16 -3
  5. package/dist/components/auth/auth-page-layout.js.map +1 -0
  6. package/dist/components/{countdown.js → auth/countdown.js} +8 -4
  7. package/dist/components/auth/countdown.js.map +1 -0
  8. package/dist/components/{forgot-password.d.ts → auth/forgot-password.d.ts} +1 -1
  9. package/dist/components/{forgot-password.js → auth/forgot-password.js} +7 -4
  10. package/dist/components/auth/forgot-password.js.map +1 -0
  11. package/dist/components/{pages → auth/pages}/forgot-password-page.js +94 -28
  12. package/dist/components/auth/pages/forgot-password-page.js.map +1 -0
  13. package/dist/components/{pages → auth/pages}/reset-password-page.d.ts +2 -1
  14. package/dist/components/{pages → auth/pages}/reset-password-page.js +111 -42
  15. package/dist/components/auth/pages/reset-password-page.js.map +1 -0
  16. package/dist/components/{pages → auth/pages}/sign-in-page.js +130 -30
  17. package/dist/components/auth/pages/sign-in-page.js.map +1 -0
  18. package/dist/components/{pages → auth/pages}/sign-up-page.d.ts +2 -1
  19. package/dist/components/{pages → auth/pages}/sign-up-page.js +93 -29
  20. package/dist/components/auth/pages/sign-up-page.js.map +1 -0
  21. package/dist/components/{pages → auth/pages}/verify-email-page.d.ts +2 -1
  22. package/dist/components/{pages → auth/pages}/verify-email-page.js +131 -61
  23. package/dist/components/auth/pages/verify-email-page.js.map +1 -0
  24. package/dist/components/{pages → auth/pages}/verify-phone-page.d.ts +2 -1
  25. package/dist/components/{pages → auth/pages}/verify-phone-page.js +136 -63
  26. package/dist/components/auth/pages/verify-phone-page.js.map +1 -0
  27. package/dist/components/{reset-password-form.d.ts → auth/reset-password-form.d.ts} +3 -2
  28. package/dist/components/{reset-password-form.js → auth/reset-password-form.js} +17 -15
  29. package/dist/components/auth/reset-password-form.js.map +1 -0
  30. package/dist/components/{sign-in.js → auth/sign-in.js} +43 -6
  31. package/dist/components/auth/sign-in.js.map +1 -0
  32. package/dist/components/{sign-up.js → auth/sign-up.js} +4 -4
  33. package/dist/components/auth/sign-up.js.map +1 -0
  34. package/dist/components/{verification-form.d.ts → auth/verification-form.d.ts} +2 -1
  35. package/dist/components/{verification-form.js → auth/verification-form.js} +33 -33
  36. package/dist/components/auth/verification-form.js.map +1 -0
  37. package/dist/handle-error-H0iqQxJ5.d.ts +6 -0
  38. package/dist/index.d.ts +49 -15
  39. package/dist/index.js +304 -142
  40. package/dist/index.js.map +1 -1
  41. package/package.json +3 -3
  42. package/dist/components/auth-card.js +0 -11
  43. package/dist/components/auth-card.js.map +0 -1
  44. package/dist/components/auth-page-layout.js.map +0 -1
  45. package/dist/components/countdown.js.map +0 -1
  46. package/dist/components/forgot-password.js.map +0 -1
  47. package/dist/components/pages/forgot-password-page.js.map +0 -1
  48. package/dist/components/pages/reset-password-page.js.map +0 -1
  49. package/dist/components/pages/sign-in-page.js.map +0 -1
  50. package/dist/components/pages/sign-up-page.js.map +0 -1
  51. package/dist/components/pages/verify-email-page.js.map +0 -1
  52. package/dist/components/pages/verify-phone-page.js.map +0 -1
  53. package/dist/components/reset-password-form.js.map +0 -1
  54. package/dist/components/sign-in.js.map +0 -1
  55. package/dist/components/sign-up.js.map +0 -1
  56. package/dist/components/verification-form.js.map +0 -1
  57. /package/dist/components/{auth-card.d.ts → auth/auth-card.d.ts} +0 -0
  58. /package/dist/components/{countdown.d.ts → auth/countdown.d.ts} +0 -0
  59. /package/dist/components/{pages → auth/pages}/forgot-password-page.d.ts +0 -0
  60. /package/dist/components/{pages → auth/pages}/sign-in-page.d.ts +0 -0
  61. /package/dist/components/{sign-in.d.ts → auth/sign-in.d.ts} +0 -0
  62. /package/dist/components/{sign-up.d.ts → auth/sign-up.d.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- // src/components/pages/reset-password-page.tsx
3
+ // src/components/auth/pages/reset-password-page.tsx
4
4
  import { useTranslations as useTranslations2 } from "next-intl";
5
5
  import { useState as useState3 } from "react";
6
6
 
@@ -37,40 +37,95 @@ var AuthError = class extends Error {
37
37
  };
38
38
 
39
39
  // src/constants/auth.error.codes.ts
40
- var validCodes = [
41
- "USER_NOT_FOUND",
42
- "INVALID_PASSWORD",
43
- "USER_EXISTS",
44
- "VERIFICATION_EXPIRED",
45
- "VERIFICATION_MISMATCH",
46
- "VERIFICATION_NOT_FOUND",
47
- "TOO_MANY_ATTEMPTS",
48
- "REQUIRES_VERIFICATION",
49
- "UNAUTHORIZED",
50
- "ACCESS_DENIED",
51
- "HAS_NO_PASSWORD"
52
- ];
40
+ var AUTH_ERROR_MAPPING = {
41
+ USER_NOT_FOUND: {
42
+ title: "Account Not Found",
43
+ description: "We could not find an account with that identifier. Please check your spelling or sign up."
44
+ },
45
+ INVALID_PASSWORD: {
46
+ title: "Invalid Password",
47
+ description: "The password you entered is incorrect. Please try again."
48
+ },
49
+ USER_EXISTS: {
50
+ title: "Account Already Exists",
51
+ description: "An account with this identifier already exists. Please sign in instead."
52
+ },
53
+ VERIFICATION_EXPIRED: {
54
+ title: "Verification Expired",
55
+ description: "The verification code or link has expired. Please request a new one."
56
+ },
57
+ VERIFICATION_MISMATCH: {
58
+ title: "Invalid Code",
59
+ description: "The verification code you entered is invalid. Please double-check and try again."
60
+ },
61
+ VERIFICATION_NOT_FOUND: {
62
+ title: "Verification Not Found",
63
+ description: "We could not find a pending verification request. Please restart the process."
64
+ },
65
+ TOO_MANY_ATTEMPTS: {
66
+ title: "Too Many Attempts",
67
+ description: "You have made too many requests recently. Please wait a moment before trying again."
68
+ },
69
+ REQUIRES_VERIFICATION: {
70
+ title: "Verification Required",
71
+ description: "You need to verify your account before you can continue. Please check your email or phone."
72
+ },
73
+ UNAUTHORIZED: {
74
+ title: "Unauthorized",
75
+ description: "You are not authorized to perform this action. Please sign in again."
76
+ },
77
+ ACCESS_DENIED: {
78
+ title: "Access Denied",
79
+ description: "You do not have permission to access this resource. Please contact support if you believe this is an error."
80
+ },
81
+ HAS_NO_PASSWORD: {
82
+ title: "No Password Set",
83
+ description: "Your account does not have a password set (e.g. social login). Please sign in with your provider or reset your password."
84
+ }
85
+ };
86
+ var validCodes = Object.keys(AUTH_ERROR_MAPPING);
53
87
 
54
88
  // src/utils/handle-error.ts
55
89
  var handleError = (err, setError, t) => {
56
90
  if (err instanceof AuthError) {
57
- let errorKey = "errors.fallback";
58
- if (err.code) {
59
- errorKey = `errors.${err.code.toLowerCase()}`;
91
+ let errorCode = "";
92
+ if (err.code && validCodes.includes(err.code)) {
93
+ errorCode = err.code;
60
94
  } else if (err.message) {
61
95
  const messageUpper = err.message.toUpperCase().trim();
62
96
  if (validCodes.includes(messageUpper)) {
63
- errorKey = `errors.${messageUpper.toLowerCase()}`;
97
+ errorCode = messageUpper;
64
98
  }
65
99
  }
66
- setError(t(errorKey, { defaultValue: err.message }));
100
+ if (errorCode && AUTH_ERROR_MAPPING[errorCode]) {
101
+ const mapping = AUTH_ERROR_MAPPING[errorCode];
102
+ setError({
103
+ title: mapping.title,
104
+ description: mapping.description
105
+ });
106
+ return;
107
+ }
108
+ setError({
109
+ title: t("errors.fallback"),
110
+ // or 'Error'
111
+ description: err.message || t("errors.fallback")
112
+ });
67
113
  } else {
68
- setError(err instanceof Error ? err.message : t("errors.fallback"));
114
+ const message = err instanceof Error ? err.message : t("errors.fallback");
115
+ setError({
116
+ title: "Error",
117
+ description: message
118
+ });
69
119
  }
70
120
  };
71
121
 
72
- // src/components/auth-page-layout.tsx
73
- import { Alert, AlertDescription } from "@mesob/ui/components/alert";
122
+ // src/components/auth/auth-page-layout.tsx
123
+ import {
124
+ Alert,
125
+ AlertDescription,
126
+ AlertTitle
127
+ } from "@mesob/ui/components/alert";
128
+ import { IconAlertCircle } from "@tabler/icons-react";
74
129
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
75
130
  var AuthPageLayout = ({
76
131
  title,
@@ -80,6 +135,10 @@ var AuthPageLayout = ({
80
135
  footer,
81
136
  logoImage
82
137
  }) => {
138
+ const errorContent = error ? (
139
+ // biome-ignore lint/style/noNestedTernary: <explanation>
140
+ typeof error === "string" ? { title: "Error", description: error } : error
141
+ ) : null;
83
142
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
84
143
  /* @__PURE__ */ jsx2("div", { className: "flex size-8 mb-6 w-full items-center justify-center rounded-md", children: /* @__PURE__ */ jsx2("img", { src: logoImage || "", alt: "Jiret", width: 42, height: 42 }) }),
85
144
  /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
@@ -87,12 +146,16 @@ var AuthPageLayout = ({
87
146
  description && /* @__PURE__ */ jsx2("p", { className: "mt-2 text-sm text-muted-foreground", children: description })
88
147
  ] }),
89
148
  children,
90
- error && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: error }) }),
149
+ errorContent && /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
150
+ /* @__PURE__ */ jsx2(IconAlertCircle, { className: "h-4 w-4" }),
151
+ /* @__PURE__ */ jsx2(AlertTitle, { children: errorContent.title }),
152
+ /* @__PURE__ */ jsx2(AlertDescription, { children: errorContent.description })
153
+ ] }),
91
154
  /* @__PURE__ */ jsx2("div", { className: "mt-2 w-full", children: footer && /* @__PURE__ */ jsx2("div", { className: "w-full text-center text-sm text-muted-foreground", children: footer }) })
92
155
  ] });
93
156
  };
94
157
 
95
- // src/components/reset-password-form.tsx
158
+ // src/components/auth/reset-password-form.tsx
96
159
  import { zodResolver } from "@hookform/resolvers/zod";
97
160
  import { Button } from "@mesob/ui/components/button";
98
161
  import {
@@ -109,17 +172,16 @@ import {
109
172
  InputOTPGroup,
110
173
  InputOTPSlot
111
174
  } from "@mesob/ui/components/input-otp";
112
- import { Eye, EyeOff } from "lucide-react";
175
+ import { Spinner } from "@mesob/ui/components/spinner";
176
+ import { IconEye, IconEyeOff } from "@tabler/icons-react";
113
177
  import { useTranslations } from "next-intl";
114
178
  import { useMemo, useState as useState2 } from "react";
115
179
  import { useForm } from "react-hook-form";
116
180
  import { z } from "zod";
117
181
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
118
182
  var ResetPasswordForm = ({
119
- verificationId,
120
183
  onSubmit,
121
- isLoading = false,
122
- error
184
+ isLoading = false
123
185
  }) => {
124
186
  const t = useTranslations("Auth.resetPassword");
125
187
  const [showPassword, setShowPassword] = useState2(false);
@@ -153,13 +215,13 @@ var ResetPasswordForm = ({
153
215
  name: "code",
154
216
  render: ({ field }) => /* @__PURE__ */ jsxs2(FormItem, { children: [
155
217
  /* @__PURE__ */ jsx3("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx3(FormLabel, { children: t("form.codeLabel") }) }),
156
- /* @__PURE__ */ jsx3(FormControl, { children: /* @__PURE__ */ jsx3("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx3(InputOTP, { maxLength: 6, ...field, children: /* @__PURE__ */ jsxs2(InputOTPGroup, { children: [
157
- /* @__PURE__ */ jsx3(InputOTPSlot, { index: 0 }),
158
- /* @__PURE__ */ jsx3(InputOTPSlot, { index: 1 }),
159
- /* @__PURE__ */ jsx3(InputOTPSlot, { index: 2 }),
160
- /* @__PURE__ */ jsx3(InputOTPSlot, { index: 3 }),
161
- /* @__PURE__ */ jsx3(InputOTPSlot, { index: 4 }),
162
- /* @__PURE__ */ jsx3(InputOTPSlot, { index: 5 })
218
+ /* @__PURE__ */ jsx3(FormControl, { children: /* @__PURE__ */ jsx3("div", { className: "flex mt-2 justify-center", children: /* @__PURE__ */ jsx3(InputOTP, { maxLength: 6, ...field, containerClassName: "gap-4", children: /* @__PURE__ */ jsxs2(InputOTPGroup, { className: "gap-3 *:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-12 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border *:data-[slot=input-otp-slot]:text-xl", children: [
219
+ /* @__PURE__ */ jsx3(InputOTPSlot, { className: "h-12", index: 0 }),
220
+ /* @__PURE__ */ jsx3(InputOTPSlot, { className: "h-12", index: 1 }),
221
+ /* @__PURE__ */ jsx3(InputOTPSlot, { className: "h-12", index: 2 }),
222
+ /* @__PURE__ */ jsx3(InputOTPSlot, { className: "h-12", index: 3 }),
223
+ /* @__PURE__ */ jsx3(InputOTPSlot, { className: "h-12", index: 4 }),
224
+ /* @__PURE__ */ jsx3(InputOTPSlot, { className: "h-12", index: 5 })
163
225
  ] }) }) }) }),
164
226
  /* @__PURE__ */ jsx3(FormMessage, {})
165
227
  ] })
@@ -187,7 +249,7 @@ var ResetPasswordForm = ({
187
249
  type: "button",
188
250
  onClick: () => setShowPassword(!showPassword),
189
251
  className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
190
- children: showPassword ? /* @__PURE__ */ jsx3(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(Eye, { className: "h-4 w-4" })
252
+ children: showPassword ? /* @__PURE__ */ jsx3(IconEyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(IconEye, { className: "h-4 w-4" })
191
253
  }
192
254
  )
193
255
  ] }) }),
@@ -217,7 +279,7 @@ var ResetPasswordForm = ({
217
279
  type: "button",
218
280
  onClick: () => setShowPassword(!showPassword),
219
281
  className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
220
- children: showPassword ? /* @__PURE__ */ jsx3(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(Eye, { className: "h-4 w-4" })
282
+ children: showPassword ? /* @__PURE__ */ jsx3(IconEyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(IconEye, { className: "h-4 w-4" })
221
283
  }
222
284
  )
223
285
  ] }) }),
@@ -225,18 +287,22 @@ var ResetPasswordForm = ({
225
287
  ] })
226
288
  }
227
289
  ),
228
- /* @__PURE__ */ jsx3(Button, { type: "submit", className: "w-full", disabled: isLoading, children: isLoading ? t("form.submitting") : t("form.submit") })
290
+ /* @__PURE__ */ jsxs2(Button, { type: "submit", className: "w-full", disabled: isLoading, children: [
291
+ isLoading && /* @__PURE__ */ jsx3(Spinner, {}),
292
+ isLoading ? t("form.submitting") : t("form.submit")
293
+ ] })
229
294
  ] }) });
230
295
  };
231
296
 
232
- // src/components/pages/reset-password-page.tsx
297
+ // src/components/auth/pages/reset-password-page.tsx
233
298
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
234
299
  var ResetPasswordPage = ({
235
300
  verificationId,
236
301
  onNavigate,
237
302
  linkComponent: Link,
238
303
  links,
239
- logoImage
304
+ logoImage,
305
+ redirectUrl
240
306
  }) => {
241
307
  const t = useTranslations2("Auth.resetPassword");
242
308
  const common = useTranslations2("Common");
@@ -247,7 +313,10 @@ var ResetPasswordPage = ({
247
313
  const forgotPasswordLink = links?.forgotPassword || "/auth/forgot-password";
248
314
  const handleSubmit = async (values) => {
249
315
  if (!verificationId) {
250
- setError(t("errors.missingVerificationId"));
316
+ setError({
317
+ title: t("errors.fallback"),
318
+ description: t("errors.missingVerificationId")
319
+ });
251
320
  return;
252
321
  }
253
322
  setIsLoading(true);
@@ -259,7 +328,7 @@ var ResetPasswordPage = ({
259
328
  password: values.password
260
329
  });
261
330
  await refresh();
262
- onNavigate("/dashboard");
331
+ onNavigate(redirectUrl || "/");
263
332
  } catch (err) {
264
333
  handleError(err, setError, t);
265
334
  } finally {
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/components/auth/pages/reset-password-page.tsx","../../../../src/context/auth-provider.tsx","../../../../src/client.ts","../../../../src/constants/auth.error.codes.ts","../../../../src/utils/handle-error.ts","../../../../src/components/auth/auth-page-layout.tsx","../../../../src/components/auth/reset-password-form.tsx"],"sourcesContent":["'use client';\n\nimport { useTranslations } from 'next-intl';\nimport type { ComponentProps } from 'react';\nimport { useState } from 'react';\nimport { useAuth } from '../../../context/auth-provider';\nimport type { AuthErrorContent } from '../../../utils/handle-error';\nimport { handleError } from '../../../utils/handle-error';\n\nimport { AuthPageLayout } from '../auth-page-layout';\nimport { ResetPasswordForm } from '../reset-password-form';\n\ntype ResetPasswordPageProps = {\n locale?: string;\n verificationId: string;\n onNavigate: (path: string) => void;\n linkComponent?: React.ComponentType<ComponentProps<'a'> & { href: string }>;\n links?: {\n signIn?: string;\n forgotPassword?: string;\n };\n logoImage?: string;\n redirectUrl?: string;\n};\n\nexport const ResetPasswordPage = ({\n verificationId,\n onNavigate,\n linkComponent: Link,\n links,\n logoImage,\n redirectUrl,\n}: ResetPasswordPageProps) => {\n const t = useTranslations('Auth.resetPassword');\n const common = useTranslations('Common');\n const { client, refresh } = useAuth();\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<AuthErrorContent | null>(null);\n\n const signInLink = links?.signIn || '/auth/sign-in';\n const forgotPasswordLink = links?.forgotPassword || '/auth/forgot-password';\n\n const handleSubmit = async (values: {\n code: string;\n password: string;\n confirmPassword: string;\n }) => {\n if (!verificationId) {\n setError({\n title: t('errors.fallback'),\n description: t('errors.missingVerificationId'),\n });\n return;\n }\n\n setIsLoading(true);\n setError(null);\n\n try {\n await client.resetPassword({\n verificationId,\n code: values.code,\n password: values.password,\n });\n await refresh();\n onNavigate(redirectUrl || '/');\n } catch (err) {\n handleError(err, setError, t);\n } finally {\n setIsLoading(false);\n }\n };\n\n if (!verificationId) {\n return (\n <AuthPageLayout\n title={common('invalidLinkTitle')}\n description={common('invalidLinkDescription')}\n logoImage={logoImage}\n footer={\n Link ? (\n <Link\n href={forgotPasswordLink}\n className=\"text-primary hover:underline\"\n >\n {t('footer.requestNew')}\n </Link>\n ) : (\n <a\n href={forgotPasswordLink}\n onClick={(e) => {\n e.preventDefault();\n onNavigate(forgotPasswordLink);\n }}\n className=\"text-primary hover:underline\"\n >\n {t('footer.requestNew')}\n </a>\n )\n }\n >\n <div />\n </AuthPageLayout>\n );\n }\n\n return (\n <AuthPageLayout\n title={t('title')}\n description={t('description')}\n error={error}\n logoImage={logoImage}\n footer={\n Link ? (\n <div className=\"flex items-center justify-between w-full gap-2\">\n <Link href={signInLink} className=\"text-primary hover:underline\">\n {t('footer.backToSignIn')}\n </Link>\n <Link\n href={forgotPasswordLink}\n className=\"text-primary hover:underline\"\n >\n {t('footer.changeAccount')}\n </Link>\n </div>\n ) : (\n <div className=\"flex items-center justify-between w-full gap-2\">\n <a\n href={forgotPasswordLink}\n onClick={(e) => {\n e.preventDefault();\n onNavigate(forgotPasswordLink);\n }}\n className=\"text-primary hover:underline\"\n >\n {t('footer.backToSignIn')}\n </a>\n <a\n href={forgotPasswordLink}\n onClick={(e) => {\n e.preventDefault();\n onNavigate(forgotPasswordLink);\n }}\n className=\"text-primary hover:underline\"\n >\n {t('footer.changeAccount')}\n </a>\n </div>\n )\n }\n >\n <ResetPasswordForm\n verificationId={verificationId}\n onSubmit={handleSubmit}\n isLoading={isLoading}\n error={error}\n />\n </AuthPageLayout>\n );\n};\n","'use client';\n\nimport {\n createContext,\n type ReactNode,\n useCallback,\n useContext,\n useEffect,\n useState,\n} from 'react';\nimport type { AuthClient } from '../client';\nimport type { AuthResponse, Session, User } from '../types';\n\ntype AuthContextValue = {\n user: User | null;\n session: Session | null;\n loading: boolean;\n error: Error | null;\n client: AuthClient;\n refresh: () => Promise<void>;\n setAuth: (data: AuthResponse) => void;\n};\n\nconst AuthContext = createContext<AuthContextValue | null>(null);\n\nexport const useAuth = () => {\n const context = useContext(AuthContext);\n if (!context) {\n throw new Error('useAuth must be used within AuthProvider');\n }\n return context;\n};\n\ntype AuthProviderProps = {\n client: AuthClient;\n children: ReactNode;\n};\n\nexport const AuthProvider = ({ client, children }: AuthProviderProps) => {\n const [user, setUser] = useState<User | null>(null);\n const [session, setSession] = useState<Session | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const refresh = useCallback(async () => {\n try {\n setLoading(true);\n setError(null);\n const data = await client.getSession();\n setUser(data.user);\n setSession(data.session);\n } catch (err) {\n setError(\n err instanceof Error ? err : new Error('Failed to fetch session'),\n );\n setUser(null);\n setSession(null);\n } finally {\n setLoading(false);\n }\n }, [client]);\n\n const setAuth = useCallback((data: AuthResponse) => {\n setUser(data.user);\n setSession(data.session);\n setError(null);\n setLoading(false);\n }, []);\n\n useEffect(() => {\n refresh();\n }, [refresh]);\n\n return (\n <AuthContext.Provider\n value={{\n user,\n session,\n loading,\n error,\n client,\n refresh,\n setAuth,\n }}\n >\n {children}\n </AuthContext.Provider>\n );\n};\n","import type {\n AuthErrorCode,\n AuthErrorResponse,\n AuthResponse,\n PendingAccountChangeResponse,\n SessionResponse,\n User,\n VerificationResponse,\n} from './types';\n\n// Backend returns error code in 'error' field (AUTH_ERRORS values)\n// or in 'code' field, message in 'message' or 'error' field\nconst validErrorCodes: readonly AuthErrorCode[] = [\n 'USER_NOT_FOUND',\n 'INVALID_PASSWORD',\n 'USER_EXISTS',\n 'VERIFICATION_EXPIRED',\n 'VERIFICATION_MISMATCH',\n 'VERIFICATION_NOT_FOUND',\n 'TOO_MANY_ATTEMPTS',\n 'REQUIRES_VERIFICATION',\n 'UNAUTHORIZED',\n 'ACCESS_DENIED',\n 'HAS_NO_PASSWORD',\n] as const;\n\nexport class AuthError extends Error {\n code?: AuthErrorCode;\n status?: number;\n details?: Record<string, unknown>;\n\n constructor(\n message: string,\n code?: AuthErrorCode,\n status?: number,\n details?: Record<string, unknown>,\n ) {\n super(message);\n this.name = 'AuthError';\n this.code = code;\n this.status = status;\n this.details = details;\n }\n}\n\nexport type AuthClientConfig = {\n baseURL: string;\n};\n\nexport class AuthClient {\n private baseURL: string;\n\n constructor(config: AuthClientConfig) {\n this.baseURL = config.baseURL.replace(/\\/$/, '');\n }\n\n private async request<T>(\n endpoint: string,\n options: RequestInit = {},\n ): Promise<T> {\n const url = `${this.baseURL}${endpoint}`;\n const response = await fetch(url, {\n ...options,\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n ...options.headers,\n },\n });\n if (!response.ok) {\n const text = await response.text();\n let errorData: AuthErrorResponse;\n try {\n errorData = JSON.parse(text);\n } catch {\n errorData = { error: 'Unknown error', message: text };\n }\n\n // Extract error code from code, error, or message field\n // Backend returns error codes in 'error' field (e.g., { error: 'INVALID_PASSWORD' })\n const potentialCode =\n errorData.code ||\n (typeof errorData.error === 'string' ? errorData.error : null) ||\n (typeof errorData.message === 'string' ? errorData.message : null);\n const upperCode = potentialCode?.toUpperCase().trim();\n const errorCode =\n upperCode && validErrorCodes.includes(upperCode as AuthErrorCode)\n ? (upperCode as AuthErrorCode)\n : undefined;\n const errorMessage =\n errorData.message || errorData.error || 'Request failed';\n\n throw new AuthError(\n errorMessage,\n errorCode as AuthErrorCode | undefined,\n response.status,\n errorData.details,\n );\n }\n\n const data = await response.json();\n return data;\n }\n\n signUpWithEmail(data: {\n email: string;\n password: string;\n fullName: string;\n handle?: string;\n }): Promise<AuthResponse | VerificationResponse> {\n return this.request<AuthResponse | VerificationResponse>('/sign-up', {\n method: 'POST',\n body: JSON.stringify({\n email: data.email,\n password: data.password,\n fullName: data.fullName,\n handle: data.handle,\n }),\n });\n }\n\n signUpWithPhone(data: {\n phone: string;\n password: string;\n fullName: string;\n handle?: string;\n }): Promise<AuthResponse | VerificationResponse> {\n return this.request<AuthResponse | VerificationResponse>('/sign-up', {\n method: 'POST',\n body: JSON.stringify({\n phone: data.phone,\n password: data.password,\n fullName: data.fullName,\n handle: data.handle,\n }),\n });\n }\n\n checkUser(data: { identifier: string }): Promise<{ exists: boolean }> {\n return this.request<{ exists: boolean }>('/check-user', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n signInWithPassword(data: {\n identifier: string;\n password: string;\n }): Promise<AuthResponse | VerificationResponse> {\n return this.request<AuthResponse | VerificationResponse>('/sign-in', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n signOut(): Promise<{ message: string }> {\n return this.request<{ message: string }>('/sign-out', {\n method: 'POST',\n });\n }\n\n requestEmailVerification(data?: {\n email?: string;\n }): Promise<{ verificationId: string }> {\n return this.request<{ verificationId: string }>(\n '/email/verification/request',\n {\n method: 'POST',\n body: JSON.stringify(data || {}),\n },\n );\n }\n\n verifyEmail(data: {\n verificationId: string;\n code: string;\n }): Promise<AuthResponse> {\n return this.request<AuthResponse>('/email/verification/confirm', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n resendVerification(_verificationId: string): Promise<{\n verificationId: string;\n }> {\n // For email, request new verification\n return this.requestEmailVerification();\n }\n\n requestPhoneOtp(data: {\n phone: string;\n context: 'sign-up' | 'sign-in' | 'change-phone';\n }): Promise<{ verificationId: string }> {\n return this.request<{ verificationId: string }>(\n '/phone/verification/request',\n {\n method: 'POST',\n body: JSON.stringify(data),\n },\n );\n }\n\n verifyPhoneOtp(data: {\n verificationId: string;\n code: string;\n context: 'sign-up' | 'sign-in' | 'change-phone';\n }): Promise<\n AuthResponse | { user: User | null; session: null; verified: boolean }\n > {\n return this.request('/phone/verification/confirm', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n forgotPassword(data: { identifier: string }): Promise<{\n message: string;\n }> {\n return this.request<{ message: string }>('/password/forgot', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n resetPassword(data: {\n verificationId: string;\n code: string;\n password: string;\n }): Promise<AuthResponse> {\n return this.request<AuthResponse>('/password/reset', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n verifyPassword(data: { password: string }): Promise<{ message: string }> {\n return this.request<{ message: string }>('/password/verify', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n changePassword(data: {\n currentPassword: string;\n newPassword: string;\n }): Promise<{ message: string }> {\n return this.request<{ message: string }>('/password/change', {\n method: 'POST',\n body: JSON.stringify(data),\n });\n }\n\n getSession(): Promise<SessionResponse> {\n return this.request<SessionResponse>('/session', {\n method: 'GET',\n });\n }\n\n getPendingAccountChange(): Promise<PendingAccountChangeResponse> {\n return this.request<PendingAccountChangeResponse>(\n '/account-change/pending',\n {\n method: 'GET',\n },\n );\n }\n\n updateProfile(data: { fullName?: string }): Promise<{ user: User }> {\n return this.request<{ user: User }>('/profile', {\n method: 'PUT',\n body: JSON.stringify(data),\n });\n }\n\n updateEmail(data: { email: string }): Promise<{ user: User }> {\n return this.request<{ user: User }>('/profile/email', {\n method: 'PUT',\n body: JSON.stringify(data),\n });\n }\n\n updatePhone(data: { phone: string }): Promise<{ user: User }> {\n return this.request<{ user: User }>('/profile/phone', {\n method: 'PUT',\n body: JSON.stringify(data),\n });\n }\n}\n","export const AUTH_ERROR_MAPPING: Record<\n string,\n { title: string; description: string }\n> = {\n USER_NOT_FOUND: {\n title: 'Account Not Found',\n description:\n 'We could not find an account with that identifier. Please check your spelling or sign up.',\n },\n INVALID_PASSWORD: {\n title: 'Invalid Password',\n description: 'The password you entered is incorrect. Please try again.',\n },\n USER_EXISTS: {\n title: 'Account Already Exists',\n description:\n 'An account with this identifier already exists. Please sign in instead.',\n },\n VERIFICATION_EXPIRED: {\n title: 'Verification Expired',\n description:\n 'The verification code or link has expired. Please request a new one.',\n },\n VERIFICATION_MISMATCH: {\n title: 'Invalid Code',\n description:\n 'The verification code you entered is invalid. Please double-check and try again.',\n },\n VERIFICATION_NOT_FOUND: {\n title: 'Verification Not Found',\n description:\n 'We could not find a pending verification request. Please restart the process.',\n },\n TOO_MANY_ATTEMPTS: {\n title: 'Too Many Attempts',\n description:\n 'You have made too many requests recently. Please wait a moment before trying again.',\n },\n REQUIRES_VERIFICATION: {\n title: 'Verification Required',\n description:\n 'You need to verify your account before you can continue. Please check your email or phone.',\n },\n UNAUTHORIZED: {\n title: 'Unauthorized',\n description:\n 'You are not authorized to perform this action. Please sign in again.',\n },\n ACCESS_DENIED: {\n title: 'Access Denied',\n description:\n 'You do not have permission to access this resource. Please contact support if you believe this is an error.',\n },\n HAS_NO_PASSWORD: {\n title: 'No Password Set',\n description:\n 'Your account does not have a password set (e.g. social login). Please sign in with your provider or reset your password.',\n },\n};\n\nexport const validCodes = Object.keys(AUTH_ERROR_MAPPING);\n","import type { _Translator } from 'next-intl';\nimport { AuthError } from '../client';\nimport { AUTH_ERROR_MAPPING, validCodes } from '../constants/auth.error.codes';\n\nexport type AuthErrorContent = {\n title: string;\n description: string;\n};\n\nexport const handleError = (\n err: any,\n setError: (error: AuthErrorContent | null) => void,\n t: _Translator<Record<string, any>, string>,\n) => {\n if (err instanceof AuthError) {\n let errorCode = '';\n\n if (err.code && validCodes.includes(err.code)) {\n errorCode = err.code;\n } else if (err.message) {\n const messageUpper = err.message.toUpperCase().trim();\n if (validCodes.includes(messageUpper)) {\n errorCode = messageUpper;\n }\n }\n\n if (errorCode && AUTH_ERROR_MAPPING[errorCode]) {\n const mapping = AUTH_ERROR_MAPPING[errorCode];\n // Try to translate the description if a key exists, otherwise use English fallback\n // Since mapping.description is English text, we might want to check if there is a translation key\n // But for now, let's use the provided English mapping or try to find a translation if the key matches 'errors.code'\n\n // We can try to lookup translation, if it returns the key (or fallback), we might prefer the mapping text (which is nice English).\n // However, usually we prefer translation.\n // Let's rely on the mapping text primarily as per the request to \"Generate a description and title...\".\n // But if we want i18n support, we should probably add keys.\n // Given the prompt \"Generate a description... for each error code... and display\",\n // I will prioritize the static mapping I just created, but allows future i18n improvement.\n\n setError({\n title: mapping.title,\n description: mapping.description,\n });\n return;\n }\n\n // Fallback for AuthError without known code\n setError({\n title: t('errors.fallback'), // or 'Error'\n description: err.message || t('errors.fallback'),\n });\n } else {\n // Generic error\n const message = err instanceof Error ? err.message : t('errors.fallback');\n setError({\n title: 'Error',\n description: message,\n });\n }\n};\n","'use client';\n\nimport {\n Alert,\n AlertDescription,\n AlertTitle,\n} from '@mesob/ui/components/alert';\nimport { IconAlertCircle } from '@tabler/icons-react';\nimport type { ReactNode } from 'react';\nimport type { AuthErrorContent } from '../../utils/handle-error';\n\ntype AuthPageLayoutProps = {\n title: string;\n description?: string;\n children: ReactNode;\n error?: AuthErrorContent | string | null;\n footer?: ReactNode;\n logoImage?: string;\n};\n\nexport const AuthPageLayout = ({\n title,\n description,\n children,\n error,\n footer,\n logoImage,\n}: AuthPageLayoutProps) => {\n const errorContent: AuthErrorContent | null = error\n ? // biome-ignore lint/style/noNestedTernary: <explanation>\n typeof error === 'string'\n ? { title: 'Error', description: error }\n : error\n : null;\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex size-8 mb-6 w-full items-center justify-center rounded-md\">\n {/** biome-ignore lint/performance/noImgElement: logo image */}\n <img src={logoImage || ''} alt=\"Jiret\" width={42} height={42} />\n </div>\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{title}</h1>\n {description && (\n <p className=\"mt-2 text-sm text-muted-foreground\">{description}</p>\n )}\n </div>\n\n {children}\n\n {errorContent && (\n <Alert variant=\"destructive\">\n <IconAlertCircle className=\"h-4 w-4\" />\n <AlertTitle>{errorContent.title}</AlertTitle>\n <AlertDescription>{errorContent.description}</AlertDescription>\n </Alert>\n )}\n <div className=\"mt-2 w-full\">\n {footer && (\n <div className=\"w-full text-center text-sm text-muted-foreground\">\n {footer}\n </div>\n )}\n </div>\n </div>\n );\n};\n","'use client';\n\nimport { zodResolver } from '@hookform/resolvers/zod';\nimport { Button } from '@mesob/ui/components/button';\nimport {\n Form,\n FormControl,\n FormField,\n FormItem,\n FormLabel,\n FormMessage,\n} from '@mesob/ui/components/form';\nimport { Input } from '@mesob/ui/components/input';\nimport {\n InputOTP,\n InputOTPGroup,\n InputOTPSlot,\n} from '@mesob/ui/components/input-otp';\nimport { Spinner } from '@mesob/ui/components/spinner';\nimport { IconEye, IconEyeOff } from '@tabler/icons-react';\nimport { useTranslations } from 'next-intl';\nimport { useMemo, useState } from 'react';\nimport { useForm } from 'react-hook-form';\nimport { z } from 'zod';\nimport type { AuthErrorContent } from '../../utils/handle-error';\n\ntype ResetPasswordFormValues = {\n code: string;\n password: string;\n confirmPassword: string;\n};\n\ntype ResetPasswordFormProps = {\n verificationId: string;\n onSubmit: (values: ResetPasswordFormValues) => Promise<void> | void;\n isLoading?: boolean;\n error?: AuthErrorContent | string | null;\n};\n\nexport const ResetPasswordForm = ({\n onSubmit,\n isLoading = false,\n}: ResetPasswordFormProps) => {\n const t = useTranslations('Auth.resetPassword');\n const [showPassword, setShowPassword] = useState(false);\n const resetPasswordSchema = useMemo(\n () =>\n z\n .object({\n code: z.string().length(6, t('errors.codeLength')),\n password: z.string().min(8, t('errors.passwordLength')),\n confirmPassword: z.string(),\n })\n .refine((data) => data.password === data.confirmPassword, {\n message: t('errors.passwordsMismatch'),\n path: ['confirmPassword'],\n }),\n [t],\n );\n\n const form = useForm<ResetPasswordFormValues>({\n resolver: zodResolver(resetPasswordSchema),\n defaultValues: {\n code: '',\n password: '',\n confirmPassword: '',\n },\n });\n\n const handleSubmit = form.handleSubmit(async (values) => {\n await onSubmit(values);\n });\n\n return (\n <Form {...form}>\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <FormField\n control={form.control}\n name=\"code\"\n render={({ field }) => (\n <FormItem>\n <div className=\"flex justify-center\">\n <FormLabel>{t('form.codeLabel')}</FormLabel>\n </div>\n <FormControl>\n <div className=\"flex mt-2 justify-center\">\n <InputOTP maxLength={6} {...field} containerClassName=\"gap-4\">\n <InputOTPGroup className=\"gap-3 *:data-[slot=input-otp-slot]:h-12 *:data-[slot=input-otp-slot]:w-12 *:data-[slot=input-otp-slot]:rounded-md *:data-[slot=input-otp-slot]:border *:data-[slot=input-otp-slot]:text-xl\">\n <InputOTPSlot className=\"h-12\" index={0} />\n <InputOTPSlot className=\"h-12\" index={1} />\n <InputOTPSlot className=\"h-12\" index={2} />\n <InputOTPSlot className=\"h-12\" index={3} />\n <InputOTPSlot className=\"h-12\" index={4} />\n <InputOTPSlot className=\"h-12\" index={5} />\n </InputOTPGroup>\n </InputOTP>\n </div>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"password\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>{t('form.passwordLabel')}</FormLabel>\n <FormControl>\n <div className=\"relative\">\n <Input\n type={showPassword ? 'text' : 'password'}\n placeholder={t('form.passwordPlaceholder')}\n {...field}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <IconEyeOff className=\"h-4 w-4\" />\n ) : (\n <IconEye className=\"h-4 w-4\" />\n )}\n </button>\n </div>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"confirmPassword\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>{t('form.confirmPasswordLabel')}</FormLabel>\n <FormControl>\n <div className=\"relative\">\n <Input\n type={showPassword ? 'text' : 'password'}\n placeholder={t('form.passwordPlaceholder')}\n {...field}\n />\n <button\n type=\"button\"\n onClick={() => setShowPassword(!showPassword)}\n className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground\"\n >\n {showPassword ? (\n <IconEyeOff className=\"h-4 w-4\" />\n ) : (\n <IconEye className=\"h-4 w-4\" />\n )}\n </button>\n </div>\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <Button type=\"submit\" className=\"w-full\" disabled={isLoading}>\n {isLoading && <Spinner />}\n {isLoading ? t('form.submitting') : t('form.submit')}\n </Button>\n </form>\n </Form>\n );\n};\n"],"mappings":"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","names":["useTranslations","useState","jsx","useState","jsx","jsxs","jsx","jsxs","useTranslations","useState"]}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- // src/components/pages/sign-in-page.tsx
3
+ // src/components/auth/pages/sign-in-page.tsx
4
4
  import { useTranslations as useTranslations2 } from "next-intl";
5
5
  import { useState as useState3 } from "react";
6
6
 
@@ -37,40 +37,95 @@ var AuthError = class extends Error {
37
37
  };
38
38
 
39
39
  // src/constants/auth.error.codes.ts
40
- var validCodes = [
41
- "USER_NOT_FOUND",
42
- "INVALID_PASSWORD",
43
- "USER_EXISTS",
44
- "VERIFICATION_EXPIRED",
45
- "VERIFICATION_MISMATCH",
46
- "VERIFICATION_NOT_FOUND",
47
- "TOO_MANY_ATTEMPTS",
48
- "REQUIRES_VERIFICATION",
49
- "UNAUTHORIZED",
50
- "ACCESS_DENIED",
51
- "HAS_NO_PASSWORD"
52
- ];
40
+ var AUTH_ERROR_MAPPING = {
41
+ USER_NOT_FOUND: {
42
+ title: "Account Not Found",
43
+ description: "We could not find an account with that identifier. Please check your spelling or sign up."
44
+ },
45
+ INVALID_PASSWORD: {
46
+ title: "Invalid Password",
47
+ description: "The password you entered is incorrect. Please try again."
48
+ },
49
+ USER_EXISTS: {
50
+ title: "Account Already Exists",
51
+ description: "An account with this identifier already exists. Please sign in instead."
52
+ },
53
+ VERIFICATION_EXPIRED: {
54
+ title: "Verification Expired",
55
+ description: "The verification code or link has expired. Please request a new one."
56
+ },
57
+ VERIFICATION_MISMATCH: {
58
+ title: "Invalid Code",
59
+ description: "The verification code you entered is invalid. Please double-check and try again."
60
+ },
61
+ VERIFICATION_NOT_FOUND: {
62
+ title: "Verification Not Found",
63
+ description: "We could not find a pending verification request. Please restart the process."
64
+ },
65
+ TOO_MANY_ATTEMPTS: {
66
+ title: "Too Many Attempts",
67
+ description: "You have made too many requests recently. Please wait a moment before trying again."
68
+ },
69
+ REQUIRES_VERIFICATION: {
70
+ title: "Verification Required",
71
+ description: "You need to verify your account before you can continue. Please check your email or phone."
72
+ },
73
+ UNAUTHORIZED: {
74
+ title: "Unauthorized",
75
+ description: "You are not authorized to perform this action. Please sign in again."
76
+ },
77
+ ACCESS_DENIED: {
78
+ title: "Access Denied",
79
+ description: "You do not have permission to access this resource. Please contact support if you believe this is an error."
80
+ },
81
+ HAS_NO_PASSWORD: {
82
+ title: "No Password Set",
83
+ description: "Your account does not have a password set (e.g. social login). Please sign in with your provider or reset your password."
84
+ }
85
+ };
86
+ var validCodes = Object.keys(AUTH_ERROR_MAPPING);
53
87
 
54
88
  // src/utils/handle-error.ts
55
89
  var handleError = (err, setError, t) => {
56
90
  if (err instanceof AuthError) {
57
- let errorKey = "errors.fallback";
58
- if (err.code) {
59
- errorKey = `errors.${err.code.toLowerCase()}`;
91
+ let errorCode = "";
92
+ if (err.code && validCodes.includes(err.code)) {
93
+ errorCode = err.code;
60
94
  } else if (err.message) {
61
95
  const messageUpper = err.message.toUpperCase().trim();
62
96
  if (validCodes.includes(messageUpper)) {
63
- errorKey = `errors.${messageUpper.toLowerCase()}`;
97
+ errorCode = messageUpper;
64
98
  }
65
99
  }
66
- setError(t(errorKey, { defaultValue: err.message }));
100
+ if (errorCode && AUTH_ERROR_MAPPING[errorCode]) {
101
+ const mapping = AUTH_ERROR_MAPPING[errorCode];
102
+ setError({
103
+ title: mapping.title,
104
+ description: mapping.description
105
+ });
106
+ return;
107
+ }
108
+ setError({
109
+ title: t("errors.fallback"),
110
+ // or 'Error'
111
+ description: err.message || t("errors.fallback")
112
+ });
67
113
  } else {
68
- setError(err instanceof Error ? err.message : t("errors.fallback"));
114
+ const message = err instanceof Error ? err.message : t("errors.fallback");
115
+ setError({
116
+ title: "Error",
117
+ description: message
118
+ });
69
119
  }
70
120
  };
71
121
 
72
- // src/components/auth-page-layout.tsx
73
- import { Alert, AlertDescription } from "@mesob/ui/components/alert";
122
+ // src/components/auth/auth-page-layout.tsx
123
+ import {
124
+ Alert,
125
+ AlertDescription,
126
+ AlertTitle
127
+ } from "@mesob/ui/components/alert";
128
+ import { IconAlertCircle } from "@tabler/icons-react";
74
129
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
75
130
  var AuthPageLayout = ({
76
131
  title,
@@ -80,6 +135,10 @@ var AuthPageLayout = ({
80
135
  footer,
81
136
  logoImage
82
137
  }) => {
138
+ const errorContent = error ? (
139
+ // biome-ignore lint/style/noNestedTernary: <explanation>
140
+ typeof error === "string" ? { title: "Error", description: error } : error
141
+ ) : null;
83
142
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
84
143
  /* @__PURE__ */ jsx2("div", { className: "flex size-8 mb-6 w-full items-center justify-center rounded-md", children: /* @__PURE__ */ jsx2("img", { src: logoImage || "", alt: "Jiret", width: 42, height: 42 }) }),
85
144
  /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
@@ -87,12 +146,16 @@ var AuthPageLayout = ({
87
146
  description && /* @__PURE__ */ jsx2("p", { className: "mt-2 text-sm text-muted-foreground", children: description })
88
147
  ] }),
89
148
  children,
90
- error && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: error }) }),
149
+ errorContent && /* @__PURE__ */ jsxs(Alert, { variant: "destructive", children: [
150
+ /* @__PURE__ */ jsx2(IconAlertCircle, { className: "h-4 w-4" }),
151
+ /* @__PURE__ */ jsx2(AlertTitle, { children: errorContent.title }),
152
+ /* @__PURE__ */ jsx2(AlertDescription, { children: errorContent.description })
153
+ ] }),
91
154
  /* @__PURE__ */ jsx2("div", { className: "mt-2 w-full", children: footer && /* @__PURE__ */ jsx2("div", { className: "w-full text-center text-sm text-muted-foreground", children: footer }) })
92
155
  ] });
93
156
  };
94
157
 
95
- // src/components/sign-in.tsx
158
+ // src/components/auth/sign-in.tsx
96
159
  import { zodResolver } from "@hookform/resolvers/zod";
97
160
  import { Button } from "@mesob/ui/components/button";
98
161
  import {
@@ -104,11 +167,32 @@ import {
104
167
  FormMessage
105
168
  } from "@mesob/ui/components/form";
106
169
  import { Input } from "@mesob/ui/components/input";
107
- import { Eye, EyeOff } from "lucide-react";
170
+ import { Spinner } from "@mesob/ui/components/spinner";
171
+ import { IconEye, IconEyeOff } from "@tabler/icons-react";
108
172
  import { useTranslations } from "next-intl";
109
173
  import { useMemo, useState as useState2 } from "react";
110
174
  import { useForm } from "react-hook-form";
111
175
  import { z } from "zod";
176
+
177
+ // src/utils/normalize-phone.ts
178
+ function normalizePhone(phone) {
179
+ const cleaned = phone.trim().replace(/\s/g, "");
180
+ if (cleaned.startsWith("+2519")) {
181
+ return cleaned;
182
+ }
183
+ if (cleaned.startsWith("2519")) {
184
+ return `+${cleaned}`;
185
+ }
186
+ if (cleaned.startsWith("09")) {
187
+ return `+251${cleaned.slice(1)}`;
188
+ }
189
+ if (cleaned.startsWith("9") && cleaned.length === 9) {
190
+ return `+251${cleaned}`;
191
+ }
192
+ return cleaned;
193
+ }
194
+
195
+ // src/components/auth/sign-in.tsx
112
196
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
113
197
  var SignIn = ({
114
198
  onSubmit,
@@ -121,7 +205,17 @@ var SignIn = ({
121
205
  const [showPassword, setShowPassword] = useState2(false);
122
206
  const identifierSchema = useMemo(
123
207
  () => z.object({
124
- account: z.string().min(1, t("errors.accountRequired")).email(t("errors.invalidEmail"))
208
+ account: z.string().trim().min(1, { message: t("errors.requiredField") }).refine(
209
+ (val) => {
210
+ const isEmail = z.string().email().safeParse(val).success;
211
+ const phoneRegex = /^(\+2519|2519|09)\d{8}$/;
212
+ const isPhone2 = phoneRegex.test(val);
213
+ return isEmail || isPhone2;
214
+ },
215
+ {
216
+ message: t("errors.invalidEmailOrPhone")
217
+ }
218
+ )
125
219
  }),
126
220
  [t]
127
221
  );
@@ -144,7 +238,9 @@ var SignIn = ({
144
238
  }
145
239
  });
146
240
  const handleIdentifierSubmit = identifierForm.handleSubmit(async (values) => {
147
- await onSubmit({ account: values.account, password: "" }, "identifier");
241
+ const phoneRegex = /^(\+2519|2519|09)\d{8}$/;
242
+ const normalizedAccount = phoneRegex.test(values.account) ? normalizePhone(values.account) : values.account;
243
+ await onSubmit({ account: normalizedAccount, password: "" }, "identifier");
148
244
  });
149
245
  const handlePasswordSubmit = passwordForm.handleSubmit(async (values) => {
150
246
  await onSubmit(
@@ -171,6 +267,7 @@ var SignIn = ({
171
267
  {
172
268
  type: showPassword ? "text" : "password",
173
269
  placeholder: t("form.passwordPlaceholder"),
270
+ autoComplete: "current-password",
174
271
  ...field
175
272
  }
176
273
  ),
@@ -180,7 +277,7 @@ var SignIn = ({
180
277
  type: "button",
181
278
  onClick: () => setShowPassword(!showPassword),
182
279
  className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
183
- children: showPassword ? /* @__PURE__ */ jsx3(EyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(Eye, { className: "h-4 w-4" })
280
+ children: showPassword ? /* @__PURE__ */ jsx3(IconEyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx3(IconEye, { className: "h-4 w-4" })
184
281
  }
185
282
  )
186
283
  ] }) }),
@@ -211,11 +308,14 @@ var SignIn = ({
211
308
  ] })
212
309
  }
213
310
  ),
214
- /* @__PURE__ */ jsx3(Button, { type: "submit", className: "w-full", disabled: isLoading, children: isLoading ? t("form.submitting") : t("form.continue") })
311
+ /* @__PURE__ */ jsxs2(Button, { type: "submit", className: "w-full", disabled: isLoading, children: [
312
+ isLoading && /* @__PURE__ */ jsx3(Spinner, {}),
313
+ isLoading ? t("form.submitting") : t("form.continue")
314
+ ] })
215
315
  ] }) });
216
316
  };
217
317
 
218
- // src/components/pages/sign-in-page.tsx
318
+ // src/components/auth/pages/sign-in-page.tsx
219
319
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
220
320
  var isPhone = (s) => /^\+?[0-9()[\]\s-]{6,}$/.test(s);
221
321
  var SignInPage = ({