@mesob/auth-react 0.3.5 → 0.4.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 (124) hide show
  1. package/dist/components/auth/forgot-password.js +5 -1
  2. package/dist/components/auth/forgot-password.js.map +1 -1
  3. package/dist/components/auth/reset-password-form.js +5 -1
  4. package/dist/components/auth/reset-password-form.js.map +1 -1
  5. package/dist/components/auth/set-password.d.ts +9 -0
  6. package/dist/components/auth/set-password.js +527 -0
  7. package/dist/components/auth/set-password.js.map +1 -0
  8. package/dist/components/auth/sign-in.js +22 -1
  9. package/dist/components/auth/sign-in.js.map +1 -1
  10. package/dist/components/auth/sign-up.js +7 -5
  11. package/dist/components/auth/sign-up.js.map +1 -1
  12. package/dist/components/auth/verify-email.js +5 -1
  13. package/dist/components/auth/verify-email.js.map +1 -1
  14. package/dist/components/auth/verify-phone.js +5 -1
  15. package/dist/components/auth/verify-phone.js.map +1 -1
  16. package/dist/components/authorization/deny.d.ts +11 -0
  17. package/dist/components/authorization/deny.js +52 -0
  18. package/dist/components/authorization/deny.js.map +1 -0
  19. package/dist/components/authorization/grant.d.ts +12 -0
  20. package/dist/components/authorization/grant.js +57 -0
  21. package/dist/components/authorization/grant.js.map +1 -0
  22. package/dist/components/error-boundary.d.ts +2 -2
  23. package/dist/components/iam/permission-selector.d.ts +19 -0
  24. package/dist/components/iam/permission-selector.js +122 -0
  25. package/dist/components/iam/permission-selector.js.map +1 -0
  26. package/dist/components/iam/permissions.js +12 -31
  27. package/dist/components/iam/permissions.js.map +1 -1
  28. package/dist/components/iam/role-detail-layout.d.ts +11 -0
  29. package/dist/components/iam/role-detail-layout.js +137 -0
  30. package/dist/components/iam/role-detail-layout.js.map +1 -0
  31. package/dist/components/iam/role-detail-page.d.ts +9 -0
  32. package/dist/components/iam/role-detail-page.js +229 -0
  33. package/dist/components/iam/role-detail-page.js.map +1 -0
  34. package/dist/components/iam/role-permissions-page.d.ts +8 -0
  35. package/dist/components/iam/role-permissions-page.js +397 -0
  36. package/dist/components/iam/role-permissions-page.js.map +1 -0
  37. package/dist/components/iam/roles.js +11 -8
  38. package/dist/components/iam/roles.js.map +1 -1
  39. package/dist/components/iam/users.js +1 -7
  40. package/dist/components/iam/users.js.map +1 -1
  41. package/dist/components/profile/account.js +110 -19
  42. package/dist/components/profile/account.js.map +1 -1
  43. package/dist/components/profile/change-profile.d.ts +2 -1
  44. package/dist/components/profile/change-profile.js +16 -8
  45. package/dist/components/profile/change-profile.js.map +1 -1
  46. package/dist/components/profile/security.js +51 -17
  47. package/dist/components/profile/security.js.map +1 -1
  48. package/dist/index.d.ts +9 -1
  49. package/dist/index.js +1813 -725
  50. package/dist/index.js.map +1 -1
  51. package/dist/pages/auth/forgot-password.d.ts +7 -0
  52. package/dist/pages/auth/forgot-password.js +784 -0
  53. package/dist/pages/auth/forgot-password.js.map +1 -0
  54. package/dist/pages/auth/layout.d.ts +8 -0
  55. package/dist/pages/auth/layout.js +562 -0
  56. package/dist/pages/auth/layout.js.map +1 -0
  57. package/dist/pages/auth/reset-password.d.ts +10 -0
  58. package/dist/pages/auth/reset-password.js +913 -0
  59. package/dist/pages/auth/reset-password.js.map +1 -0
  60. package/dist/pages/auth/set-password.d.ts +10 -0
  61. package/dist/pages/auth/set-password.js +946 -0
  62. package/dist/pages/auth/set-password.js.map +1 -0
  63. package/dist/pages/auth/sign-in.d.ts +10 -0
  64. package/dist/pages/auth/sign-in.js +984 -0
  65. package/dist/pages/auth/sign-in.js.map +1 -0
  66. package/dist/pages/auth/sign-up.d.ts +10 -0
  67. package/dist/pages/auth/sign-up.js +940 -0
  68. package/dist/pages/auth/sign-up.js.map +1 -0
  69. package/dist/pages/auth/verify-email.d.ts +10 -0
  70. package/dist/pages/auth/verify-email.js +950 -0
  71. package/dist/pages/auth/verify-email.js.map +1 -0
  72. package/dist/pages/auth/verify-phone.d.ts +10 -0
  73. package/dist/pages/auth/verify-phone.js +964 -0
  74. package/dist/pages/auth/verify-phone.js.map +1 -0
  75. package/dist/pages/iam/permissions.d.ts +5 -0
  76. package/dist/pages/iam/permissions.js +308 -0
  77. package/dist/pages/iam/permissions.js.map +1 -0
  78. package/dist/pages/iam/role-detail-layout.d.ts +12 -0
  79. package/dist/pages/iam/role-detail-layout.js +145 -0
  80. package/dist/pages/iam/role-detail-layout.js.map +1 -0
  81. package/dist/pages/iam/role-detail.d.ts +12 -0
  82. package/dist/pages/iam/role-detail.js +241 -0
  83. package/dist/pages/iam/role-detail.js.map +1 -0
  84. package/dist/pages/iam/role-permissions.d.ts +12 -0
  85. package/dist/pages/iam/role-permissions.js +409 -0
  86. package/dist/pages/iam/role-permissions.js.map +1 -0
  87. package/dist/pages/iam/role-users.d.ts +12 -0
  88. package/dist/pages/iam/role-users.js +825 -0
  89. package/dist/pages/iam/role-users.js.map +1 -0
  90. package/dist/pages/iam/roles.d.ts +5 -0
  91. package/dist/pages/iam/roles.js +684 -0
  92. package/dist/pages/iam/roles.js.map +1 -0
  93. package/dist/pages/iam/sessions.d.ts +5 -0
  94. package/dist/pages/iam/sessions.js +315 -0
  95. package/dist/pages/iam/sessions.js.map +1 -0
  96. package/dist/pages/iam/tenant-detail.d.ts +10 -0
  97. package/dist/pages/iam/tenant-detail.js +186 -0
  98. package/dist/pages/iam/tenant-detail.js.map +1 -0
  99. package/dist/pages/iam/tenants.d.ts +5 -0
  100. package/dist/pages/iam/tenants.js +610 -0
  101. package/dist/pages/iam/tenants.js.map +1 -0
  102. package/dist/pages/iam/user-activity.d.ts +10 -0
  103. package/dist/pages/iam/user-activity.js +850 -0
  104. package/dist/pages/iam/user-activity.js.map +1 -0
  105. package/dist/pages/iam/user-detail-layout.d.ts +12 -0
  106. package/dist/pages/iam/user-detail-layout.js +106 -0
  107. package/dist/pages/iam/user-detail-layout.js.map +1 -0
  108. package/dist/pages/iam/user-detail.d.ts +10 -0
  109. package/dist/pages/iam/user-detail.js +102 -0
  110. package/dist/pages/iam/user-detail.js.map +1 -0
  111. package/dist/pages/iam/users.d.ts +5 -0
  112. package/dist/pages/iam/users.js +1275 -0
  113. package/dist/pages/iam/users.js.map +1 -0
  114. package/dist/pages/profile/account.d.ts +5 -0
  115. package/dist/pages/profile/account.js +182 -0
  116. package/dist/pages/profile/account.js.map +1 -0
  117. package/dist/pages/profile/layout.d.ts +8 -0
  118. package/dist/pages/profile/layout.js +133 -0
  119. package/dist/pages/profile/layout.js.map +1 -0
  120. package/dist/pages/profile/security.d.ts +5 -0
  121. package/dist/pages/profile/security.js +1539 -0
  122. package/dist/pages/profile/security.js.map +1 -0
  123. package/dist/{types-vcfvnAzQ.d.ts → types-g9QcNRxT.d.ts} +13 -7
  124. package/package.json +102 -3
@@ -0,0 +1,1539 @@
1
+ "use client";
2
+
3
+ // src/pages/profile/security.tsx
4
+ import {
5
+ PageBody,
6
+ PageContainer,
7
+ PageTitle,
8
+ useBreadcrumbs
9
+ } from "@mesob/ui/components";
10
+ import { IconShieldLock } from "@tabler/icons-react";
11
+
12
+ // src/components/profile/security.tsx
13
+ import { Badge, Card, CardContent, Separator } from "@mesob/ui/components";
14
+
15
+ // src/provider.tsx
16
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
17
+ import { deepmerge } from "deepmerge-ts";
18
+ import createFetchClient from "openapi-fetch";
19
+ import createClient from "openapi-react-query";
20
+ import { createContext, useContext, useMemo, useState } from "react";
21
+
22
+ // src/lib/translations.ts
23
+ function createTranslator(messages, namespace) {
24
+ return (key, params) => {
25
+ const fullKey = namespace ? `${namespace}.${key}` : key;
26
+ const keys = fullKey.split(".");
27
+ let value = messages;
28
+ for (const k of keys) {
29
+ if (value && typeof value === "object" && value !== null) {
30
+ value = value[k];
31
+ } else {
32
+ return fullKey;
33
+ }
34
+ }
35
+ if (typeof value !== "string") {
36
+ return fullKey;
37
+ }
38
+ if (params) {
39
+ return value.replace(
40
+ /\{(\w+)\}/g,
41
+ (_, param) => String(params[param] ?? `{${param}}`)
42
+ );
43
+ }
44
+ return value;
45
+ };
46
+ }
47
+
48
+ // src/utils/cookie.ts
49
+ var isProduction = typeof process !== "undefined" && process.env.NODE_ENV === "production";
50
+
51
+ // src/provider.tsx
52
+ import { jsx } from "react/jsx-runtime";
53
+ var SessionContext = createContext(null);
54
+ var ApiContext = createContext(null);
55
+ var ConfigContext = createContext(null);
56
+ var queryClient = new QueryClient({
57
+ defaultOptions: {
58
+ queries: {
59
+ refetchOnWindowFocus: false
60
+ }
61
+ }
62
+ });
63
+ function useSession() {
64
+ const context = useContext(SessionContext);
65
+ if (!context) {
66
+ throw new Error("useSession must be used within MesobAuthProvider");
67
+ }
68
+ return context;
69
+ }
70
+ function useApi() {
71
+ const context = useContext(ApiContext);
72
+ if (!context) {
73
+ throw new Error("useApi must be used within MesobAuthProvider");
74
+ }
75
+ return context;
76
+ }
77
+ function useConfig() {
78
+ const context = useContext(ConfigContext);
79
+ if (!context) {
80
+ throw new Error("useConfig must be used within MesobAuthProvider");
81
+ }
82
+ return context;
83
+ }
84
+
85
+ // src/components/profile/change-email-form.tsx
86
+ import {
87
+ Button as Button3,
88
+ Collapsible,
89
+ CollapsibleContent,
90
+ CollapsibleTrigger
91
+ } from "@mesob/ui/components";
92
+ import { IconChevronDown } from "@tabler/icons-react";
93
+ import { useState as useState5 } from "react";
94
+
95
+ // src/components/profile/request-change-email-form.tsx
96
+ import { zodResolver } from "@hookform/resolvers/zod";
97
+ import { Button, Input, Label, Spinner } from "@mesob/ui/components";
98
+ import { IconEye, IconEyeOff } from "@tabler/icons-react";
99
+ import { useEffect, useState as useState2 } from "react";
100
+ import { useForm } from "react-hook-form";
101
+ import { toast } from "sonner";
102
+ import { z } from "zod";
103
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
104
+ var emailPasswordSchema = z.object({
105
+ email: z.string().email("Invalid email address"),
106
+ password: z.string().min(8, "Password must be at least 8 characters").max(128, "Password too long")
107
+ });
108
+ function isAuthError(error) {
109
+ return typeof error === "object" && error !== null && ("code" in error || "message" in error || "name" in error);
110
+ }
111
+ function getErrorCode(error) {
112
+ if (error.code) {
113
+ return error.code;
114
+ }
115
+ if (error.message) {
116
+ const upperMessage = error.message.toUpperCase().trim();
117
+ const validCodes = [
118
+ "USER_NOT_FOUND",
119
+ "USER_EXISTS",
120
+ "INVALID_PASSWORD",
121
+ "VERIFICATION_EXPIRED",
122
+ "VERIFICATION_MISMATCH",
123
+ "VERIFICATION_NOT_FOUND",
124
+ "TOO_MANY_ATTEMPTS",
125
+ "UNAUTHORIZED"
126
+ ];
127
+ if (validCodes.includes(upperMessage)) {
128
+ return upperMessage;
129
+ }
130
+ }
131
+ return void 0;
132
+ }
133
+ function getErrorMessage(error) {
134
+ if (isAuthError(error)) {
135
+ const errorCode = getErrorCode(error);
136
+ switch (errorCode) {
137
+ case "USER_EXISTS":
138
+ return "This email is already taken. Please use a different email.";
139
+ case "VERIFICATION_EXPIRED":
140
+ return "Verification code has expired. Please request a new one.";
141
+ case "VERIFICATION_MISMATCH":
142
+ return "Invalid verification code. Please try again.";
143
+ case "VERIFICATION_NOT_FOUND":
144
+ return "Verification not found. Please request a new code.";
145
+ default:
146
+ return error.message || "An error occurred. Please try again.";
147
+ }
148
+ }
149
+ if (error instanceof Error) {
150
+ return error.message;
151
+ }
152
+ return "An error occurred. Please try again.";
153
+ }
154
+ function RequestChangeEmailForm({
155
+ onSuccess,
156
+ onCancel,
157
+ buttonText
158
+ }) {
159
+ const { user } = useSession();
160
+ const { hooks } = useApi();
161
+ const [isSubmitting, setIsSubmitting] = useState2(false);
162
+ const [isChecking, setIsChecking] = useState2(true);
163
+ const [showPassword, setShowPassword] = useState2(false);
164
+ const getPendingAccountChangeQuery = hooks.useQuery(
165
+ "get",
166
+ "/account-change/pending",
167
+ {},
168
+ { enabled: false }
169
+ );
170
+ const verifyPasswordMutation = hooks.useMutation("post", "/password/verify");
171
+ const checkUserMutation = hooks.useMutation("post", "/check-account");
172
+ const requestEmailVerificationMutation = hooks.useMutation(
173
+ "post",
174
+ "/email/verification/request"
175
+ );
176
+ const emailPasswordForm = useForm({
177
+ resolver: zodResolver(emailPasswordSchema),
178
+ defaultValues: {
179
+ email: "",
180
+ password: ""
181
+ }
182
+ });
183
+ const {
184
+ register,
185
+ handleSubmit,
186
+ getValues,
187
+ setValue,
188
+ formState: { errors }
189
+ } = emailPasswordForm;
190
+ useEffect(() => {
191
+ let active = true;
192
+ const run = async () => {
193
+ try {
194
+ const data = await getPendingAccountChangeQuery.refetch();
195
+ if (!active) {
196
+ return;
197
+ }
198
+ const accountChange = data.data?.accountChange;
199
+ const verificationId = data.data?.verificationId;
200
+ if (accountChange?.changeType !== "email") {
201
+ setIsChecking(false);
202
+ return;
203
+ }
204
+ if (!accountChange.newEmail) {
205
+ setIsChecking(false);
206
+ return;
207
+ }
208
+ if (getValues("email")) {
209
+ setIsChecking(false);
210
+ return;
211
+ }
212
+ setValue("email", accountChange.newEmail, { shouldValidate: true });
213
+ if (verificationId) {
214
+ toast.message("Resuming verification\u2026");
215
+ onSuccess(verificationId, accountChange.newEmail);
216
+ return;
217
+ }
218
+ setIsChecking(false);
219
+ } catch {
220
+ setIsChecking(false);
221
+ }
222
+ };
223
+ run().catch(() => void 0);
224
+ return () => {
225
+ active = false;
226
+ };
227
+ }, [getPendingAccountChangeQuery.refetch, getValues, onSuccess, setValue]);
228
+ const onEmailPasswordSubmit = async (data) => {
229
+ if (!user) {
230
+ toast.error("User not found");
231
+ return;
232
+ }
233
+ try {
234
+ setIsSubmitting(true);
235
+ await verifyPasswordMutation.mutateAsync({
236
+ body: { password: data.password }
237
+ });
238
+ const checkResult = await checkUserMutation.mutateAsync({
239
+ body: { identifier: data.email }
240
+ });
241
+ if (checkResult.data?.exists) {
242
+ if (user?.email?.toLowerCase() === data.email.toLowerCase()) {
243
+ toast.error("This is already your current email address.");
244
+ return;
245
+ }
246
+ toast.error(
247
+ "This email is already taken. Please use a different email."
248
+ );
249
+ return;
250
+ }
251
+ const verification = await requestEmailVerificationMutation.mutateAsync({
252
+ body: { email: data.email }
253
+ });
254
+ toast.success("Verification code sent to your email");
255
+ onSuccess(verification.data?.verificationId ?? "", data.email);
256
+ } catch (error) {
257
+ const errorMessage = getErrorMessage(error);
258
+ if (isAuthError(error)) {
259
+ const errorCode = getErrorCode(error);
260
+ if (errorCode === "INVALID_PASSWORD" || errorCode === "USER_NOT_FOUND") {
261
+ toast.error("Incorrect password. Please try again.");
262
+ return;
263
+ }
264
+ }
265
+ toast.error(errorMessage);
266
+ } finally {
267
+ setIsSubmitting(false);
268
+ }
269
+ };
270
+ const isLoading = isSubmitting || isChecking;
271
+ return /* @__PURE__ */ jsxs(
272
+ "form",
273
+ {
274
+ onSubmit: handleSubmit(onEmailPasswordSubmit),
275
+ className: "p-4 space-y-4 border-t",
276
+ children: [
277
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4 w-full md:w-1/2", children: [
278
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
279
+ /* @__PURE__ */ jsx2(Label, { htmlFor: "email", children: "New Email Address" }),
280
+ /* @__PURE__ */ jsx2(
281
+ Input,
282
+ {
283
+ id: "email",
284
+ type: "email",
285
+ placeholder: "Enter your new email",
286
+ ...register("email"),
287
+ disabled: isLoading
288
+ }
289
+ ),
290
+ errors.email && /* @__PURE__ */ jsx2("p", { className: "text-sm text-destructive", children: errors.email.message })
291
+ ] }),
292
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
293
+ /* @__PURE__ */ jsx2(Label, { htmlFor: "password", children: "Password" }),
294
+ /* @__PURE__ */ jsxs("div", { className: "relative", children: [
295
+ /* @__PURE__ */ jsx2(
296
+ Input,
297
+ {
298
+ id: "password",
299
+ type: showPassword ? "text" : "password",
300
+ autoComplete: "current-password",
301
+ placeholder: "Enter your password",
302
+ ...register("password"),
303
+ disabled: isLoading
304
+ }
305
+ ),
306
+ /* @__PURE__ */ jsx2(
307
+ "button",
308
+ {
309
+ type: "button",
310
+ onClick: () => setShowPassword(!showPassword),
311
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
312
+ disabled: isLoading,
313
+ children: showPassword ? /* @__PURE__ */ jsx2(IconEyeOff, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx2(IconEye, { className: "h-4 w-4" })
314
+ }
315
+ )
316
+ ] }),
317
+ errors.password && /* @__PURE__ */ jsx2("p", { className: "text-sm text-destructive", children: errors.password.message })
318
+ ] })
319
+ ] }),
320
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
321
+ /* @__PURE__ */ jsx2(
322
+ Button,
323
+ {
324
+ type: "button",
325
+ variant: "outline",
326
+ onClick: onCancel,
327
+ disabled: isLoading,
328
+ children: "Cancel"
329
+ }
330
+ ),
331
+ /* @__PURE__ */ jsxs(Button, { type: "submit", disabled: isLoading, children: [
332
+ isLoading && /* @__PURE__ */ jsx2(Spinner, { className: "mr-2 h-4 w-4" }),
333
+ isChecking ? "Checking\u2026" : buttonText
334
+ ] })
335
+ ] })
336
+ ]
337
+ }
338
+ );
339
+ }
340
+
341
+ // src/components/profile/verify-change-email-form.tsx
342
+ import { useState as useState4 } from "react";
343
+ import { toast as toast2 } from "sonner";
344
+
345
+ // src/components/profile/otp-verification-modal.tsx
346
+ import {
347
+ Dialog,
348
+ DialogContent,
349
+ DialogDescription,
350
+ DialogHeader,
351
+ DialogTitle
352
+ } from "@mesob/ui/components";
353
+
354
+ // src/components/auth/verification-form.tsx
355
+ import { zodResolver as zodResolver2 } from "@hookform/resolvers/zod";
356
+ import {
357
+ Button as Button2,
358
+ Form,
359
+ FormControl,
360
+ FormField,
361
+ FormItem,
362
+ FormLabel,
363
+ FormMessage,
364
+ InputOTP,
365
+ InputOTPGroup,
366
+ InputOTPSlot
367
+ } from "@mesob/ui/components";
368
+ import { useForm as useForm2 } from "react-hook-form";
369
+ import { z as z2 } from "zod";
370
+
371
+ // src/hooks/use-translator.ts
372
+ import { useMesob } from "@mesob/ui/providers";
373
+ function useTranslator(namespace) {
374
+ const mesob = useMesob();
375
+ const { config } = useConfig();
376
+ if (mesob?.t) {
377
+ return (key, params) => {
378
+ const fullKey = namespace ? `${namespace}.${key}` : key;
379
+ return mesob.t?.(fullKey, params) ?? fullKey;
380
+ };
381
+ }
382
+ return createTranslator(config.messages || {}, namespace);
383
+ }
384
+
385
+ // src/components/auth/countdown.tsx
386
+ import { Spinner as Spinner2 } from "@mesob/ui/components";
387
+ import { useEffect as useEffect2, useState as useState3 } from "react";
388
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
389
+ var Countdown = ({
390
+ initialSeconds = 60,
391
+ onResend,
392
+ resending = false
393
+ }) => {
394
+ const t = useTranslator("Common");
395
+ const [seconds, setSeconds] = useState3(initialSeconds);
396
+ const [isResending, setIsResending] = useState3(false);
397
+ useEffect2(() => {
398
+ if (seconds <= 0) {
399
+ return;
400
+ }
401
+ const timer = setInterval(() => {
402
+ setSeconds((prev) => {
403
+ if (prev <= 1) {
404
+ clearInterval(timer);
405
+ return 0;
406
+ }
407
+ return prev - 1;
408
+ });
409
+ }, 1e3);
410
+ return () => clearInterval(timer);
411
+ }, [seconds]);
412
+ const handleResend = async () => {
413
+ setIsResending(true);
414
+ try {
415
+ await onResend();
416
+ setSeconds(initialSeconds);
417
+ } catch (_error) {
418
+ } finally {
419
+ setIsResending(false);
420
+ }
421
+ };
422
+ const busy = isResending || resending;
423
+ if (seconds > 0) {
424
+ return /* @__PURE__ */ jsx3("p", { className: "text-sm text-muted-foreground", children: t("resendIn", { seconds }) });
425
+ }
426
+ return /* @__PURE__ */ jsxs2(
427
+ "button",
428
+ {
429
+ type: "button",
430
+ onClick: handleResend,
431
+ disabled: busy,
432
+ className: "text-sm text-primary hover:underline disabled:opacity-50 flex items-center gap-1",
433
+ children: [
434
+ busy && /* @__PURE__ */ jsx3(Spinner2, { className: "h-3 w-3" }),
435
+ t("resend")
436
+ ]
437
+ }
438
+ );
439
+ };
440
+
441
+ // src/components/auth/verification-form.tsx
442
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
443
+ var verificationSchema = (t) => z2.object({
444
+ code: z2.string().length(6, t("form.codeLength"))
445
+ });
446
+ var VerificationForm = ({
447
+ onSubmit,
448
+ onResend,
449
+ isLoading = false
450
+ }) => {
451
+ const t = useTranslator("Auth.verification");
452
+ const form = useForm2({
453
+ resolver: zodResolver2(verificationSchema(t)),
454
+ defaultValues: { code: "" }
455
+ });
456
+ const handleSubmit = form.handleSubmit(async (values) => {
457
+ await onSubmit(values);
458
+ });
459
+ const codeLength = form.watch("code").length;
460
+ return /* @__PURE__ */ jsx4(Form, { ...form, children: /* @__PURE__ */ jsxs3(
461
+ "form",
462
+ {
463
+ id: "verification-form",
464
+ onSubmit: handleSubmit,
465
+ className: "space-y-4",
466
+ children: [
467
+ /* @__PURE__ */ jsx4(
468
+ FormField,
469
+ {
470
+ control: form.control,
471
+ name: "code",
472
+ render: ({ field }) => /* @__PURE__ */ jsxs3(FormItem, { children: [
473
+ /* @__PURE__ */ jsx4("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx4(FormLabel, { children: t("form.codeLabel") }) }),
474
+ /* @__PURE__ */ jsx4(FormControl, { children: /* @__PURE__ */ jsx4(
475
+ InputOTP,
476
+ {
477
+ maxLength: 6,
478
+ required: true,
479
+ value: field.value ?? "",
480
+ onChange: field.onChange,
481
+ onBlur: field.onBlur,
482
+ containerClassName: "gap-4 justify-center mb-2 flex items-center",
483
+ children: /* @__PURE__ */ jsxs3(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: [
484
+ /* @__PURE__ */ jsx4(InputOTPSlot, { className: "h-12", index: 0 }),
485
+ /* @__PURE__ */ jsx4(InputOTPSlot, { className: "h-12", index: 1 }),
486
+ /* @__PURE__ */ jsx4(InputOTPSlot, { className: "h-12", index: 2 }),
487
+ /* @__PURE__ */ jsx4(InputOTPSlot, { className: "h-12", index: 3 }),
488
+ /* @__PURE__ */ jsx4(InputOTPSlot, { className: "h-12", index: 4 }),
489
+ /* @__PURE__ */ jsx4(InputOTPSlot, { className: "h-12", index: 5 })
490
+ ] })
491
+ }
492
+ ) }),
493
+ /* @__PURE__ */ jsx4(FormMessage, {})
494
+ ] })
495
+ }
496
+ ),
497
+ /* @__PURE__ */ jsx4(
498
+ Button2,
499
+ {
500
+ type: "submit",
501
+ form: "verification-form",
502
+ className: "w-full",
503
+ disabled: isLoading || codeLength !== 6,
504
+ loading: isLoading,
505
+ children: t("form.confirm")
506
+ }
507
+ ),
508
+ /* @__PURE__ */ jsx4("div", { className: "flex justify-center", children: /* @__PURE__ */ jsx4(Countdown, { onResend, resending: isLoading }) })
509
+ ]
510
+ }
511
+ ) });
512
+ };
513
+
514
+ // src/components/profile/otp-verification-modal.tsx
515
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
516
+ function OtpVerificationModal({
517
+ open,
518
+ title,
519
+ description,
520
+ verificationId,
521
+ isLoading,
522
+ onSubmit,
523
+ onResend,
524
+ onCancel
525
+ }) {
526
+ return /* @__PURE__ */ jsx5(
527
+ Dialog,
528
+ {
529
+ open,
530
+ onOpenChange: (nextOpen) => {
531
+ if (!nextOpen) {
532
+ onCancel?.();
533
+ }
534
+ },
535
+ children: /* @__PURE__ */ jsxs4(DialogContent, { children: [
536
+ /* @__PURE__ */ jsxs4(DialogHeader, { children: [
537
+ /* @__PURE__ */ jsx5(DialogTitle, { children: title }),
538
+ description && /* @__PURE__ */ jsx5(DialogDescription, { children: description })
539
+ ] }),
540
+ /* @__PURE__ */ jsx5(
541
+ VerificationForm,
542
+ {
543
+ verificationId,
544
+ isLoading,
545
+ onSubmit: async ({ code }) => onSubmit(code),
546
+ onResend: onResend ?? (() => void 0)
547
+ }
548
+ )
549
+ ] })
550
+ }
551
+ );
552
+ }
553
+
554
+ // src/components/profile/verify-change-email-form.tsx
555
+ import { jsx as jsx6 } from "react/jsx-runtime";
556
+ function isAuthError2(error) {
557
+ return typeof error === "object" && error !== null && ("code" in error || "message" in error || "name" in error);
558
+ }
559
+ function getErrorCode2(error) {
560
+ if (error.code) {
561
+ return error.code;
562
+ }
563
+ if (error.message) {
564
+ const upperMessage = error.message.toUpperCase().trim();
565
+ const validCodes = [
566
+ "USER_NOT_FOUND",
567
+ "USER_EXISTS",
568
+ "INVALID_PASSWORD",
569
+ "VERIFICATION_EXPIRED",
570
+ "VERIFICATION_MISMATCH",
571
+ "VERIFICATION_NOT_FOUND",
572
+ "TOO_MANY_ATTEMPTS",
573
+ "UNAUTHORIZED"
574
+ ];
575
+ if (validCodes.includes(upperMessage)) {
576
+ return upperMessage;
577
+ }
578
+ }
579
+ return void 0;
580
+ }
581
+ function getErrorMessage2(error) {
582
+ if (isAuthError2(error)) {
583
+ const errorCode = getErrorCode2(error);
584
+ switch (errorCode) {
585
+ case "USER_EXISTS":
586
+ return "This email is already taken. Please use a different email.";
587
+ case "VERIFICATION_EXPIRED":
588
+ return "Verification code has expired. Please request a new one.";
589
+ case "VERIFICATION_MISMATCH":
590
+ return "Invalid verification code. Please try again.";
591
+ case "VERIFICATION_NOT_FOUND":
592
+ return "Verification not found. Please request a new code.";
593
+ default:
594
+ return error.message || "An error occurred. Please try again.";
595
+ }
596
+ }
597
+ if (error instanceof Error) {
598
+ return error.message;
599
+ }
600
+ return "An error occurred. Please try again.";
601
+ }
602
+ function VerifyChangeEmailForm({
603
+ email,
604
+ verificationId,
605
+ onSuccess,
606
+ onCancel
607
+ }) {
608
+ const { refresh } = useSession();
609
+ const { hooks } = useApi();
610
+ const [isSubmitting, setIsSubmitting] = useState4(false);
611
+ const [currentVerificationId, setCurrentVerificationId] = useState4(verificationId);
612
+ const verifyEmailMutation = hooks.useMutation(
613
+ "post",
614
+ "/email/verification/confirm"
615
+ );
616
+ const updateEmailMutation = hooks.useMutation("put", "/profile/email");
617
+ const requestEmailVerificationMutation = hooks.useMutation(
618
+ "post",
619
+ "/email/verification/request"
620
+ );
621
+ const onOtpSubmit = async (code) => {
622
+ if (!currentVerificationId) {
623
+ toast2.error("Verification not found. Please request a new code.");
624
+ return;
625
+ }
626
+ try {
627
+ setIsSubmitting(true);
628
+ await verifyEmailMutation.mutateAsync({
629
+ body: {
630
+ verificationId: currentVerificationId,
631
+ code
632
+ }
633
+ });
634
+ await updateEmailMutation.mutateAsync({
635
+ body: { email }
636
+ });
637
+ toast2.success("Email updated successfully");
638
+ await refresh();
639
+ onSuccess();
640
+ } catch (error) {
641
+ const errorMessage = getErrorMessage2(error);
642
+ toast2.error(errorMessage);
643
+ } finally {
644
+ setIsSubmitting(false);
645
+ }
646
+ };
647
+ if (!currentVerificationId) {
648
+ toast2.error("Verification not found. Please request a new code.");
649
+ return null;
650
+ }
651
+ return /* @__PURE__ */ jsx6(
652
+ OtpVerificationModal,
653
+ {
654
+ open: true,
655
+ title: "Verify email",
656
+ description: `Enter the verification code sent to ${email}`,
657
+ verificationId: currentVerificationId,
658
+ isLoading: isSubmitting,
659
+ onSubmit: onOtpSubmit,
660
+ onResend: async () => {
661
+ try {
662
+ setIsSubmitting(true);
663
+ const next = await requestEmailVerificationMutation.mutateAsync({
664
+ body: { email }
665
+ });
666
+ setCurrentVerificationId(next.data?.verificationId ?? null);
667
+ toast2.success("Verification code resent");
668
+ } catch (error) {
669
+ toast2.error(getErrorMessage2(error));
670
+ } finally {
671
+ setIsSubmitting(false);
672
+ }
673
+ },
674
+ onCancel
675
+ }
676
+ );
677
+ }
678
+
679
+ // src/components/profile/change-email-form.tsx
680
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
681
+ function ChangeEmailForm() {
682
+ const { user } = useSession();
683
+ const [isOpen, setIsOpen] = useState5(false);
684
+ const [showOtp, setShowOtp] = useState5(false);
685
+ const [verificationId, setVerificationId] = useState5(null);
686
+ const [newEmail, setNewEmail] = useState5("");
687
+ const resetForms = () => {
688
+ setShowOtp(false);
689
+ setVerificationId(null);
690
+ setNewEmail("");
691
+ };
692
+ const handleRequestSuccess = (id, email) => {
693
+ setVerificationId(id);
694
+ setNewEmail(email);
695
+ setShowOtp(true);
696
+ };
697
+ const handleVerifySuccess = () => {
698
+ resetForms();
699
+ setIsOpen(false);
700
+ };
701
+ const handleCancel = () => {
702
+ resetForms();
703
+ setIsOpen(false);
704
+ };
705
+ const title = user?.email ? "Change Email" : "Add Email";
706
+ const description = user?.email ? "Update your email address" : "Add an email address to your account";
707
+ return /* @__PURE__ */ jsx7(Collapsible, { open: isOpen, onOpenChange: setIsOpen, children: /* @__PURE__ */ jsxs5("div", { className: "border rounded-lg", children: [
708
+ /* @__PURE__ */ jsxs5(
709
+ CollapsibleTrigger,
710
+ {
711
+ render: /* @__PURE__ */ jsx7(
712
+ Button3,
713
+ {
714
+ variant: "ghost",
715
+ className: "w-full justify-between p-4 h-auto"
716
+ }
717
+ ),
718
+ children: [
719
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col items-start", children: [
720
+ /* @__PURE__ */ jsx7("span", { className: "font-medium", children: title }),
721
+ /* @__PURE__ */ jsx7("span", { className: "text-sm text-muted-foreground", children: description })
722
+ ] }),
723
+ /* @__PURE__ */ jsx7(
724
+ IconChevronDown,
725
+ {
726
+ className: `h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`
727
+ }
728
+ )
729
+ ]
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx7(CollapsibleContent, { children: showOtp ? /* @__PURE__ */ jsx7(
733
+ VerifyChangeEmailForm,
734
+ {
735
+ email: newEmail,
736
+ verificationId,
737
+ onSuccess: handleVerifySuccess,
738
+ onCancel: handleCancel
739
+ }
740
+ ) : /* @__PURE__ */ jsx7(
741
+ RequestChangeEmailForm,
742
+ {
743
+ onSuccess: handleRequestSuccess,
744
+ onCancel: handleCancel,
745
+ buttonText: title
746
+ }
747
+ ) })
748
+ ] }) });
749
+ }
750
+
751
+ // src/components/profile/change-password-form.tsx
752
+ import { zodResolver as zodResolver3 } from "@hookform/resolvers/zod";
753
+ import {
754
+ Button as Button4,
755
+ Collapsible as Collapsible2,
756
+ CollapsibleContent as CollapsibleContent2,
757
+ CollapsibleTrigger as CollapsibleTrigger2,
758
+ Input as Input2,
759
+ Label as Label2,
760
+ Spinner as Spinner3
761
+ } from "@mesob/ui/components";
762
+ import { IconChevronDown as IconChevronDown2, IconEye as IconEye2, IconEyeOff as IconEyeOff2 } from "@tabler/icons-react";
763
+ import { useState as useState6 } from "react";
764
+ import { useForm as useForm3 } from "react-hook-form";
765
+ import { toast as toast3 } from "sonner";
766
+ import { z as z3 } from "zod";
767
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
768
+ var changePasswordSchema = z3.object({
769
+ currentPassword: z3.string().min(8, "Password must be at least 8 characters"),
770
+ newPassword: z3.string().min(8, "Password must be at least 8 characters"),
771
+ confirmPassword: z3.string().min(8, "Password must be at least 8 characters")
772
+ }).refine((data) => data.newPassword === data.confirmPassword, {
773
+ message: "Passwords don't match",
774
+ path: ["confirmPassword"]
775
+ });
776
+ function isAuthError3(error) {
777
+ return typeof error === "object" && error !== null && ("code" in error || "message" in error || "name" in error);
778
+ }
779
+ function getErrorCode3(error) {
780
+ if (error.code) {
781
+ return error.code;
782
+ }
783
+ if (error.message) {
784
+ const upperMessage = error.message.toUpperCase().trim();
785
+ const validCodes = [
786
+ "INVALID_PASSWORD",
787
+ "UNAUTHORIZED",
788
+ "HAS_NO_PASSWORD",
789
+ "USER_NOT_FOUND",
790
+ "USER_EXISTS",
791
+ "VERIFICATION_EXPIRED",
792
+ "VERIFICATION_MISMATCH",
793
+ "VERIFICATION_NOT_FOUND",
794
+ "TOO_MANY_ATTEMPTS",
795
+ "REQUIRES_VERIFICATION",
796
+ "ACCESS_DENIED"
797
+ ];
798
+ if (validCodes.includes(upperMessage)) {
799
+ return upperMessage;
800
+ }
801
+ }
802
+ return void 0;
803
+ }
804
+ function getPasswordChangeErrorMessage(error) {
805
+ if (isAuthError3(error)) {
806
+ const errorCode = getErrorCode3(error);
807
+ switch (errorCode) {
808
+ case "INVALID_PASSWORD":
809
+ return "The current password you entered is incorrect. Please try again.";
810
+ case "UNAUTHORIZED":
811
+ return "You are not authorized to perform this action. Please sign in again.";
812
+ case "HAS_NO_PASSWORD":
813
+ return "Your account does not have a password set. Please use password reset instead.";
814
+ default:
815
+ return error.message || "Failed to change password. Please try again.";
816
+ }
817
+ }
818
+ if (error instanceof Error) {
819
+ return error.message;
820
+ }
821
+ return "Failed to change password. Please try again.";
822
+ }
823
+ function ChangePasswordForm() {
824
+ const { user: _user } = useSession();
825
+ const [isOpen, setIsOpen] = useState6(false);
826
+ const [isSubmitting, setIsSubmitting] = useState6(false);
827
+ const [showPassword, setShowPassword] = useState6(false);
828
+ const [showNewPassword, setShowNewPassword] = useState6(false);
829
+ const [showConfirmPassword, setShowConfirmPassword] = useState6(false);
830
+ const form = useForm3({
831
+ resolver: zodResolver3(changePasswordSchema),
832
+ defaultValues: {
833
+ currentPassword: "",
834
+ newPassword: "",
835
+ confirmPassword: ""
836
+ }
837
+ });
838
+ const { register, handleSubmit, formState, reset } = form;
839
+ const onSubmit = (_data) => {
840
+ try {
841
+ setIsSubmitting(true);
842
+ toast3.error("Password change unavailable");
843
+ setIsOpen(false);
844
+ } catch (error) {
845
+ const errorMessage = getPasswordChangeErrorMessage(error);
846
+ toast3.error(errorMessage);
847
+ } finally {
848
+ setIsSubmitting(false);
849
+ }
850
+ };
851
+ return /* @__PURE__ */ jsx8(Collapsible2, { open: isOpen, onOpenChange: setIsOpen, children: /* @__PURE__ */ jsxs6("div", { className: "border rounded-lg", children: [
852
+ /* @__PURE__ */ jsxs6(
853
+ CollapsibleTrigger2,
854
+ {
855
+ render: /* @__PURE__ */ jsx8(
856
+ Button4,
857
+ {
858
+ variant: "ghost",
859
+ className: "w-full justify-between p-4 h-auto"
860
+ }
861
+ ),
862
+ children: [
863
+ /* @__PURE__ */ jsxs6("div", { className: "flex flex-col items-start", children: [
864
+ /* @__PURE__ */ jsx8("span", { className: "font-medium", children: "Change Password" }),
865
+ /* @__PURE__ */ jsx8("span", { className: "text-sm text-muted-foreground", children: "Update your account password" })
866
+ ] }),
867
+ /* @__PURE__ */ jsx8(
868
+ IconChevronDown2,
869
+ {
870
+ className: `h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`
871
+ }
872
+ )
873
+ ]
874
+ }
875
+ ),
876
+ /* @__PURE__ */ jsx8(CollapsibleContent2, { children: /* @__PURE__ */ jsxs6(
877
+ "form",
878
+ {
879
+ onSubmit: handleSubmit(onSubmit),
880
+ className: "p-4 space-y-4 border-t",
881
+ children: [
882
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2 w-full md:w-1/2", children: [
883
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "currentPassword", children: "Old Password" }),
884
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
885
+ /* @__PURE__ */ jsx8(
886
+ Input2,
887
+ {
888
+ id: "currentPassword",
889
+ type: showPassword ? "text" : "password",
890
+ autoComplete: "current-password",
891
+ placeholder: "Enter your current password",
892
+ ...register("currentPassword")
893
+ }
894
+ ),
895
+ /* @__PURE__ */ jsx8(
896
+ "button",
897
+ {
898
+ type: "button",
899
+ onClick: () => setShowPassword(!showPassword),
900
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
901
+ children: showPassword ? /* @__PURE__ */ jsx8(IconEyeOff2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx8(IconEye2, { className: "h-4 w-4" })
902
+ }
903
+ )
904
+ ] }),
905
+ formState.errors.currentPassword && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: formState.errors.currentPassword.message })
906
+ ] }),
907
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2 w-full md:w-1/2", children: [
908
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "newPassword", children: "New Password" }),
909
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
910
+ /* @__PURE__ */ jsx8(
911
+ Input2,
912
+ {
913
+ id: "newPassword",
914
+ type: showNewPassword ? "text" : "password",
915
+ placeholder: "Enter your new password",
916
+ autoComplete: "new-password",
917
+ ...register("newPassword")
918
+ }
919
+ ),
920
+ /* @__PURE__ */ jsx8(
921
+ "button",
922
+ {
923
+ type: "button",
924
+ onClick: () => setShowNewPassword(!showNewPassword),
925
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
926
+ children: showNewPassword ? /* @__PURE__ */ jsx8(IconEyeOff2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx8(IconEye2, { className: "h-4 w-4" })
927
+ }
928
+ )
929
+ ] }),
930
+ formState.errors.newPassword && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: formState.errors.newPassword.message })
931
+ ] }),
932
+ /* @__PURE__ */ jsxs6("div", { className: "space-y-2 w-full md:w-1/2", children: [
933
+ /* @__PURE__ */ jsx8(Label2, { htmlFor: "confirmPassword", children: "Confirm New Password" }),
934
+ /* @__PURE__ */ jsxs6("div", { className: "relative", children: [
935
+ /* @__PURE__ */ jsx8(
936
+ Input2,
937
+ {
938
+ id: "confirmPassword",
939
+ type: showConfirmPassword ? "text" : "password",
940
+ placeholder: "Confirm your new password",
941
+ autoComplete: "new-password",
942
+ ...register("confirmPassword")
943
+ }
944
+ ),
945
+ /* @__PURE__ */ jsx8(
946
+ "button",
947
+ {
948
+ type: "button",
949
+ onClick: () => setShowConfirmPassword(!showConfirmPassword),
950
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
951
+ children: showConfirmPassword ? /* @__PURE__ */ jsx8(IconEyeOff2, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx8(IconEye2, { className: "h-4 w-4" })
952
+ }
953
+ )
954
+ ] }),
955
+ formState.errors.confirmPassword && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: formState.errors.confirmPassword.message })
956
+ ] }),
957
+ /* @__PURE__ */ jsxs6("div", { className: "flex justify-end gap-2", children: [
958
+ /* @__PURE__ */ jsx8(
959
+ Button4,
960
+ {
961
+ type: "button",
962
+ variant: "outline",
963
+ onClick: () => {
964
+ reset();
965
+ setIsOpen(false);
966
+ },
967
+ disabled: isSubmitting,
968
+ children: "Cancel"
969
+ }
970
+ ),
971
+ /* @__PURE__ */ jsxs6(Button4, { type: "submit", disabled: isSubmitting, children: [
972
+ isSubmitting && /* @__PURE__ */ jsx8(Spinner3, { className: "mr-2 h-4 w-4" }),
973
+ "Change Password"
974
+ ] })
975
+ ] })
976
+ ]
977
+ }
978
+ ) })
979
+ ] }) });
980
+ }
981
+
982
+ // src/components/profile/change-phone-form.tsx
983
+ import {
984
+ Button as Button6,
985
+ Collapsible as Collapsible3,
986
+ CollapsibleContent as CollapsibleContent3,
987
+ CollapsibleTrigger as CollapsibleTrigger3
988
+ } from "@mesob/ui/components";
989
+ import { IconChevronDown as IconChevronDown3 } from "@tabler/icons-react";
990
+ import { useState as useState9 } from "react";
991
+
992
+ // src/components/profile/request-change-phone-form.tsx
993
+ import { zodResolver as zodResolver4 } from "@hookform/resolvers/zod";
994
+ import { Button as Button5, Input as Input3, Label as Label3, Spinner as Spinner4 } from "@mesob/ui/components";
995
+ import { IconEye as IconEye3, IconEyeOff as IconEyeOff3 } from "@tabler/icons-react";
996
+ import { useEffect as useEffect3, useState as useState7 } from "react";
997
+ import { useForm as useForm4 } from "react-hook-form";
998
+ import { toast as toast4 } from "sonner";
999
+ import { z as z4 } from "zod";
1000
+
1001
+ // src/utils/normalize-phone.ts
1002
+ function normalizePhone(phone) {
1003
+ const cleaned = phone.trim().replace(/\s/g, "");
1004
+ if (cleaned.startsWith("+2519") || cleaned.startsWith("+2517")) {
1005
+ return cleaned;
1006
+ }
1007
+ if (cleaned.startsWith("2519") || cleaned.startsWith("2517")) {
1008
+ return `+${cleaned}`;
1009
+ }
1010
+ if (cleaned.startsWith("09") || cleaned.startsWith("07")) {
1011
+ return `+251${cleaned.slice(1)}`;
1012
+ }
1013
+ if ((cleaned.startsWith("9") || cleaned.startsWith("7")) && cleaned.length === 9) {
1014
+ return `+251${cleaned}`;
1015
+ }
1016
+ return cleaned;
1017
+ }
1018
+
1019
+ // src/components/profile/request-change-phone-form.tsx
1020
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1021
+ var phonePasswordSchema = (phoneRegex) => z4.object({
1022
+ phone: z4.string().trim().min(1, { message: "Phone number is required" }).refine(
1023
+ (val) => {
1024
+ const isPhone = phoneRegex.test(val);
1025
+ return isPhone;
1026
+ },
1027
+ {
1028
+ message: "Invalid phone number"
1029
+ }
1030
+ ),
1031
+ password: z4.string().min(8, "Password must be at least 8 characters").max(128, "Password too long")
1032
+ });
1033
+ function isAuthError4(error) {
1034
+ return typeof error === "object" && error !== null && ("code" in error || "message" in error || "name" in error);
1035
+ }
1036
+ function getErrorCode4(error) {
1037
+ if (error.code) {
1038
+ return error.code;
1039
+ }
1040
+ if (error.message) {
1041
+ const upperMessage = error.message.toUpperCase().trim();
1042
+ const validCodes = [
1043
+ "USER_NOT_FOUND",
1044
+ "USER_EXISTS",
1045
+ "INVALID_PASSWORD",
1046
+ "VERIFICATION_EXPIRED",
1047
+ "VERIFICATION_MISMATCH",
1048
+ "VERIFICATION_NOT_FOUND",
1049
+ "TOO_MANY_ATTEMPTS",
1050
+ "UNAUTHORIZED"
1051
+ ];
1052
+ if (validCodes.includes(upperMessage)) {
1053
+ return upperMessage;
1054
+ }
1055
+ }
1056
+ return void 0;
1057
+ }
1058
+ function getErrorMessage3(error) {
1059
+ if (isAuthError4(error)) {
1060
+ const errorCode = getErrorCode4(error);
1061
+ switch (errorCode) {
1062
+ case "USER_EXISTS":
1063
+ return "This phone number is already taken. Please use a different number.";
1064
+ case "VERIFICATION_EXPIRED":
1065
+ return "Verification code has expired. Please request a new one.";
1066
+ case "VERIFICATION_MISMATCH":
1067
+ return "Invalid verification code. Please try again.";
1068
+ case "VERIFICATION_NOT_FOUND":
1069
+ return "Verification not found. Please request a new code.";
1070
+ default:
1071
+ return error.message || "An error occurred. Please try again.";
1072
+ }
1073
+ }
1074
+ if (error instanceof Error) {
1075
+ return error.message;
1076
+ }
1077
+ return "An error occurred. Please try again.";
1078
+ }
1079
+ function RequestChangePhoneForm({
1080
+ onSuccess,
1081
+ onCancel,
1082
+ buttonText
1083
+ }) {
1084
+ const { user } = useSession();
1085
+ const { hooks } = useApi();
1086
+ const { config } = useConfig();
1087
+ const [isSubmitting, setIsSubmitting] = useState7(false);
1088
+ const [isChecking, setIsChecking] = useState7(true);
1089
+ const [showPassword, setShowPassword] = useState7(false);
1090
+ const phoneRegex = typeof config.phoneRegex === "string" ? new RegExp(config.phoneRegex) : config.phoneRegex || /^(\+2519|\+2517|2519|2517|09|07)\d{8}$/;
1091
+ const getPendingAccountChangeQuery = hooks.useQuery(
1092
+ "get",
1093
+ "/account-change/pending",
1094
+ {},
1095
+ { enabled: false }
1096
+ );
1097
+ const verifyPasswordMutation = hooks.useMutation("post", "/password/verify");
1098
+ const checkUserMutation = hooks.useMutation("post", "/check-account");
1099
+ const requestPhoneOtpMutation = hooks.useMutation(
1100
+ "post",
1101
+ "/phone/verification/request"
1102
+ );
1103
+ const phonePasswordForm = useForm4({
1104
+ resolver: zodResolver4(phonePasswordSchema(phoneRegex)),
1105
+ defaultValues: {
1106
+ phone: "",
1107
+ password: ""
1108
+ }
1109
+ });
1110
+ const {
1111
+ register,
1112
+ handleSubmit,
1113
+ getValues,
1114
+ setValue,
1115
+ formState: { errors }
1116
+ } = phonePasswordForm;
1117
+ useEffect3(() => {
1118
+ let active = true;
1119
+ const run = async () => {
1120
+ try {
1121
+ const data = await getPendingAccountChangeQuery.refetch();
1122
+ if (!active) {
1123
+ return;
1124
+ }
1125
+ const accountChange = data.data?.accountChange;
1126
+ const verificationId = data.data?.verificationId;
1127
+ if (accountChange?.changeType !== "phone") {
1128
+ setIsChecking(false);
1129
+ return;
1130
+ }
1131
+ if (!accountChange.newPhone) {
1132
+ setIsChecking(false);
1133
+ return;
1134
+ }
1135
+ if (getValues("phone")) {
1136
+ setIsChecking(false);
1137
+ return;
1138
+ }
1139
+ setValue("phone", accountChange.newPhone, { shouldValidate: true });
1140
+ if (verificationId) {
1141
+ toast4.message("Resuming verification\u2026");
1142
+ onSuccess(verificationId, accountChange.newPhone);
1143
+ return;
1144
+ }
1145
+ setIsChecking(false);
1146
+ } catch {
1147
+ setIsChecking(false);
1148
+ }
1149
+ };
1150
+ run().catch(() => void 0);
1151
+ return () => {
1152
+ active = false;
1153
+ };
1154
+ }, [getPendingAccountChangeQuery.refetch, getValues, onSuccess, setValue]);
1155
+ const onPhonePasswordSubmit = async (data) => {
1156
+ if (!user) {
1157
+ toast4.error("User not found");
1158
+ return;
1159
+ }
1160
+ try {
1161
+ setIsSubmitting(true);
1162
+ const normalizedPhone = normalizePhone(data.phone);
1163
+ await verifyPasswordMutation.mutateAsync({
1164
+ body: { password: data.password }
1165
+ });
1166
+ const checkResult = await checkUserMutation.mutateAsync({
1167
+ body: { identifier: normalizedPhone }
1168
+ });
1169
+ if (checkResult.data?.exists) {
1170
+ if (user?.phone?.replace(/\s/g, "") === normalizedPhone.replace(/\s/g, "")) {
1171
+ toast4.error("This is already your current phone number.");
1172
+ return;
1173
+ }
1174
+ toast4.error(
1175
+ "This phone number is already taken. Please use a different number."
1176
+ );
1177
+ return;
1178
+ }
1179
+ const verification = await requestPhoneOtpMutation.mutateAsync({
1180
+ body: {
1181
+ phone: normalizedPhone,
1182
+ context: "change-phone"
1183
+ }
1184
+ });
1185
+ toast4.success("Verification code sent to your phone");
1186
+ onSuccess(verification.data?.verificationId ?? "", normalizedPhone);
1187
+ } catch (error) {
1188
+ const errorMessage = getErrorMessage3(error);
1189
+ if (isAuthError4(error)) {
1190
+ const errorCode = getErrorCode4(error);
1191
+ if (errorCode === "INVALID_PASSWORD" || errorCode === "USER_NOT_FOUND") {
1192
+ toast4.error("Incorrect password. Please try again.");
1193
+ return;
1194
+ }
1195
+ }
1196
+ toast4.error(errorMessage);
1197
+ } finally {
1198
+ setIsSubmitting(false);
1199
+ }
1200
+ };
1201
+ const isLoading = isSubmitting || isChecking;
1202
+ return /* @__PURE__ */ jsxs7(
1203
+ "form",
1204
+ {
1205
+ onSubmit: handleSubmit(onPhonePasswordSubmit),
1206
+ className: "p-4 space-y-4 border-t",
1207
+ children: [
1208
+ /* @__PURE__ */ jsxs7("div", { className: "space-y-4 w-full md:w-1/2", children: [
1209
+ /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1210
+ /* @__PURE__ */ jsx9(Label3, { htmlFor: "phone", children: "Phone Number" }),
1211
+ /* @__PURE__ */ jsx9(
1212
+ Input3,
1213
+ {
1214
+ id: "phone",
1215
+ type: "tel",
1216
+ placeholder: "Enter your new phone number",
1217
+ ...register("phone"),
1218
+ disabled: isLoading
1219
+ }
1220
+ ),
1221
+ errors.phone && /* @__PURE__ */ jsx9("p", { className: "text-sm text-destructive", children: errors.phone.message })
1222
+ ] }),
1223
+ /* @__PURE__ */ jsxs7("div", { className: "space-y-2", children: [
1224
+ /* @__PURE__ */ jsx9(Label3, { htmlFor: "password", children: "Password" }),
1225
+ /* @__PURE__ */ jsxs7("div", { className: "relative", children: [
1226
+ /* @__PURE__ */ jsx9(
1227
+ Input3,
1228
+ {
1229
+ id: "password",
1230
+ type: showPassword ? "text" : "password",
1231
+ autoComplete: "current-password",
1232
+ placeholder: "Enter your password",
1233
+ ...register("password"),
1234
+ disabled: isLoading
1235
+ }
1236
+ ),
1237
+ /* @__PURE__ */ jsx9(
1238
+ "button",
1239
+ {
1240
+ type: "button",
1241
+ onClick: () => setShowPassword(!showPassword),
1242
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground",
1243
+ children: showPassword ? /* @__PURE__ */ jsx9(IconEyeOff3, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx9(IconEye3, { className: "h-4 w-4" })
1244
+ }
1245
+ )
1246
+ ] }),
1247
+ errors.password && /* @__PURE__ */ jsx9("p", { className: "text-sm text-destructive", children: errors.password.message })
1248
+ ] })
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxs7("div", { className: "flex justify-end gap-2", children: [
1251
+ /* @__PURE__ */ jsx9(
1252
+ Button5,
1253
+ {
1254
+ type: "button",
1255
+ variant: "outline",
1256
+ onClick: onCancel,
1257
+ disabled: isLoading,
1258
+ children: "Cancel"
1259
+ }
1260
+ ),
1261
+ /* @__PURE__ */ jsxs7(Button5, { type: "submit", disabled: isLoading, children: [
1262
+ isLoading && /* @__PURE__ */ jsx9(Spinner4, { className: "mr-2 h-4 w-4" }),
1263
+ isChecking ? "Checking\u2026" : buttonText
1264
+ ] })
1265
+ ] })
1266
+ ]
1267
+ }
1268
+ );
1269
+ }
1270
+
1271
+ // src/components/profile/verify-change-phone-form.tsx
1272
+ import { useState as useState8 } from "react";
1273
+ import { toast as toast5 } from "sonner";
1274
+ import { jsx as jsx10 } from "react/jsx-runtime";
1275
+ function isAuthError5(error) {
1276
+ return typeof error === "object" && error !== null && ("code" in error || "message" in error || "name" in error);
1277
+ }
1278
+ function getErrorCode5(error) {
1279
+ if (error.code) {
1280
+ return error.code;
1281
+ }
1282
+ if (error.message) {
1283
+ const upperMessage = error.message.toUpperCase().trim();
1284
+ const validCodes = [
1285
+ "USER_NOT_FOUND",
1286
+ "USER_EXISTS",
1287
+ "INVALID_PASSWORD",
1288
+ "VERIFICATION_EXPIRED",
1289
+ "VERIFICATION_MISMATCH",
1290
+ "VERIFICATION_NOT_FOUND",
1291
+ "TOO_MANY_ATTEMPTS",
1292
+ "UNAUTHORIZED"
1293
+ ];
1294
+ if (validCodes.includes(upperMessage)) {
1295
+ return upperMessage;
1296
+ }
1297
+ }
1298
+ return void 0;
1299
+ }
1300
+ function getErrorMessage4(error) {
1301
+ if (isAuthError5(error)) {
1302
+ const errorCode = getErrorCode5(error);
1303
+ switch (errorCode) {
1304
+ case "USER_EXISTS":
1305
+ return "This phone number is already taken. Please use a different number.";
1306
+ case "VERIFICATION_EXPIRED":
1307
+ return "Verification code has expired. Please request a new one.";
1308
+ case "VERIFICATION_MISMATCH":
1309
+ return "Invalid verification code. Please try again.";
1310
+ case "VERIFICATION_NOT_FOUND":
1311
+ return "Verification not found. Please request a new code.";
1312
+ default:
1313
+ return error.message || "An error occurred. Please try again.";
1314
+ }
1315
+ }
1316
+ if (error instanceof Error) {
1317
+ return error.message;
1318
+ }
1319
+ return "An error occurred. Please try again.";
1320
+ }
1321
+ function VerifyChangePhoneForm({
1322
+ phone,
1323
+ verificationId,
1324
+ onSuccess,
1325
+ onCancel
1326
+ }) {
1327
+ const { refresh } = useSession();
1328
+ const { hooks } = useApi();
1329
+ const [isSubmitting, setIsSubmitting] = useState8(false);
1330
+ const [currentVerificationId, setCurrentVerificationId] = useState8(verificationId);
1331
+ const verifyPhoneOtpMutation = hooks.useMutation(
1332
+ "post",
1333
+ "/phone/verification/confirm"
1334
+ );
1335
+ const updatePhoneMutation = hooks.useMutation("put", "/profile/phone");
1336
+ const requestPhoneOtpMutation = hooks.useMutation(
1337
+ "post",
1338
+ "/phone/verification/request"
1339
+ );
1340
+ const onOtpSubmit = async (code) => {
1341
+ if (!currentVerificationId) {
1342
+ toast5.error("Verification not found. Please request a new code.");
1343
+ return;
1344
+ }
1345
+ try {
1346
+ setIsSubmitting(true);
1347
+ await verifyPhoneOtpMutation.mutateAsync({
1348
+ body: {
1349
+ verificationId: currentVerificationId,
1350
+ code,
1351
+ context: "change-phone"
1352
+ }
1353
+ });
1354
+ await updatePhoneMutation.mutateAsync({
1355
+ body: { phone }
1356
+ });
1357
+ toast5.success("Phone number updated successfully");
1358
+ await refresh();
1359
+ onSuccess();
1360
+ } catch (error) {
1361
+ const errorMessage = getErrorMessage4(error);
1362
+ toast5.error(errorMessage);
1363
+ } finally {
1364
+ setIsSubmitting(false);
1365
+ }
1366
+ };
1367
+ if (!currentVerificationId) {
1368
+ toast5.error("Verification not found. Please request a new code.");
1369
+ return null;
1370
+ }
1371
+ return /* @__PURE__ */ jsx10(
1372
+ OtpVerificationModal,
1373
+ {
1374
+ open: true,
1375
+ title: "Verify phone",
1376
+ description: `Enter the verification code sent to ${phone}`,
1377
+ verificationId: currentVerificationId,
1378
+ isLoading: isSubmitting,
1379
+ onSubmit: onOtpSubmit,
1380
+ onResend: async () => {
1381
+ try {
1382
+ setIsSubmitting(true);
1383
+ const next = await requestPhoneOtpMutation.mutateAsync({
1384
+ body: {
1385
+ phone,
1386
+ context: "change-phone"
1387
+ }
1388
+ });
1389
+ setCurrentVerificationId(next.data?.verificationId ?? null);
1390
+ toast5.success("Verification code resent");
1391
+ } catch (error) {
1392
+ toast5.error(getErrorMessage4(error));
1393
+ } finally {
1394
+ setIsSubmitting(false);
1395
+ }
1396
+ },
1397
+ onCancel
1398
+ }
1399
+ );
1400
+ }
1401
+
1402
+ // src/components/profile/change-phone-form.tsx
1403
+ import { jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1404
+ function ChangePhoneForm() {
1405
+ const { user } = useSession();
1406
+ const [isOpen, setIsOpen] = useState9(false);
1407
+ const [showOtp, setShowOtp] = useState9(false);
1408
+ const [verificationId, setVerificationId] = useState9(null);
1409
+ const [newPhone, setNewPhone] = useState9("");
1410
+ const resetForms = () => {
1411
+ setShowOtp(false);
1412
+ setVerificationId(null);
1413
+ setNewPhone("");
1414
+ };
1415
+ const handleRequestSuccess = (id, phone) => {
1416
+ setVerificationId(id);
1417
+ setNewPhone(phone);
1418
+ setShowOtp(true);
1419
+ };
1420
+ const handleVerifySuccess = () => {
1421
+ resetForms();
1422
+ setIsOpen(false);
1423
+ };
1424
+ const handleCancel = () => {
1425
+ resetForms();
1426
+ setIsOpen(false);
1427
+ };
1428
+ const title = user?.phone ? "Change Phone" : "Add Phone";
1429
+ const description = user?.phone ? "Update your phone number" : "Add a phone number to your account";
1430
+ return /* @__PURE__ */ jsx11(Collapsible3, { open: isOpen, onOpenChange: setIsOpen, children: /* @__PURE__ */ jsxs8("div", { className: "border rounded-lg", children: [
1431
+ /* @__PURE__ */ jsxs8(
1432
+ CollapsibleTrigger3,
1433
+ {
1434
+ render: /* @__PURE__ */ jsx11(
1435
+ Button6,
1436
+ {
1437
+ variant: "ghost",
1438
+ className: "w-full justify-between p-4 h-auto"
1439
+ }
1440
+ ),
1441
+ children: [
1442
+ /* @__PURE__ */ jsxs8("div", { className: "flex flex-col items-start", children: [
1443
+ /* @__PURE__ */ jsx11("span", { className: "font-medium", children: title }),
1444
+ /* @__PURE__ */ jsx11("span", { className: "text-sm text-muted-foreground", children: description })
1445
+ ] }),
1446
+ /* @__PURE__ */ jsx11(
1447
+ IconChevronDown3,
1448
+ {
1449
+ className: `h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`
1450
+ }
1451
+ )
1452
+ ]
1453
+ }
1454
+ ),
1455
+ /* @__PURE__ */ jsx11(CollapsibleContent3, { children: showOtp ? /* @__PURE__ */ jsx11(
1456
+ VerifyChangePhoneForm,
1457
+ {
1458
+ phone: newPhone,
1459
+ verificationId,
1460
+ onSuccess: handleVerifySuccess,
1461
+ onCancel: handleCancel
1462
+ }
1463
+ ) : /* @__PURE__ */ jsx11(
1464
+ RequestChangePhoneForm,
1465
+ {
1466
+ onSuccess: handleRequestSuccess,
1467
+ onCancel: handleCancel,
1468
+ buttonText: title
1469
+ }
1470
+ ) })
1471
+ ] }) });
1472
+ }
1473
+
1474
+ // src/components/profile/security.tsx
1475
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1476
+ function Security() {
1477
+ const { user } = useSession();
1478
+ return /* @__PURE__ */ jsxs9("div", { className: "mx-auto flex w-full max-w-6xl flex-col gap-6", children: [
1479
+ /* @__PURE__ */ jsxs9("div", { className: "relative overflow-hidden rounded-[2rem] border border-border/60 bg-gradient-to-br from-background via-background to-amber-500/5 p-6 shadow-sm", children: [
1480
+ /* @__PURE__ */ jsx12("div", { className: "absolute left-0 top-0 h-32 w-32 rounded-full bg-primary/10 blur-3xl" }),
1481
+ /* @__PURE__ */ jsxs9("div", { className: "relative flex flex-col gap-4", children: [
1482
+ /* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap gap-2", children: [
1483
+ /* @__PURE__ */ jsx12(Badge, { variant: "outline", children: "Security center" }),
1484
+ /* @__PURE__ */ jsx12(Badge, { variant: "secondary", children: user?.emailVerified || user?.phoneVerified ? "Recovery methods available" : "Add a recovery method" })
1485
+ ] }),
1486
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-2", children: [
1487
+ /* @__PURE__ */ jsx12("h1", { className: "text-2xl font-semibold tracking-tight", children: "Security" }),
1488
+ /* @__PURE__ */ jsx12("p", { className: "max-w-2xl text-sm text-muted-foreground", children: "Password, email, and phone updates all live here. Keep recovery channels current so account recovery stays predictable." })
1489
+ ] })
1490
+ ] })
1491
+ ] }),
1492
+ /* @__PURE__ */ jsxs9("div", { className: "grid gap-4 lg:grid-cols-[0.85fr_1.4fr]", children: [
1493
+ /* @__PURE__ */ jsx12(Card, { className: "rounded-[1.75rem] border-border/60 shadow-sm", children: /* @__PURE__ */ jsxs9(CardContent, { className: "space-y-5 p-6", children: [
1494
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-1", children: [
1495
+ /* @__PURE__ */ jsx12("div", { className: "text-sm font-medium text-muted-foreground", children: "Verification state" }),
1496
+ /* @__PURE__ */ jsx12("div", { className: "text-lg font-semibold", children: "Recovery readiness" })
1497
+ ] }),
1498
+ /* @__PURE__ */ jsx12(Separator, {}),
1499
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-3", children: [
1500
+ /* @__PURE__ */ jsxs9("div", { className: "rounded-2xl border border-border/60 bg-muted/20 p-4", children: [
1501
+ /* @__PURE__ */ jsx12("div", { className: "text-sm font-medium", children: "Email" }),
1502
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 text-sm text-muted-foreground", children: user?.email ?? "No email added" }),
1503
+ /* @__PURE__ */ jsx12("div", { className: "mt-3", children: /* @__PURE__ */ jsx12(Badge, { variant: "outline", children: user?.emailVerified ? "Verified" : "Unverified" }) })
1504
+ ] }),
1505
+ /* @__PURE__ */ jsxs9("div", { className: "rounded-2xl border border-border/60 bg-muted/20 p-4", children: [
1506
+ /* @__PURE__ */ jsx12("div", { className: "text-sm font-medium", children: "Phone" }),
1507
+ /* @__PURE__ */ jsx12("div", { className: "mt-1 text-sm text-muted-foreground", children: user?.phone ?? "No phone added" }),
1508
+ /* @__PURE__ */ jsx12("div", { className: "mt-3", children: /* @__PURE__ */ jsx12(Badge, { variant: "outline", children: user?.phoneVerified ? "Verified" : "Unverified" }) })
1509
+ ] })
1510
+ ] })
1511
+ ] }) }),
1512
+ /* @__PURE__ */ jsxs9("div", { className: "space-y-4", children: [
1513
+ /* @__PURE__ */ jsx12(ChangePasswordForm, {}),
1514
+ /* @__PURE__ */ jsx12(ChangeEmailForm, {}),
1515
+ /* @__PURE__ */ jsx12(ChangePhoneForm, {})
1516
+ ] })
1517
+ ] })
1518
+ ] });
1519
+ }
1520
+
1521
+ // src/pages/profile/security.tsx
1522
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1523
+ function ProfileSecurityPage() {
1524
+ useBreadcrumbs({
1525
+ items: [
1526
+ { label: "Home", href: "/dashboard" },
1527
+ { label: "Profile", href: "/profile/security" },
1528
+ { label: "Security" }
1529
+ ]
1530
+ });
1531
+ return /* @__PURE__ */ jsxs10(PageContainer, { className: "flex flex-1 flex-col overflow-auto p-4 pt-0 lg:p-6 lg:pt-0", children: [
1532
+ /* @__PURE__ */ jsx13(PageTitle, { icon: /* @__PURE__ */ jsx13(IconShieldLock, { className: "size-5" }), children: "Security" }),
1533
+ /* @__PURE__ */ jsx13(PageBody, { className: "px-0 pb-6", children: /* @__PURE__ */ jsx13(Security, {}) })
1534
+ ] });
1535
+ }
1536
+ export {
1537
+ ProfileSecurityPage as default
1538
+ };
1539
+ //# sourceMappingURL=security.js.map