From 048e1269ada4bdbf0551055bd11b202fb917b904 Mon Sep 17 00:00:00 2001 From: smanylov Date: Thu, 19 Feb 2026 17:23:52 +0700 Subject: [PATCH] add tokens bundle payments --- src/app/actions/action.ts | 35 ++++ src/app/actions/yooKassa.ts | 36 ++-- src/app/components/UploadSectionFile.tsx | 4 +- src/app/styles/global-styles.scss | 13 ++ src/app/styles/payment-container.scss | 5 + src/app/ui/payment/payment-tab-buy-tokens.tsx | 193 +++++++++++++++++- src/app/ui/payment/payment-tab-tariffs.tsx | 65 ++++-- src/app/ui/search/section-search-file.tsx | 4 +- src/i18n/messages/en.json | 4 +- src/i18n/messages/ru.json | 4 +- 10 files changed, 316 insertions(+), 47 deletions(-) diff --git a/src/app/actions/action.ts b/src/app/actions/action.ts index 86b8f2b..c83b761 100644 --- a/src/app/actions/action.ts +++ b/src/app/actions/action.ts @@ -106,6 +106,41 @@ export async function fetchTariffs() { } } +export async function fetchTokensBundle() { + + try { + const response = await fetch(`${API_BASE_URL}/api/v1/data`, { + method: 'POST', + body: JSON.stringify({ + version: 1, + msg_id: 30001, + message_body: { + action: 'tariffs_by_type', + type: 'token' + } + }), + 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?.token_tariffs; + } else { + return null + } + + } else { + return null + } + } catch (error) { + return null + } +} + export async function getBuildData() { try { diff --git a/src/app/actions/yooKassa.ts b/src/app/actions/yooKassa.ts index 16c551d..a6a00de 100644 --- a/src/app/actions/yooKassa.ts +++ b/src/app/actions/yooKassa.ts @@ -6,17 +6,7 @@ import { API_BASE_URL } from '@/app/actions/definitions'; const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4' }); -const idempotenceKey = '02347fc4-a1f0-49db-807e-f0d67c2ed5a5'; - export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string) { - console.log('makePaymentOperation'); - console.log({ - email, - tariffId: tariffId, - operationType: 'TARIFF', - operationUuid, - }); - const formData = new FormData(); formData.append('email', email); @@ -31,12 +21,14 @@ export async function makePaymentOperation(email: string, tariffId: number, oper }); if (response.ok) { - const parsed = await response.json() - console.log(parsed); - return true - + const parsed = await response.json(); + if (parsed.paymentUuid) { + return true + } else { + throw 'error' + } } else { - return null + throw 'error' } } catch (error) { return null @@ -44,11 +36,11 @@ export async function makePaymentOperation(email: string, tariffId: number, oper } export async function yooKasaCreatePayment(amount: number, tariff: number) { - console.log(amount); - console.log(tariff); - /* return true; */ const userEmail = await getSessionData('email'); + const timestamp = Date.now(); + const random = Math.random().toString(36).substring(2, 10); + const idempotenceKey = `${userEmail}-${timestamp}-${amount}-${tariff}-${random}`; if (!userEmail) { return null; @@ -73,17 +65,13 @@ export async function yooKasaCreatePayment(amount: number, tariff: number) { }; try { - const payment = await checkout.createPayment(createPayload, Date.now().toLocaleString()); + const payment = await checkout.createPayment(createPayload, idempotenceKey); const response = await makePaymentOperation(userEmail, tariff, payment.id); - console.log('yooKasaCreatePayment') - console.log(response); - if (response) { + if (response && payment) { return payment?.confirmation.confirmation_url; } else { return null } - - /* return payment; */ } catch (error) { return null } diff --git a/src/app/components/UploadSectionFile.tsx b/src/app/components/UploadSectionFile.tsx index 941bb45..651e58d 100644 --- a/src/app/components/UploadSectionFile.tsx +++ b/src/app/components/UploadSectionFile.tsx @@ -464,8 +464,8 @@ export default function UploadSectionFile({ { error && ( -
-

{error}

+
+

{error}

) } diff --git a/src/app/styles/global-styles.scss b/src/app/styles/global-styles.scss index 4dd2171..e501ab0 100644 --- a/src/app/styles/global-styles.scss +++ b/src/app/styles/global-styles.scss @@ -634,4 +634,17 @@ .yaPreloadingSuggestBlockContainer { height: 52px !important; +} + +.error-message { + margin-top: 1rem; + padding: 0.75rem; + background-color: #fef2f2; + border: 1px solid #fecaca; + border-radius: 0.5rem; + + .error-message-text { + color: #dc2626; + font-weight: 500; + } } \ No newline at end of file diff --git a/src/app/styles/payment-container.scss b/src/app/styles/payment-container.scss index 5d92c98..2aba30e 100644 --- a/src/app/styles/payment-container.scss +++ b/src/app/styles/payment-container.scss @@ -260,6 +260,11 @@ font-weight: 600; margin: 12px auto; display: inline-block; + + &.header { + font-size: 2rem; + font-weight: 800; + } } .plan-features { diff --git a/src/app/ui/payment/payment-tab-buy-tokens.tsx b/src/app/ui/payment/payment-tab-buy-tokens.tsx index 64929a8..2e64eee 100644 --- a/src/app/ui/payment/payment-tab-buy-tokens.tsx +++ b/src/app/ui/payment/payment-tab-buy-tokens.tsx @@ -1,7 +1,194 @@ +'use client' + +import { fetchTariffs, fetchTokensBundle } from '@/app/actions/action'; +import { useQuery } from '@tanstack/react-query'; +import { convertBytes } from '@/app/lib/convertBytes'; +import { useTranslations } from 'next-intl'; +import { yooKasaCreatePayment } from '@/app/actions/yooKassa'; +import { useState } from 'react'; +import ModalWindow from '@/app/components/ModalWindow'; + +interface Tariff { + id: number; + type: string; + name: string; + price: number; + tokens: number; + maxFilesCount: number; + diskSize: number; + maxUsers: number; +} + export default function PaymentTubBuyTokens() { + const t = useTranslations('Global'); + const { + data: tokensPaymentData, + isLoading, + isError, + error + } = useQuery({ + queryKey: ['tokensPaymentData'], + queryFn: () => { + return fetchTokensBundle(); + }, + select: (data: Tariff[] | null): Tariff[] => { + if (data) { + return data.sort((a, b) => a.price - b.price); + } else { + return [] + } + } + }); + + const [openWindow, setOpenWindow] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [selectedTokensBundle, setselectedTokensBundle] = useState<{ + value: number; + tokensBundle: string; + tokens: number; + tokensBundleID: number + } | null>(null); + const [isProcessing, setIsProcessing] = useState(false); + + async function createPayment(value: number, tokensBundle: number) { + if (isProcessing) return; + + setIsProcessing(true); + + try { + const response = await yooKasaCreatePayment(value, tokensBundle); + if (response) { + window.open(response, '_blank', 'noopener,noreferrer'); + closeModal(false); + } else { + setErrorMessage('payment-error'); + } + } catch (error) { + setErrorMessage('payment-error'); + } finally { + setIsProcessing(false); + } + } + + function openPaymentWindow(value: number, tokensBundle: string, tokens: number, tokensBundleID: number) { + setselectedTokensBundle({ value, tokensBundle, tokens, tokensBundleID }); + setIsProcessing(false); + setOpenWindow(true); + } + + function closeModal(isWindowOpen: boolean) { + if (!isWindowOpen) { + setOpenWindow(false); + setselectedTokensBundle(null); + setIsProcessing(false); + setErrorMessage(null); + } + + } + + function PaymentConfirmationModal() { + if (!selectedTokensBundle) return null; + + const handleConfirm = () => { + createPayment(selectedTokensBundle.value, selectedTokensBundle.tokensBundleID); + }; + + const handleCancel = () => { + closeModal(false); + }; + + return ( +
+

+ Подтверждение оплаты +

+

+ Купить: {selectedTokensBundle.tokens} токенов +

+
+
{selectedTokensBundle.value}₽
+
+
+ +
+ + +
+ { + errorMessage && ( +
+

+ {t(errorMessage)} +

+
+ ) + } +
+ ); + } + return ( -
- PaymentTubBuyTokens -
+ <> + + + +
+
+
+ + +
+
+
+ {tokensPaymentData?.map((item: { + id: number; + name: string; + price: number; + tokens: number; + maxFilesCount: number; + diskSize: number; + }) => ( +
+
+
{item.price ?? 0} ₽
+
+ {item.tokens ?? 0} +
+ токенов +
+
+
+ +
+
+ ))} +
+
+ + ) } \ No newline at end of file diff --git a/src/app/ui/payment/payment-tab-tariffs.tsx b/src/app/ui/payment/payment-tab-tariffs.tsx index dba749b..9570d37 100644 --- a/src/app/ui/payment/payment-tab-tariffs.tsx +++ b/src/app/ui/payment/payment-tab-tariffs.tsx @@ -5,13 +5,24 @@ import { useQuery } from '@tanstack/react-query'; import { convertBytes } from '@/app/lib/convertBytes'; import { useTranslations } from 'next-intl'; import { yooKasaCreatePayment } from '@/app/actions/yooKassa'; -import { useState, ReactNode } from 'react'; +import { useState, ReactNode, useEffect } from 'react'; import ModalWindow from '@/app/components/ModalWindow'; +interface Tariff { + id: number; + type: string; + name: string; + price: number; + tokens: number; + maxFilesCount: number; + diskSize: number; + maxUsers: number; +} + export default function PaymentTabTariffs() { const t = useTranslations('Global'); const { - data: userData, + data: tariffData, isLoading, isError, error @@ -19,10 +30,18 @@ export default function PaymentTabTariffs() { queryKey: ['tariffData'], queryFn: () => { return fetchTariffs(); + }, + select: (data: Tariff[] | null): Tariff[] => { + if (data) { + return data.sort((a, b) => a.price - b.price); + } else { + return [] + } } }); const [openWindow, setOpenWindow] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); const [selectedTariff, setSelectedTariff] = useState<{ value: number; tariff: string; @@ -38,12 +57,14 @@ export default function PaymentTabTariffs() { try { const response = await yooKasaCreatePayment(value, tariff); - console.log(response); if (response) { window.open(response, '_blank', 'noopener,noreferrer'); + closeModal(false); + } else { + setErrorMessage('payment-error'); } } catch (error) { - console.error('Payment error:', error); + setErrorMessage('payment-error'); } finally { setIsProcessing(false); } @@ -55,10 +76,14 @@ export default function PaymentTabTariffs() { setOpenWindow(true); } - function closeModal() { - setOpenWindow(false); - setSelectedTariff(null); - setIsProcessing(false); + function closeModal(isWindowOpen: boolean) { + if (!isWindowOpen) { + setOpenWindow(false); + setSelectedTariff(null); + setIsProcessing(false); + setErrorMessage(null); + } + } function PaymentConfirmationModal() { @@ -69,7 +94,7 @@ export default function PaymentTabTariffs() { }; const handleCancel = () => { - closeModal(); + closeModal(false); }; return ( @@ -107,13 +132,26 @@ export default function PaymentTabTariffs() { {isProcessing ? 'Обработка...' : t('yes')}
+ { + errorMessage && ( +
+

+ {t(errorMessage)} +

+
+ ) + } ); } return ( <> - + @@ -128,7 +166,7 @@ export default function PaymentTabTariffs() {
- {userData?.map((item: { + {tariffData?.map((item: { id: number; name: string; price: number; @@ -138,13 +176,12 @@ export default function PaymentTabTariffs() { }) => (
-
🚀

{t.has(item.name) ? t(item.name) : item.name}

-
{item.price}
+
{item.price ?? 0} ₽
в месяц
-
{item.tokens} токенов включено
+
{item.tokens ?? 0} токенов включено
  • diff --git a/src/app/ui/search/section-search-file.tsx b/src/app/ui/search/section-search-file.tsx index b89f212..e67f2c3 100644 --- a/src/app/ui/search/section-search-file.tsx +++ b/src/app/ui/search/section-search-file.tsx @@ -340,8 +340,8 @@ export default function SectionSearchFile({ allowedExtensions, maxFileSize }: Se {error && ( -
    -

    {error}

    +
    +

    {error}

    )} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 2ecd534..a06201f 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -227,7 +227,9 @@ "monitoring": "Monitoring", "efficiency": "Efficiency", "convert-to": "Convert to", - "do-not-convert": "Do not convert" + "do-not-convert": "Do not convert", + "payment-error": "Payment error", + "buy": "Buy" }, "Login-register-form": { "and": "and", diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index dc4e879..a7d08ae 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -227,7 +227,9 @@ "monitoring": "Мониторинг", "efficiency": "Эффективность", "convert-to": "Конвертировать в", - "do-not-convert": "Не конвертировать" + "do-not-convert": "Не конвертировать", + "payment-error": "Ошибка оплаты", + "buy": "Купить" }, "Login-register-form": { "and": "и",