feat: интеграция платежей через Telegram Stars API

- обавлена интеграция с эндпоинтом /create-invoice-link для создания инвойсов
- бновлен paymentService для работы с новым API платежей
- обавлено получение информации о пользователе через /users/{user_id}
- бновлен интерфейс профиля для отображения актуальных данных после оплаты
This commit is contained in:
kazachilo 2025-03-27 14:51:00 +03:00
parent c3e6a6b05d
commit 92424f6902
4 changed files with 116 additions and 45 deletions

View File

@ -38,9 +38,20 @@ const Profile: React.FC = () => {
const pack = tokenPacks.find(p => p.id === packId);
if (!pack) return;
paymentService.showBuyTokensPopup(pack, () => {
// Обновляем данные после успешной оплаты
paymentService.showBuyTokensPopup(pack, (userData) => {
if (userData) {
// Обновляем данные на основе полученной информации
if (userData.stickers_count !== undefined) {
setStickersCount(userData.stickers_count);
}
if (userData.packs_count !== undefined) {
setPacksCount(userData.packs_count);
}
// Можно также обновить другие данные, если они есть в ответе
} else {
// Если данные не получены, просто перезагружаем страницу
window.location.reload();
}
});
};

View File

@ -63,26 +63,11 @@ export const trackButtonClick = (buttonName: string): void => {
* @param prompt - Промпт для генерации
*/
export const trackStickerGeneration = (prompt: string): void => {
// Создаем объект события с дополнительными данными
const eventData: {
event: string;
category: string;
value_num: number;
prompt?: string;
} = {
event: 'Генерация стикера',
category: 'generation',
value_num: 1
};
// Добавляем промпт в данные события, если он предоставлен
if (prompt) {
eventData.prompt = prompt;
}
// Отправляем событие напрямую, чтобы включить дополнительные данные
if (typeof window !== 'undefined' && window.graspil) {
window.graspil.push(eventData);
window.graspil.push({
event: prompt, // Сам промпт как название события
category: 'generation' // Категория для фильтрации
});
}
};
@ -103,3 +88,16 @@ export const trackStickerPackCreation = (packName: string): void => {
export const trackTokenPurchase = (amount: number, price: number, currency: string): void => {
trackEvent('Покупка токенов', 'purchase', price, currency);
};
/**
* Отслеживает неудавшуюся генерацию стикера из-за непрошедшего фильтр промпта
* @param originalPrompt - Оригинальный промпт пользователя
*/
export const trackRejectedPrompt = (originalPrompt: string): void => {
if (typeof window !== 'undefined' && window.graspil) {
window.graspil.push({
event: `Не принятый промпт: ${originalPrompt}`, // Оригинальный промпт как название события
category: 'rejected_generation' // Категория для фильтрации
});
}
};

View File

@ -3,7 +3,7 @@ import { baseWorkflow } from '../constants/baseWorkflow';
import { prompts } from '../assets/prompts';
import translateService from './translateService';
import { getCurrentUserId, isTelegramWebAppAvailable } from '../constants/user';
import { trackStickerGeneration } from './analyticsService';
import { trackStickerGeneration, trackRejectedPrompt } from './analyticsService';
const API_BASE_URL = 'https://stickerserver.gymnasticstuff.uk';
@ -50,32 +50,62 @@ class GenerationError extends Error {
}
// Временное решение для работы с балансом токенов
// В будущем будет заменено на API-запросы
// Используется только если не удалось получить баланс с сервера
let mockBalance = 50; // Начальное значение из MOCK_USER
const apiService = {
// Метод для списания токенов
async deductTokens(userId: number, amount: number): Promise<boolean> {
// Метод для получения информации о пользователе
async getUserInfo(userId: number = getCurrentUserId()): Promise<any> {
try {
// В будущем здесь будет API-запрос для списания токенов
// Пока реализуем локальное списание
mockBalance -= amount;
return true;
const response = await fetch(`${API_BASE_URL}/users/${userId}`, {
method: 'GET',
headers: {
'accept': 'application/json',
}
});
if (!response.ok) {
throw new Error('Failed to fetch user info');
}
return await response.json();
} catch (error) {
console.error('Error deducting tokens:', error);
return false;
console.error('Error fetching user info:', error);
throw error;
}
},
// Метод для создания ссылки на инвойс
async createInvoiceLink(userId: number, starsAmount: number, tokens: number): Promise<string> {
try {
const response = await fetch(`${API_BASE_URL}/create-invoice-link?user_id=${userId}&stars_amount=${starsAmount}&tokens=${tokens}`, {
method: 'POST',
headers: {
'accept': 'application/json',
}
});
if (!response.ok) {
throw new Error('Failed to create invoice link');
}
// Предполагаем, что бэкенд возвращает строку с URL инвойса
return await response.text();
} catch (error) {
console.error('Error creating invoice link:', error);
throw error;
}
},
// Метод для получения текущего баланса
async getBalance(userId: number): Promise<number> {
async getBalance(userId: number = getCurrentUserId()): Promise<number> {
try {
// В будущем здесь будет API-запрос для получения баланса
// Пока возвращаем локальное значение
return mockBalance;
const userInfo = await this.getUserInfo(userId);
return userInfo.balance || 0;
} catch (error) {
console.error('Error getting balance:', error);
throw error;
// В случае ошибки возвращаем mockBalance для обратной совместимости
return mockBalance;
}
},
@ -218,6 +248,9 @@ const apiService = {
console.error('Не удалось перевести промпт:', translationResult.text);
translationFailed = true;
// Отслеживаем событие неудавшейся генерации
trackRejectedPrompt(userPrompt);
// Не продолжаем генерацию, возвращаем ошибку с сообщением
return {
translationFailed: true,

View File

@ -1,7 +1,9 @@
import { TokenPack } from '../constants/tokenPacks';
import apiService from '../services/api';
import { getCurrentUserId } from '../constants/user';
export const paymentService = {
showBuyTokensPopup: (pack: TokenPack, onSuccess?: () => void) => {
showBuyTokensPopup: async (pack: TokenPack, onSuccess?: (userData?: any) => void) => {
// Проверяем наличие Telegram WebApp
if (!window.Telegram?.WebApp) {
console.error('Telegram WebApp не доступен');
@ -9,6 +11,7 @@ export const paymentService = {
}
const webApp = window.Telegram.WebApp;
const userId = getCurrentUserId();
// Открываем окно оплаты Telegram
webApp.showPopup({
@ -25,14 +28,40 @@ export const paymentService = {
text: 'Отмена'
}
]
}, (buttonId: string) => {
}, async (buttonId: string) => {
if (buttonId === 'buy') {
try {
// Получаем ссылку на инвойс от бэкенда
const invoiceLink = await apiService.createInvoiceLink(
userId,
pack.price,
pack.tokens + pack.bonusTokens
);
// Открываем встроенный платеж Telegram
webApp.openInvoice(`sticker_tokens_${pack.id}`, (status: 'paid' | 'cancelled' | 'failed' | 'pending') => {
if (status === 'paid' && onSuccess) {
webApp.openInvoice(invoiceLink, async (status: 'paid' | 'cancelled' | 'failed' | 'pending') => {
if (status === 'paid') {
try {
// Получаем обновленную информацию о пользователе
const userData = await apiService.getUserInfo(userId);
if (onSuccess) {
onSuccess(userData);
}
} catch (error) {
console.error('Ошибка при получении данных пользователя:', error);
// Даже если не удалось получить данные, вызываем onSuccess
if (onSuccess) {
onSuccess();
}
}
}
});
} catch (error) {
console.error('Ошибка при создании инвойса:', error);
webApp.showAlert('Произошла ошибка при создании платежа. Пожалуйста, попробуйте позже.');
}
}
});
}