@greatapps/common 1.1.24 → 1.1.25

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 (125) hide show
  1. package/dist/components/account/ConfigurationsMyAccountModal.mjs +121 -0
  2. package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -0
  3. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +45 -0
  4. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -0
  5. package/dist/components/account/DisableTwoFactorAuthModal.mjs +67 -0
  6. package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -0
  7. package/dist/components/account/TwoFactorAuthModal.mjs +190 -0
  8. package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -0
  9. package/dist/components/account/constants.mjs +32 -0
  10. package/dist/components/account/constants.mjs.map +1 -0
  11. package/dist/components/account/hooks/useChangeEmailForm.mjs +53 -0
  12. package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -0
  13. package/dist/components/account/hooks/useChangePasswordForm.mjs +47 -0
  14. package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -0
  15. package/dist/components/account/hooks/useChangePhoneForm.mjs +64 -0
  16. package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -0
  17. package/dist/components/account/hooks/useConfigurationsModal.mjs +33 -0
  18. package/dist/components/account/hooks/useConfigurationsModal.mjs.map +1 -0
  19. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +54 -0
  20. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -0
  21. package/dist/components/account/hooks/useOtpVerification.mjs +94 -0
  22. package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -0
  23. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +88 -0
  24. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -0
  25. package/dist/components/account/sections/ChangeEmailModal.mjs +180 -0
  26. package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -0
  27. package/dist/components/account/sections/ChangePasswordSection.mjs +64 -0
  28. package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -0
  29. package/dist/components/account/sections/ChangePhoneModal.mjs +182 -0
  30. package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -0
  31. package/dist/components/account/sections/MyProfileSection.mjs +312 -0
  32. package/dist/components/account/sections/MyProfileSection.mjs.map +1 -0
  33. package/dist/components/account/sections/PreferencesSection.mjs +246 -0
  34. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -0
  35. package/dist/components/account/sections/SecuritySection.mjs +147 -0
  36. package/dist/components/account/sections/SecuritySection.mjs.map +1 -0
  37. package/dist/components/ui/form/ComboboxField.mjs +130 -0
  38. package/dist/components/ui/form/ComboboxField.mjs.map +1 -0
  39. package/dist/components/ui/form/FormField.mjs +103 -0
  40. package/dist/components/ui/form/FormField.mjs.map +1 -0
  41. package/dist/components/ui/form/PhoneInput.mjs +78 -0
  42. package/dist/components/ui/form/PhoneInput.mjs.map +1 -0
  43. package/dist/components/ui/form/SelectField.mjs +80 -0
  44. package/dist/components/ui/form/SelectField.mjs.map +1 -0
  45. package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs +40 -0
  46. package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs.map +1 -0
  47. package/dist/components/ui/form/TextAreaField.mjs +56 -0
  48. package/dist/components/ui/form/TextAreaField.mjs.map +1 -0
  49. package/dist/enums/AccountSectionType.mjs +11 -0
  50. package/dist/enums/AccountSectionType.mjs.map +1 -0
  51. package/dist/hooks/useCountdownTimer.mjs +29 -0
  52. package/dist/hooks/useCountdownTimer.mjs.map +1 -0
  53. package/dist/hooks/useIsMobile.mjs +18 -0
  54. package/dist/hooks/useIsMobile.mjs.map +1 -0
  55. package/dist/hooks/usePasswordVisibility.mjs +13 -0
  56. package/dist/hooks/usePasswordVisibility.mjs.map +1 -0
  57. package/dist/index.mjs +80 -0
  58. package/dist/index.mjs.map +1 -1
  59. package/dist/modules/accounts/actions/account-management.action.mjs +163 -0
  60. package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -0
  61. package/dist/modules/accounts/hooks/useAccountManagement.mjs +64 -0
  62. package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -0
  63. package/dist/modules/accounts/services/account.service.mjs +162 -25
  64. package/dist/modules/accounts/services/account.service.mjs.map +1 -1
  65. package/dist/modules/accounts/services/two-factor.service.mjs +77 -0
  66. package/dist/modules/accounts/services/two-factor.service.mjs.map +1 -0
  67. package/dist/server.mjs +36 -0
  68. package/dist/server.mjs.map +1 -1
  69. package/dist/utils/browser/clipboard.mjs +36 -0
  70. package/dist/utils/browser/clipboard.mjs.map +1 -0
  71. package/dist/utils/file/index.mjs +9 -0
  72. package/dist/utils/file/index.mjs.map +1 -0
  73. package/dist/utils/format/masks.mjs +56 -0
  74. package/dist/utils/format/masks.mjs.map +1 -0
  75. package/dist/utils/intl/locales.mjs +12 -0
  76. package/dist/utils/intl/locales.mjs.map +1 -0
  77. package/dist/utils/intl/timezones.mjs +41 -0
  78. package/dist/utils/intl/timezones.mjs.map +1 -0
  79. package/dist/utils/validators/account.mjs +26 -0
  80. package/dist/utils/validators/account.mjs.map +1 -0
  81. package/dist/utils/validators/common.mjs +47 -0
  82. package/dist/utils/validators/common.mjs.map +1 -0
  83. package/package.json +5 -1
  84. package/src/components/account/ConfigurationsMyAccountModal.tsx +147 -0
  85. package/src/components/account/ConfirmGlobalPreferencesModal.tsx +66 -0
  86. package/src/components/account/DisableTwoFactorAuthModal.tsx +86 -0
  87. package/src/components/account/TwoFactorAuthModal.tsx +221 -0
  88. package/src/components/account/constants.ts +29 -0
  89. package/src/components/account/hooks/useChangeEmailForm.tsx +56 -0
  90. package/src/components/account/hooks/useChangePasswordForm.tsx +59 -0
  91. package/src/components/account/hooks/useChangePhoneForm.tsx +69 -0
  92. package/src/components/account/hooks/useConfigurationsModal.ts +40 -0
  93. package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +69 -0
  94. package/src/components/account/hooks/useOtpVerification.tsx +116 -0
  95. package/src/components/account/hooks/useTwoFactorAuthForm.tsx +114 -0
  96. package/src/components/account/sections/ChangeEmailModal.tsx +224 -0
  97. package/src/components/account/sections/ChangePasswordSection.tsx +64 -0
  98. package/src/components/account/sections/ChangePhoneModal.tsx +229 -0
  99. package/src/components/account/sections/MyProfileSection.tsx +337 -0
  100. package/src/components/account/sections/PreferencesSection.tsx +277 -0
  101. package/src/components/account/sections/SecuritySection.tsx +160 -0
  102. package/src/components/ui/form/ComboboxField.tsx +154 -0
  103. package/src/components/ui/form/FormField.tsx +130 -0
  104. package/src/components/ui/form/PhoneInput.tsx +98 -0
  105. package/src/components/ui/form/SelectField.tsx +118 -0
  106. package/src/components/ui/form/SwitchOptionFieldWithIcon.tsx +47 -0
  107. package/src/components/ui/form/TextAreaField.tsx +70 -0
  108. package/src/enums/AccountSectionType.ts +6 -0
  109. package/src/hooks/useCountdownTimer.ts +41 -0
  110. package/src/hooks/useIsMobile.ts +19 -0
  111. package/src/hooks/usePasswordVisibility.ts +13 -0
  112. package/src/index.ts +70 -0
  113. package/src/modules/accounts/actions/account-management.action.ts +197 -0
  114. package/src/modules/accounts/hooks/useAccountManagement.ts +79 -0
  115. package/src/modules/accounts/services/account.service.ts +229 -50
  116. package/src/modules/accounts/services/two-factor.service.ts +98 -0
  117. package/src/modules/accounts/types.ts +157 -0
  118. package/src/server.ts +21 -0
  119. package/src/utils/browser/clipboard.ts +34 -0
  120. package/src/utils/file/index.ts +5 -0
  121. package/src/utils/format/masks.ts +52 -0
  122. package/src/utils/intl/locales.ts +15 -0
  123. package/src/utils/intl/timezones.ts +53 -0
  124. package/src/utils/validators/account.ts +36 -0
  125. package/src/utils/validators/common.ts +43 -0
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { useForm } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { toast } from 'sonner';
7
+ import { Toast } from '../../ui/feedback/Toast';
8
+ import { changeEmailSchema, type ChangeEmailFormData } from '../../../utils/validators/account';
9
+ import { requestEmailChangeAction } from '../../../modules/accounts/actions/account-management.action';
10
+
11
+ export default function useChangeEmailForm() {
12
+ const [showVerification, setShowVerification] = useState(false);
13
+
14
+ const { register, handleSubmit, reset, watch, formState: { errors, isSubmitting } } = useForm<ChangeEmailFormData>({
15
+ resolver: zodResolver(changeEmailSchema),
16
+ mode: 'onChange',
17
+ defaultValues: { email: '' },
18
+ });
19
+
20
+ const handleContinue = handleSubmit(async (data) => {
21
+ const result = await requestEmailChangeAction(data.email);
22
+
23
+ if (!result.success) {
24
+ toast.custom((t) => (
25
+ <Toast
26
+ variant="error"
27
+ message={result.error ?? 'E-mail já existente ou ocorreu um erro. Tente novamente mais tarde.'}
28
+ toastId={t}
29
+ />
30
+ ));
31
+ return;
32
+ }
33
+
34
+ setShowVerification(true);
35
+ });
36
+
37
+ const handleBack = useCallback(() => {
38
+ setShowVerification(false);
39
+ }, []);
40
+
41
+ const resetForm = useCallback(() => {
42
+ reset();
43
+ setShowVerification(false);
44
+ }, [reset]);
45
+
46
+ return {
47
+ showVerification,
48
+ emailToVerify: watch('email'),
49
+ isSubmitting,
50
+ errors,
51
+ register,
52
+ handleContinue,
53
+ handleBack,
54
+ resetForm,
55
+ };
56
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { useForm } from 'react-hook-form';
4
+ import { zodResolver } from '@hookform/resolvers/zod';
5
+ import { useMutation } from '@tanstack/react-query';
6
+ import { toast } from 'sonner';
7
+ import { Toast } from '../../ui/feedback/Toast';
8
+ import { changePasswordSchema, type ChangePasswordFormData } from '../../../utils/validators/account';
9
+ import { changePasswordAction } from '../../../modules/accounts/actions/account-management.action';
10
+
11
+ interface UseChangePasswordFormProps {
12
+ onSuccess?: () => void;
13
+ }
14
+
15
+ export default function useChangePasswordForm({ onSuccess }: UseChangePasswordFormProps = {}) {
16
+ const form = useForm<ChangePasswordFormData>({
17
+ resolver: zodResolver(changePasswordSchema),
18
+ mode: 'onChange',
19
+ defaultValues: {
20
+ currentPassword: '',
21
+ newPassword: '',
22
+ },
23
+ });
24
+
25
+ const mutation = useMutation({
26
+ mutationFn: changePasswordAction,
27
+ onSuccess: (result) => {
28
+ if (!result.success) {
29
+ toast.custom((t) => (
30
+ <Toast variant="error" message={result.error ?? 'Erro ao alterar senha'} toastId={t} />
31
+ ));
32
+ return;
33
+ }
34
+
35
+ toast.custom((t) => (
36
+ <Toast variant="success" message="Senha alterada com sucesso" toastId={t} />
37
+ ));
38
+
39
+ form.reset();
40
+ onSuccess?.();
41
+ },
42
+ onError: () => {
43
+ toast.custom((t) => (
44
+ <Toast variant="error" message="Erro ao alterar senha. Tente novamente." toastId={t} />
45
+ ));
46
+ },
47
+ });
48
+
49
+ const onSubmit = (data: ChangePasswordFormData) => {
50
+ mutation.mutate(data);
51
+ };
52
+
53
+ return {
54
+ register: form.register,
55
+ handleSubmit: form.handleSubmit(onSubmit),
56
+ errors: form.formState.errors,
57
+ isLoading: mutation.isPending,
58
+ };
59
+ }
@@ -0,0 +1,69 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { useForm } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { toast } from 'sonner';
7
+ import { Toast } from '../../ui/feedback/Toast';
8
+ import { changePhoneSchema, type ChangePhoneFormData } from '../../../utils/validators/account';
9
+ import { formatPhone } from '../../../utils/format/masks';
10
+ import { requestPhoneChangeAction } from '../../../modules/accounts/actions/account-management.action';
11
+
12
+ export default function useChangePhoneForm() {
13
+ const [showVerification, setShowVerification] = useState(false);
14
+
15
+ const form = useForm<ChangePhoneFormData>({
16
+ resolver: zodResolver(changePhoneSchema),
17
+ mode: 'onChange',
18
+ defaultValues: { phone: '' },
19
+ });
20
+
21
+ const phoneValue = form.watch('phone');
22
+ const errors = form.formState.errors;
23
+ const isSubmitting = form.formState.isSubmitting;
24
+
25
+ const handlePhoneChange = useCallback(
26
+ (e: React.ChangeEvent<HTMLInputElement>) => {
27
+ const formatted = formatPhone(e.target.value);
28
+ form.setValue('phone', formatted, { shouldValidate: true });
29
+ },
30
+ [form]
31
+ );
32
+
33
+ const handleContinue = form.handleSubmit(async (data) => {
34
+ const result = await requestPhoneChangeAction(data.phone);
35
+
36
+ if (!result.success) {
37
+ toast.custom((t) => (
38
+ <Toast
39
+ variant="error"
40
+ message={result.error ?? 'Número já existente ou ocorreu um erro. Tente novamente mais tarde.'}
41
+ toastId={t}
42
+ />
43
+ ));
44
+ return;
45
+ }
46
+
47
+ setShowVerification(true);
48
+ });
49
+
50
+ const handleBack = useCallback(() => {
51
+ setShowVerification(false);
52
+ }, []);
53
+
54
+ const resetForm = useCallback(() => {
55
+ form.reset();
56
+ setShowVerification(false);
57
+ }, [form]);
58
+
59
+ return {
60
+ showVerification,
61
+ phoneValue,
62
+ isSubmitting,
63
+ errors,
64
+ handlePhoneChange,
65
+ handleContinue,
66
+ handleBack,
67
+ resetForm,
68
+ };
69
+ }
@@ -0,0 +1,40 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useEffect } from 'react';
4
+ import { AccountSectionType } from '../../../enums/AccountSectionType';
5
+
6
+ interface UseConfigurationsModalProps {
7
+ isOpen: boolean;
8
+ initialSection?: AccountSectionType;
9
+ }
10
+
11
+ export default function useConfigurationsModal({
12
+ isOpen,
13
+ initialSection,
14
+ }: UseConfigurationsModalProps) {
15
+ const [activeSection, setActiveSection] = useState<AccountSectionType>(
16
+ AccountSectionType.MY_PROFILE
17
+ );
18
+
19
+ useEffect(() => {
20
+ if (isOpen && initialSection) {
21
+ setActiveSection(initialSection);
22
+ } else if (isOpen && !initialSection) {
23
+ setActiveSection(AccountSectionType.MY_PROFILE);
24
+ }
25
+ }, [isOpen, initialSection]);
26
+
27
+ const handleSectionChange = useCallback((section: AccountSectionType) => {
28
+ setActiveSection(section);
29
+ }, []);
30
+
31
+ const resetSection = useCallback(() => {
32
+ setActiveSection(AccountSectionType.MY_PROFILE);
33
+ }, []);
34
+
35
+ return {
36
+ activeSection,
37
+ setActiveSection: handleSectionChange,
38
+ resetSection,
39
+ };
40
+ }
@@ -0,0 +1,69 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
5
+ import { toast } from 'sonner';
6
+ import { Toast } from '../../ui/feedback/Toast';
7
+ import { USER_QUERY_KEY } from '../../../modules/auth/hooks/useUserQuery';
8
+ import { disableTwoFactorAction } from '../../../modules/accounts/actions/account-management.action';
9
+
10
+ interface UseDisableTwoFactorAuthFormProps {
11
+ onClose: () => void;
12
+ onSuccess?: () => void;
13
+ }
14
+
15
+ export default function useDisableTwoFactorAuthForm({
16
+ onClose,
17
+ onSuccess,
18
+ }: UseDisableTwoFactorAuthFormProps) {
19
+ const [code, setCode] = useState('');
20
+ const queryClient = useQueryClient();
21
+
22
+ const isValid = code.length === 6;
23
+
24
+ const mutation = useMutation({
25
+ mutationFn: (otpCode: string) => disableTwoFactorAction(otpCode),
26
+ onSuccess: (result) => {
27
+ if (!result.success) {
28
+ toast.custom((t) => (
29
+ <Toast variant="error" message={result.error ?? 'Código inválido'} toastId={t} />
30
+ ));
31
+ return;
32
+ }
33
+
34
+ queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
35
+ toast.custom((t) => (
36
+ <Toast variant="success" message="Autenticação de dois fatores desabilitada" toastId={t} />
37
+ ));
38
+ setCode('');
39
+ onSuccess?.();
40
+ },
41
+ onError: () => {
42
+ toast.custom((t) => (
43
+ <Toast variant="error" message="Erro ao desabilitar 2FA. Tente novamente." toastId={t} />
44
+ ));
45
+ },
46
+ });
47
+
48
+ const { mutate, reset } = mutation;
49
+
50
+ const handleClose = useCallback(() => {
51
+ setCode('');
52
+ reset();
53
+ onClose();
54
+ }, [onClose, reset]);
55
+
56
+ const handleSubmit = useCallback(() => {
57
+ if (!isValid) return;
58
+ mutate(code);
59
+ }, [code, isValid, mutate]);
60
+
61
+ return {
62
+ code,
63
+ setCode,
64
+ isValid,
65
+ isLoading: mutation.isPending,
66
+ handleClose,
67
+ handleSubmit,
68
+ };
69
+ }
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { toast } from 'sonner';
5
+ import { Toast } from '../../ui/feedback/Toast';
6
+ import useCountdownTimer from '../../../hooks/useCountdownTimer';
7
+ import { OTP_CODE_LENGTH, VALID_OTP_CODE } from '../../../utils/validators/account';
8
+ import { hasExactLength } from '../../../utils/validators/common';
9
+
10
+ interface UseOtpVerificationOptions {
11
+ onSuccess: () => void;
12
+ successMessage: string;
13
+ validateFn?: (code: string) => Promise<boolean>;
14
+ resendFn?: () => Promise<void>;
15
+ }
16
+
17
+ export default function useOtpVerification({
18
+ onSuccess,
19
+ successMessage,
20
+ validateFn,
21
+ resendFn,
22
+ }: UseOtpVerificationOptions) {
23
+ const [code, setCode] = useState('');
24
+ const [isLoading, setIsLoading] = useState(false);
25
+ const [hasError, setHasError] = useState(false);
26
+
27
+ const {
28
+ seconds: timer,
29
+ isExpired: canResend,
30
+ reset: resetTimer,
31
+ } = useCountdownTimer({ initialSeconds: 59 });
32
+
33
+ const handleCodeChange = useCallback(
34
+ (value: string) => {
35
+ setCode(value.toUpperCase());
36
+ if (hasError) {
37
+ setHasError(false);
38
+ }
39
+ },
40
+ [hasError]
41
+ );
42
+
43
+ const handleValidate = useCallback(async () => {
44
+ if (!hasExactLength(code, OTP_CODE_LENGTH)) return;
45
+
46
+ setIsLoading(true);
47
+ setHasError(false);
48
+
49
+ try {
50
+ let isValid: boolean;
51
+
52
+ if (validateFn) {
53
+ isValid = await validateFn(code);
54
+ } else {
55
+ await new Promise((resolve) => setTimeout(resolve, 500));
56
+ isValid = code === VALID_OTP_CODE;
57
+ }
58
+
59
+ if (!isValid) {
60
+ setHasError(true);
61
+ return;
62
+ }
63
+
64
+ toast.custom((t) => <Toast variant="success" message={successMessage} toastId={t} />);
65
+
66
+ setCode('');
67
+ onSuccess();
68
+ } catch (error) {
69
+ console.error('Erro ao validar código:', error);
70
+ setHasError(true);
71
+ } finally {
72
+ setIsLoading(false);
73
+ }
74
+ }, [code, onSuccess, successMessage, validateFn]);
75
+
76
+ const handleResend = useCallback(async () => {
77
+ if (!canResend) return;
78
+
79
+ try {
80
+ resetTimer();
81
+ setHasError(false);
82
+ setCode('');
83
+
84
+ if (resendFn) {
85
+ await resendFn();
86
+ }
87
+
88
+ toast.custom((t) => <Toast variant="success" message="Código reenviado" toastId={t} />);
89
+ } catch (error) {
90
+ console.error('Erro ao reenviar código:', error);
91
+ toast.custom((t) => (
92
+ <Toast variant="error" message="Erro ao reenviar código. Tente novamente." toastId={t} />
93
+ ));
94
+ }
95
+ }, [canResend, resetTimer, resendFn]);
96
+
97
+ const reset = useCallback(() => {
98
+ setCode('');
99
+ setHasError(false);
100
+ }, []);
101
+
102
+ const isValidLength = hasExactLength(code, OTP_CODE_LENGTH);
103
+
104
+ return {
105
+ code,
106
+ isLoading,
107
+ hasError,
108
+ timer,
109
+ canResend,
110
+ isValidLength,
111
+ handleCodeChange,
112
+ handleValidate,
113
+ handleResend,
114
+ reset,
115
+ };
116
+ }
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useEffect } from 'react';
4
+ import { useMutation } from '@tanstack/react-query';
5
+ import { toast } from 'sonner';
6
+ import { Toast } from '../../ui/feedback/Toast';
7
+ import { copyToClipboard } from '../../../utils/browser/clipboard';
8
+ import {
9
+ generateTwoFactorAction,
10
+ confirmTwoFactorAction,
11
+ } from '../../../modules/accounts/actions/account-management.action';
12
+
13
+ type Step = 'code' | 'recovery';
14
+
15
+ interface UseTwoFactorAuthFormProps {
16
+ onSuccess?: () => void;
17
+ open?: boolean;
18
+ }
19
+
20
+ export default function useTwoFactorAuthForm({ onSuccess, open }: UseTwoFactorAuthFormProps = {}) {
21
+ const [code, setCode] = useState('');
22
+ const [step, setStep] = useState<Step>('code');
23
+
24
+ const generateMutation = useMutation({
25
+ mutationFn: generateTwoFactorAction,
26
+ onSuccess: (result) => {
27
+ if (!result.success) {
28
+ toast.custom((t) => (
29
+ <Toast variant="error" message={result.error ?? 'Erro ao gerar QR Code. Tente novamente.'} toastId={t} />
30
+ ));
31
+ }
32
+ },
33
+ onError: () => {
34
+ toast.custom((t) => (
35
+ <Toast variant="error" message="Erro ao gerar QR Code. Tente novamente." toastId={t} />
36
+ ));
37
+ },
38
+ });
39
+
40
+ const confirmMutation = useMutation({
41
+ mutationFn: (otpCode: string) => confirmTwoFactorAction(otpCode),
42
+ onSuccess: (result) => {
43
+ if (!result.success) {
44
+ toast.custom((t) => (
45
+ <Toast variant="error" message={result.error ?? 'Código inválido'} toastId={t} />
46
+ ));
47
+ return;
48
+ }
49
+ setStep('recovery');
50
+ },
51
+ onError: () => {
52
+ toast.custom((t) => (
53
+ <Toast variant="error" message="Erro ao confirmar código. Tente novamente." toastId={t} />
54
+ ));
55
+ },
56
+ });
57
+
58
+ const { mutate: triggerGenerate } = generateMutation;
59
+
60
+ useEffect(() => {
61
+ if (open) {
62
+ triggerGenerate();
63
+ }
64
+ }, [open, triggerGenerate]);
65
+
66
+ const generateFailed =
67
+ (generateMutation.isSuccess && !generateMutation.data?.success) || generateMutation.isError;
68
+
69
+ const isCodeComplete = code.length === 6;
70
+
71
+ const handleSubmit = () => {
72
+ if (!isCodeComplete) return;
73
+ confirmMutation.mutate(code);
74
+ };
75
+
76
+ const handleFinalize = () => {
77
+ onSuccess?.();
78
+ setTimeout(resetForm, 300);
79
+ };
80
+
81
+ const handleCopyCodes = useCallback(async () => {
82
+ const codes = generateMutation.data?.backupCodes ?? [];
83
+ const success = await copyToClipboard(codes.join('\n'));
84
+ if (success) {
85
+ toast.custom((t) => (
86
+ <Toast variant="success" message="Códigos copiados com sucesso" toastId={t} />
87
+ ));
88
+ }
89
+ }, [generateMutation.data]);
90
+
91
+ const resetForm = () => {
92
+ setCode('');
93
+ setStep('code');
94
+ generateMutation.reset();
95
+ confirmMutation.reset();
96
+ };
97
+
98
+ return {
99
+ code,
100
+ setCode,
101
+ isCodeComplete,
102
+ isGenerating: generateMutation.isPending,
103
+ isConfirming: confirmMutation.isPending,
104
+ generateFailed,
105
+ retryGenerate: triggerGenerate,
106
+ qrcode: generateMutation.data?.qrcode,
107
+ backupCodes: generateMutation.data?.backupCodes ?? [],
108
+ step,
109
+ handleSubmit,
110
+ handleFinalize,
111
+ handleCopyCodes,
112
+ resetForm,
113
+ };
114
+ }