add bank card validation

This commit is contained in:
smanylov
2026-03-12 13:06:03 +07:00
parent 6e648a522a
commit d36b724d4f
9 changed files with 91 additions and 32 deletions
+18 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.49.0", "version": "0.58.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.49.0", "version": "0.58.0",
"dependencies": { "dependencies": {
"@a2seven/yoo-checkout": "^1.1.4", "@a2seven/yoo-checkout": "^1.1.4",
"@react-input/mask": "^2.0.4", "@react-input/mask": "^2.0.4",
@@ -15,6 +15,7 @@
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2", "@vkid/sdk": "^2.6.2",
"card-validator": "^10.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
@@ -3416,6 +3417,15 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/card-validator": {
"version": "10.0.4",
"resolved": "https://registry.npmjs.org/card-validator/-/card-validator-10.0.4.tgz",
"integrity": "sha512-1Q/tDCSvM5iL3psPo6j8KF/VoI63WA7Y4Zthy1E7l/H4Iul2e2X+1S0vp33n3OMRjLgGU2+gLaK2NsGtwArJkQ==",
"license": "MIT",
"dependencies": {
"credit-card-type": "^10.1.0"
}
},
"node_modules/chalk": { "node_modules/chalk": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3504,6 +3514,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/credit-card-type": {
"version": "10.1.2",
"resolved": "https://registry.npmjs.org/credit-card-type/-/credit-card-type-10.1.2.tgz",
"integrity": "sha512-fP9vlyeYSHfKbIdE6IOR63YivTXMy0ZqyEkBhHxuAbvpltepe+zJmQ34A2Va7zJ8J7ZaB5IMAdD+1YJOmsajJA==",
"license": "MIT"
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+2 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "no-copy-frontend", "name": "no-copy-frontend",
"version": "0.58.0", "version": "0.59.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 2999", "dev": "next dev -p 2999",
@@ -16,6 +16,7 @@
"@tanstack/react-query": "^5.90.11", "@tanstack/react-query": "^5.90.11",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@vkid/sdk": "^2.6.2", "@vkid/sdk": "^2.6.2",
"card-validator": "^10.0.4",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"jose": "^6.1.2", "jose": "^6.1.2",
"next": "^16.0.7", "next": "^16.0.7",
+9
View File
@@ -112,6 +112,15 @@
&.password { &.password {
padding-right: 32px; padding-right: 32px;
} }
&:disabled {
opacity: 0.6;
}
&.valid-card {
border: 2px solid #2f9e44;
background: #ecf8ee;
}
} }
.form-options { .form-options {
+10
View File
@@ -1486,6 +1486,16 @@
transition: all 0.3s; transition: all 0.3s;
margin-top: 20px; margin-top: 20px;
} }
.card-type {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
}
} }
} }
+3 -3
View File
@@ -3,16 +3,16 @@
import { useState, ChangeEvent, useEffect } from 'react'; import { useState, ChangeEvent, useEffect } from 'react';
import { InputMask, format } from '@react-input/mask'; import { InputMask, format } from '@react-input/mask';
import styles from '@/app/styles/module/login.module.scss'; import styles from '@/app/styles/module/login.module.scss';
import { PayoutMethods } from '@/app/actions/referralsActions';
interface BankCardProps { interface BankCardProps {
bankCardState: string | undefined; bankCardState: string | undefined;
validateHandler: (e: ChangeEvent<HTMLInputElement>) => void; validateHandler: (e: ChangeEvent<HTMLInputElement>) => void;
clearCondition?: string | undefined | null; clearCondition?: string | undefined | null;
disabled: boolean; disabled: boolean;
isValid: boolean | undefined;
} }
export default function BankCardInput({ bankCardState, validateHandler, disabled }: BankCardProps) { export default function BankCardInput({ bankCardState, validateHandler, disabled, isValid }: BankCardProps) {
const [bankCard, setBankCard] = useState<string | undefined>(''); const [bankCard, setBankCard] = useState<string | undefined>('');
useEffect(() => { useEffect(() => {
@@ -37,7 +37,7 @@ export default function BankCardInput({ bankCardState, validateHandler, disabled
type="phone" type="phone"
id="bankCard" id="bankCard"
name="bankCard" name="bankCard"
className={`${styles['form-input']}`} className={`${styles['form-input']} ${isValid ? styles['valid-card'] : ''}`}
mask="____ ____ ____ ____" mask="____ ____ ____ ____"
replacement={{ _: /\d/ }} replacement={{ _: /\d/ }}
value={bankCard} value={bankCard}
-18
View File
@@ -10,24 +10,6 @@ export default function SecurePayments() {
<h3> <h3>
Безопасные платежи Безопасные платежи
</h3> </h3>
<div
className='flex gap-4'
>
<button
className='btn btn-primary'
onClick={async () => {
const response = await referralRefill();
console.log(response);
if (response?.account === 'refill') {
toast.success('refill');
} else {
toast.error(`error refill: ${response?.error?.message_desc}`);
}
}}
>
referralRefill
</button>
</div>
<div className="secure-payments-grid"> <div className="secure-payments-grid">
<div className="secure-payments-card"> <div className="secure-payments-card">
<div className="secure-payments-card-header"> <div className="secure-payments-card-header">
@@ -8,6 +8,12 @@ import { addPayoutMethod, PayoutMethodFormState, getPayoutMethods, PayoutMethods
import { bankCardFormSchema } from '@/app/actions/definitions'; import { bankCardFormSchema } from '@/app/actions/definitions';
import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner'; import { toast } from 'sonner';
import valid from 'card-validator';
interface cardInfoType {
cardType: string,
potentiallyValid: boolean
}
export default function SettingReferralProgram() { export default function SettingReferralProgram() {
const [state, formAction, isPending] = useActionState( const [state, formAction, isPending] = useActionState(
@@ -36,6 +42,8 @@ export default function SettingReferralProgram() {
const [paymentsType, setPaymentsType] = useState('bank-card'); const [paymentsType, setPaymentsType] = useState('bank-card');
const [defaultCardNumber, setDefaultCardNumber] = useState<PayoutMethods | undefined>(undefined); const [defaultCardNumber, setDefaultCardNumber] = useState<PayoutMethods | undefined>(undefined);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [cardInfo, setCardInfo] = useState<null | cardInfoType>(null);
const [initialCardInfo, setInitialCardInfo] = useState<null | cardInfoType>(null);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const t = useTranslations('Global'); const t = useTranslations('Global');
@@ -52,6 +60,17 @@ export default function SettingReferralProgram() {
if (payoutMethods) { if (payoutMethods) {
payoutMethods.forEach(e => { payoutMethods.forEach(e => {
if (e.default) { if (e.default) {
const numberValidation = valid.number(e.cardNumber);
if (numberValidation.card) {
setCardInfo({
cardType: numberValidation.card.type,
potentiallyValid: numberValidation.isPotentiallyValid
});
setInitialCardInfo({
cardType: numberValidation.card.type,
potentiallyValid: numberValidation.isPotentiallyValid
})
}
setDefaultCardNumber(e); setDefaultCardNumber(e);
} }
}) })
@@ -78,8 +97,15 @@ export default function SettingReferralProgram() {
async function onChangeHandler(e: ChangeEvent<HTMLInputElement>): Promise<void> { async function onChangeHandler(e: ChangeEvent<HTMLInputElement>): Promise<void> {
const fieldName = e.target.name; const fieldName = e.target.name;
const validatedField = await validateField(fieldName, e.target.value); const validatedField = await validateField(fieldName, e.target.value);
const numberValidation = valid.number(e.target.value);
if (numberValidation.card) {
setCardInfo({
cardType: numberValidation.card.type,
potentiallyValid: numberValidation.isPotentiallyValid
});
}
if (validatedField.success) { if (validatedField.success) {
setFormState(prevState => { setFormState(prevState => {
@@ -162,11 +188,23 @@ export default function SettingReferralProgram() {
<h4> <h4>
{t('bank-card-number')} {t('bank-card-number')}
</h4> </h4>
<BankCardInput <div
bankCardState={defaultCardNumber?.cardNumber} className="relative"
validateHandler={onChangeHandler} >
disabled={!isEditing} <BankCardInput
/> bankCardState={defaultCardNumber?.cardNumber}
validateHandler={onChangeHandler}
disabled={!isEditing}
isValid={cardInfo?.potentiallyValid}
/>
{cardInfo && (
<div
className="card-type"
>
{cardInfo.cardType}
</div>
)}
</div>
{formState?.error?.bankCard && ( {formState?.error?.bankCard && (
<p className="text-sm text-red-500"> <p className="text-sm text-red-500">
{ {
@@ -199,7 +237,8 @@ export default function SettingReferralProgram() {
className="btn-primary" className="btn-primary"
disabled={isPending} disabled={isPending}
onClick={() => { onClick={() => {
setIsEditing(false) setIsEditing(false);
setCardInfo(initialCardInfo);
}} }}
> >
{t('cancel')} {t('cancel')}
@@ -208,7 +247,7 @@ export default function SettingReferralProgram() {
<button <button
type="submit" type="submit"
className="btn-primary" className="btn-primary"
disabled={isPending} disabled={isPending || !cardInfo?.potentiallyValid}
> >
{t("save-changes")} {t("save-changes")}
{isPending && ( {isPending && (
+1
View File
@@ -225,6 +225,7 @@
"start-upload": "Start upload", "start-upload": "Start upload",
"completed": "Completed", "completed": "Completed",
"pending": "Pending", "pending": "Pending",
"canceled": "Canceled",
"all-content": "All content", "all-content": "All content",
"monitoring": "Monitoring", "monitoring": "Monitoring",
"efficiency": "Efficiency", "efficiency": "Efficiency",
+1
View File
@@ -225,6 +225,7 @@
"start-upload": "Начать загрузку", "start-upload": "Начать загрузку",
"completed": "Завершено", "completed": "Завершено",
"pending": "В ожидании", "pending": "В ожидании",
"canceled": "Отменено",
"all-content": "Весь контент", "all-content": "Весь контент",
"monitoring": "Мониторинг", "monitoring": "Мониторинг",
"efficiency": "Эффективность", "efficiency": "Эффективность",