From 1fec3b2584db1d50a5da128d79d485b0afe8b13f Mon Sep 17 00:00:00 2001 From: smanylov Date: Fri, 13 Feb 2026 19:17:10 +0700 Subject: [PATCH] add INN registration field --- package-lock.json | 17 +- package.json | 1 + src/app/actions/action.ts | 33 ++++ src/app/actions/auth.ts | 78 +++++---- src/app/lib/validation-config-parser.ts | 29 ++++ src/app/lib/validation-config.json | 14 ++ src/app/ui/forms/register-form.tsx | 217 +++++++++++++++++++----- src/i18n/messages/en.json | 8 +- src/i18n/messages/ru.json | 8 +- 9 files changed, 331 insertions(+), 74 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66e0bfe..ad6333f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "no-copy-frontend", - "version": "0.17.0", + "version": "0.40.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "no-copy-frontend", - "version": "0.17.0", + "version": "0.40.0", "dependencies": { "@react-input/mask": "^2.0.4", "@tailwindcss/postcss": "^4.1.17", @@ -26,6 +26,7 @@ "sonner": "^2.0.7", "swiper": "^12.0.3", "tailwindcss": "^4.1.17", + "use-debounce": "^10.1.0", "validator": "^13.15.26", "zod": "^4.1.13", "zustand": "^5.0.9" @@ -7822,6 +7823,18 @@ "punycode": "^2.1.0" } }, + "node_modules/use-debounce": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz", + "integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/use-intl": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz", diff --git a/package.json b/package.json index b5a2879..5a7f0f3 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "sonner": "^2.0.7", "swiper": "^12.0.3", "tailwindcss": "^4.1.17", + "use-debounce": "^10.1.0", "validator": "^13.15.26", "zod": "^4.1.13", "zustand": "^5.0.9" diff --git a/src/app/actions/action.ts b/src/app/actions/action.ts index f4c7f13..71458cc 100644 --- a/src/app/actions/action.ts +++ b/src/app/actions/action.ts @@ -126,3 +126,36 @@ export async function getBuildData() { return null; } } + +export async function fetchINN(inn: string) { + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30004, + message_body: { + inn: inn, + } + }), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + + if (response.ok) { + const parsed = await response.json() + if (parsed?.message_code === 0) { + return parsed.message_body + } else { + return null + } + + } else { + return null + } + } catch (error) { + return null + } +} \ No newline at end of file diff --git a/src/app/actions/auth.ts b/src/app/actions/auth.ts index e68cfe6..d62c0a0 100644 --- a/src/app/actions/auth.ts +++ b/src/app/actions/auth.ts @@ -3,6 +3,7 @@ import { loginFormSchema, API_BASE_URL, getSignupFormSchema } from '@/app/actions/definitions'; import { createSession, deleteSession, getSessionData, updateSession } from '@/app/actions/session'; import { redirect } from 'next/navigation'; +import { fetchINN } from '@/app/actions/action'; export async function logout() { const token = await getSessionData('token'); @@ -130,15 +131,16 @@ export async function authorization( export type FormState = { previousState: { - fullName: string; - email: string; - password: string; - confirm_password: string; - phone: string; - companyName?: string; - agree: string; - referralCode?: string - }, + fullName?: string | undefined; + email?: string | undefined; + password?: string | undefined; + confirm_password?: string | undefined; + phone?: string | undefined; + companyName?: string | undefined; + agree?: string | undefined; + referralCode?: string | undefined; + inn?: string | undefined; + } | undefined, mailConfirm?: boolean, error: Record | null }; @@ -157,6 +159,7 @@ export async function registration( const companyName = formData.get('companyName') as string || ''; const agree = formData.get('agree') as string || ''; const referralCode = formData.get('referralCode'); + const inn = formData.get('inn') as string || ''; const SignupFormSchema = await getSignupFormSchema(accountType); @@ -167,25 +170,41 @@ export async function registration( password, confirm_password, agree, - ...(accountType === 'b2b' && { companyName }) + ...(accountType === 'b2b' && { companyName, inn }) }; const validatedFields = await SignupFormSchema.safeParse(dataToValidate); console.log(`try to register ${accountType}`); - if (!validatedFields.success) { + let confirmedCompanyName: boolean = true; + if (accountType === 'b2b') { + try { + const response = await fetchINN(inn); + confirmedCompanyName = response?.companyName ? true : false; + } catch (error) { + confirmedCompanyName = false; + } + } + + if (!validatedFields.success || !confirmedCompanyName) { const errors: Record = {} - JSON.parse(validatedFields.error.message).forEach((obj: { - message: string, - path: string - }) => { - if (errors[obj.path[0]]) { - errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`; - } else { - errors[obj.path[0]] = obj.message; - } - }); + if (validatedFields?.error?.message) { + JSON?.parse(validatedFields?.error?.message).forEach((obj: { + message: string, + path: string + }) => { + if (errors[obj.path[0]]) { + errors[obj.path[0]] = `${errors[obj.path[0]]}&${obj.message}`; + } else { + errors[obj.path[0]] = obj.message; + } + }); + } + + if (!confirmedCompanyName) { + errors.inn = 'inn-invalid' + } return { previousState: { @@ -195,7 +214,7 @@ export async function registration( confirm_password, phone, agree, - ...(accountType === 'b2b' && { companyName }) + ...(accountType === 'b2b' && { companyName, inn }) }, error: errors }; @@ -214,7 +233,7 @@ export async function registration( phone: phone, password, accountType, - ...(accountType === 'b2b' && { companyName: companyName }), + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }), ...(referralCode !== '' && { referralLink: referralCode }) } }), @@ -230,7 +249,7 @@ export async function registration( phone: phone, password, accountType, - ...(accountType === 'b2b' && { companyName: companyName }), + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }), ...(referralCode !== '' && { referralLink: referralCode }) }) @@ -241,8 +260,6 @@ export async function registration( message_code: number }; - console.log(parsed); - if (parsed.message_code === 0) { console.log(`registration - ${accountType}`); console.log({ @@ -252,10 +269,11 @@ export async function registration( password, accountType, referralLink: 'fds324', - ...(accountType === 'b2b' && { companyName: companyName }) + ...(accountType === 'b2b' && { companyName: companyName, inn: inn }) }); await createSession(parsed.message_body.token, email as string); } else { + console.log(parsed); throw parsed; } } else { @@ -320,7 +338,8 @@ export async function registration( confirm_password, phone, companyName, - agree + agree, + inn }, error: errors }; @@ -334,7 +353,8 @@ export async function registration( confirm_password, phone, companyName, - agree + agree, + inn }, error: errors }; diff --git a/src/app/lib/validation-config-parser.ts b/src/app/lib/validation-config-parser.ts index 886b72f..b964ad5 100644 --- a/src/app/lib/validation-config-parser.ts +++ b/src/app/lib/validation-config-parser.ts @@ -163,6 +163,35 @@ export function createValidationSchema(config: ValidationConfig, accountType?: ' schema.password = passwordSchema; } + // inn + if (config.inn && accountType !== 'b2c') { + let innSchema = z.string(); + + // Минимальная длина + if (config.inn.minSize) { + innSchema = innSchema.min(config.inn.minSize.value, { + error: config.inn.minSize.message + }); + } + + // Максимальная длина + if (config.inn.maxSize) { + innSchema = innSchema.max(config.inn.maxSize.value, { + error: config.inn.maxSize.message + }); + } + + // Допустимые символы + if (config.inn.allowedSymbols) { + innSchema = innSchema.regex( + new RegExp(config.inn.allowedSymbols.value), + { message: config.inn.allowedSymbols.message } + ); + } + + schema.inn = innSchema; + } + // Дополнительные поля для формы регистрации schema.confirm_password = z.string(); schema.agree = z.string().min(2, { error: 'register-error-agree' }); diff --git a/src/app/lib/validation-config.json b/src/app/lib/validation-config.json index 97abe6c..bd9f6f2 100644 --- a/src/app/lib/validation-config.json +++ b/src/app/lib/validation-config.json @@ -87,5 +87,19 @@ "value": "^[0-9\\+]+$", "message": "register-error-phone-not-allowed-symbols" } + }, + "inn": { + "maxSize": { + "value": 10, + "message": "register-error-inn-max" + }, + "minSize": { + "value": 10, + "message": "register-error-inn-min" + }, + "allowedSymbols": { + "value": "^[0-9]+$", + "message": "register-error-inn-not-allowed-symbols" + } } } \ No newline at end of file diff --git a/src/app/ui/forms/register-form.tsx b/src/app/ui/forms/register-form.tsx index 0321d6f..e5fb574 100644 --- a/src/app/ui/forms/register-form.tsx +++ b/src/app/ui/forms/register-form.tsx @@ -1,15 +1,17 @@ 'use client' import styles from '@/app/styles/module/login.module.scss'; -import { ChangeEvent, useActionState, useEffect, useRef, useState } from 'react'; +import { ChangeEvent, useActionState, useEffect, useMemo, useRef, useState } from 'react'; import { registration, FormState, confirmEmail } from '@/app/actions/auth'; import PhoneInput from '@/app/ui/inputs/phone-input'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { IconEye } from '@/app/ui/icons/icons'; -import { SignupFormSchema } from '@/app/actions/definitions'; +import { getSignupFormSchema } from '@/app/actions/definitions'; import { useSearchParams } from 'next/navigation'; import * as z from 'zod' +import { fetchINN } from '@/app/actions/action'; +import { useDebouncedCallback } from 'use-debounce'; export default function RegisterForm() { const searchParams = useSearchParams(); @@ -39,7 +41,40 @@ export default function RegisterForm() { setShowMailConfirmWindow(true); } - }, [state]) + }, [state]); + + const debouncedFetchINN = useDebouncedCallback( + async (value) => { + const response = await fetchINN(value); + + if (response.companyName) { + setFormState({ + ...formState, + previousState: { + ...formState?.previousState, + companyName: response.companyName + }, + error: { + ...formState?.error, + companyName: '' + } + }); + } else { + setFormState({ + ...formState, + previousState: { + companyName: response.companyName + }, + error: { + ...formState?.error, + companyName: '', + inn: 'inn-invalid' + } + }); + } + }, + 500 + ); function showPassowrd(target: 'password' | 'confirm-password') { if (target === 'password') { @@ -49,13 +84,38 @@ export default function RegisterForm() { } } - function validateField( + type SignupFormSchema = Awaited>; + const [signupFormSchema, setSignupFormSchema] = useState(null);; + + useEffect(() => { + let isMounted = true; + + const loadSchema = async () => { + const result = await getSignupFormSchema(accountType); + if (isMounted) { + setSignupFormSchema(result); + } + }; + + loadSchema(); + + return () => { + isMounted = false; + }; + }, [accountType]); + + async function validateField( fieldName: string, value: string, ) { switch (fieldName) { case 'confirm_password': - const baseSchema = SignupFormSchema.shape.confirm_password; + const baseSchema = signupFormSchema?.shape.confirm_password; + + if (!baseSchema) { + return { success: true, data: value }; + } + const baseResult = baseSchema.safeParse(value); if (!baseResult.success) { @@ -85,7 +145,12 @@ export default function RegisterForm() { case 'password': case 'companyName': case 'agree': - const schema = SignupFormSchema.shape[fieldName]; + case 'inn': + const schema = signupFormSchema?.shape[fieldName]; + if (!schema) { + return { success: true, data: value }; + } + if (fieldName === 'phone') { return schema.safeParse(value.replace(/[-\(\)\s]/g, '')); } @@ -102,12 +167,45 @@ export default function RegisterForm() { } } - function onChangeHandler(e: ChangeEvent): void { + async function onChangeHandler(e: ChangeEvent): Promise { const fieldName = e.target.name; if (fieldName !== 'phone') { e.target.value = e.target.value.replace(/([^a-zA-Zа-яёА-ЯЁ0-9@.!#$%&"'*+/=?^_{|}~\-\s])/g, ''); } - const validatedField = validateField(fieldName, e.target.value); + + if (fieldName === 'inn') { + const currentValue = e.target.value; + + const cleanedValue = currentValue.replace(/([^0-9])/g, ''); + + if (cleanedValue.length > 10) { + e.target.value = cleanedValue.slice(0, 10); + debouncedFetchINN(e.target.value); + } else { + e.target.value = cleanedValue; + } + + if (cleanedValue.length === 10) { + debouncedFetchINN(cleanedValue); + } else { + if (formState) { + setFormState({ + ...formState, + previousState: { + ...formState?.previousState, + companyName: '' + }, + error: { + ...formState?.error, + companyName: '', + inn: '' + } + }); + } + } + } + + const validatedField = await validateField(fieldName, e.target.value); if (validatedField.success) { setFormState(prevState => { @@ -128,16 +226,18 @@ export default function RegisterForm() { function switchRegistrationTypeHandler(e: React.MouseEvent, type: 'b2c' | 'b2b') { e.preventDefault(); setAccountType(type); - if (formState) { + if (formState?.previousState) { setFormState({ ...formState, previousState: { ...formState.previousState, - companyName: '' + companyName: '', + inn: '' }, error: { ...formState.error, - companyName: '' + companyName: '', + inn: '' } }); } @@ -251,36 +351,71 @@ export default function RegisterForm() { {accountType === 'b2b' ? ( -
- - - {formState?.error?.companyName && ( -

- { - formState?.error?.companyName.split('&').map((e, index) => { - return ( - - {t(e)} -
-
- ) - }) - } -

- )} -
+ <> +
+ + + {formState?.error?.inn && ( +

+ { + formState?.error?.inn.split('&').map((e, index) => { + return ( + + {t(e)} +
+
+ ) + }) + } +

+ )} +
+ +
+ + + {formState?.error?.companyName && ( +

+ { + formState?.error?.companyName.split('&').map((e, index) => { + return ( + + {t(e)} +
+
+ ) + }) + } +

+ )} +
+ ) : (