@greatapps/common 1.1.23 → 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.
- package/dist/components/account/ConfigurationsMyAccountModal.mjs +121 -0
- package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -0
- package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +45 -0
- package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -0
- package/dist/components/account/DisableTwoFactorAuthModal.mjs +67 -0
- package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -0
- package/dist/components/account/TwoFactorAuthModal.mjs +190 -0
- package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -0
- package/dist/components/account/constants.mjs +32 -0
- package/dist/components/account/constants.mjs.map +1 -0
- package/dist/components/account/hooks/useChangeEmailForm.mjs +53 -0
- package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -0
- package/dist/components/account/hooks/useChangePasswordForm.mjs +47 -0
- package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -0
- package/dist/components/account/hooks/useChangePhoneForm.mjs +64 -0
- package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -0
- package/dist/components/account/hooks/useConfigurationsModal.mjs +33 -0
- package/dist/components/account/hooks/useConfigurationsModal.mjs.map +1 -0
- package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +54 -0
- package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -0
- package/dist/components/account/hooks/useOtpVerification.mjs +94 -0
- package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -0
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +88 -0
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -0
- package/dist/components/account/sections/ChangeEmailModal.mjs +180 -0
- package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -0
- package/dist/components/account/sections/ChangePasswordSection.mjs +64 -0
- package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -0
- package/dist/components/account/sections/ChangePhoneModal.mjs +182 -0
- package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -0
- package/dist/components/account/sections/MyProfileSection.mjs +312 -0
- package/dist/components/account/sections/MyProfileSection.mjs.map +1 -0
- package/dist/components/account/sections/PreferencesSection.mjs +246 -0
- package/dist/components/account/sections/PreferencesSection.mjs.map +1 -0
- package/dist/components/account/sections/SecuritySection.mjs +147 -0
- package/dist/components/account/sections/SecuritySection.mjs.map +1 -0
- package/dist/components/ui/form/ComboboxField.mjs +130 -0
- package/dist/components/ui/form/ComboboxField.mjs.map +1 -0
- package/dist/components/ui/form/FormField.mjs +103 -0
- package/dist/components/ui/form/FormField.mjs.map +1 -0
- package/dist/components/ui/form/PhoneInput.mjs +78 -0
- package/dist/components/ui/form/PhoneInput.mjs.map +1 -0
- package/dist/components/ui/form/Select.mjs.map +1 -1
- package/dist/components/ui/form/SelectField.mjs +80 -0
- package/dist/components/ui/form/SelectField.mjs.map +1 -0
- package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs +40 -0
- package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs.map +1 -0
- package/dist/components/ui/form/TextAreaField.mjs +56 -0
- package/dist/components/ui/form/TextAreaField.mjs.map +1 -0
- package/dist/enums/AccountSectionType.mjs +11 -0
- package/dist/enums/AccountSectionType.mjs.map +1 -0
- package/dist/hooks/useCountdownTimer.mjs +29 -0
- package/dist/hooks/useCountdownTimer.mjs.map +1 -0
- package/dist/hooks/useIsMobile.mjs +18 -0
- package/dist/hooks/useIsMobile.mjs.map +1 -0
- package/dist/hooks/usePasswordVisibility.mjs +13 -0
- package/dist/hooks/usePasswordVisibility.mjs.map +1 -0
- package/dist/index.mjs +80 -0
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/actions/account-management.action.mjs +163 -0
- package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -0
- package/dist/modules/accounts/hooks/useAccountManagement.mjs +64 -0
- package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -0
- package/dist/modules/accounts/services/account.service.mjs +162 -25
- package/dist/modules/accounts/services/account.service.mjs.map +1 -1
- package/dist/modules/accounts/services/two-factor.service.mjs +77 -0
- package/dist/modules/accounts/services/two-factor.service.mjs.map +1 -0
- package/dist/server.mjs +36 -0
- package/dist/server.mjs.map +1 -1
- package/dist/utils/browser/clipboard.mjs +36 -0
- package/dist/utils/browser/clipboard.mjs.map +1 -0
- package/dist/utils/file/index.mjs +9 -0
- package/dist/utils/file/index.mjs.map +1 -0
- package/dist/utils/format/masks.mjs +56 -0
- package/dist/utils/format/masks.mjs.map +1 -0
- package/dist/utils/intl/locales.mjs +12 -0
- package/dist/utils/intl/locales.mjs.map +1 -0
- package/dist/utils/intl/timezones.mjs +41 -0
- package/dist/utils/intl/timezones.mjs.map +1 -0
- package/dist/utils/validators/account.mjs +26 -0
- package/dist/utils/validators/account.mjs.map +1 -0
- package/dist/utils/validators/common.mjs +47 -0
- package/dist/utils/validators/common.mjs.map +1 -0
- package/package.json +6 -1
- package/src/components/account/ConfigurationsMyAccountModal.tsx +147 -0
- package/src/components/account/ConfirmGlobalPreferencesModal.tsx +66 -0
- package/src/components/account/DisableTwoFactorAuthModal.tsx +86 -0
- package/src/components/account/TwoFactorAuthModal.tsx +221 -0
- package/src/components/account/constants.ts +29 -0
- package/src/components/account/hooks/useChangeEmailForm.tsx +56 -0
- package/src/components/account/hooks/useChangePasswordForm.tsx +59 -0
- package/src/components/account/hooks/useChangePhoneForm.tsx +69 -0
- package/src/components/account/hooks/useConfigurationsModal.ts +40 -0
- package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +69 -0
- package/src/components/account/hooks/useOtpVerification.tsx +116 -0
- package/src/components/account/hooks/useTwoFactorAuthForm.tsx +114 -0
- package/src/components/account/sections/ChangeEmailModal.tsx +224 -0
- package/src/components/account/sections/ChangePasswordSection.tsx +64 -0
- package/src/components/account/sections/ChangePhoneModal.tsx +229 -0
- package/src/components/account/sections/MyProfileSection.tsx +337 -0
- package/src/components/account/sections/PreferencesSection.tsx +277 -0
- package/src/components/account/sections/SecuritySection.tsx +160 -0
- package/src/components/ui/form/ComboboxField.tsx +154 -0
- package/src/components/ui/form/FormField.tsx +130 -0
- package/src/components/ui/form/PhoneInput.tsx +98 -0
- package/src/components/ui/form/Select.tsx +1 -0
- package/src/components/ui/form/SelectField.tsx +118 -0
- package/src/components/ui/form/SwitchOptionFieldWithIcon.tsx +47 -0
- package/src/components/ui/form/TextAreaField.tsx +70 -0
- package/src/enums/AccountSectionType.ts +6 -0
- package/src/hooks/useCountdownTimer.ts +41 -0
- package/src/hooks/useIsMobile.ts +19 -0
- package/src/hooks/usePasswordVisibility.ts +13 -0
- package/src/index.ts +70 -0
- package/src/modules/accounts/actions/account-management.action.ts +197 -0
- package/src/modules/accounts/hooks/useAccountManagement.ts +79 -0
- package/src/modules/accounts/services/account.service.ts +229 -50
- package/src/modules/accounts/services/two-factor.service.ts +98 -0
- package/src/modules/accounts/types.ts +157 -0
- package/src/server.ts +21 -0
- package/src/utils/browser/clipboard.ts +34 -0
- package/src/utils/file/index.ts +5 -0
- package/src/utils/format/masks.ts +52 -0
- package/src/utils/intl/locales.ts +15 -0
- package/src/utils/intl/timezones.ts +53 -0
- package/src/utils/validators/account.ts +36 -0
- 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
|
+
}
|