diff --git a/app/components/MainApp.tsx b/app/components/MainApp.tsx index 13e9d29..970bf01 100644 --- a/app/components/MainApp.tsx +++ b/app/components/MainApp.tsx @@ -17,13 +17,13 @@ export default function MainApp() { const [user, setUser] = useState(null); const [shopItems, setShopItems] = useState([]); const toast = useToast(); - const { webApp, error: webAppError } = useTelegramWebApp(); + const { webApp, error: webAppError, isInitialized } = useTelegramWebApp(); useEffect(() => { - if (webApp) { + if (webApp && isInitialized) { initApp(); } - }, [webApp]); + }, [webApp, isInitialized]); const initApp = async () => { try { @@ -35,6 +35,10 @@ export default function MainApp() { // Авторизация пользователя const { user: telegramUser } = webApp.initDataUnsafe; + if (!telegramUser) { + throw new Error('Пользователь не найден'); + } + const authResponse = await auth(telegramUser.id.toString(), telegramUser.username || 'anonymous'); // Получение данных пользователя и магазина @@ -45,6 +49,9 @@ export default function MainApp() { setUser(profileData as SafeUser); setShopItems(shopData); + + // Сообщаем Telegram, что приложение готово + webApp.ready(); } catch (error: any) { console.error('Initialization error:', error); toast({ @@ -69,7 +76,7 @@ export default function MainApp() { ); } - if (isLoading || !webApp) { + if (isLoading || !webApp || !isInitialized) { return (
diff --git a/app/hooks/useTelegramWebApp.ts b/app/hooks/useTelegramWebApp.ts index 5da1dbb..b237ecc 100644 --- a/app/hooks/useTelegramWebApp.ts +++ b/app/hooks/useTelegramWebApp.ts @@ -3,27 +3,73 @@ import { useEffect, useState } from 'react'; import { isDemoMode, getDemoWebApp } from '../utils/demo'; +export type WebApp = { + initData: string; + initDataUnsafe: { + query_id: string; + user: { + id: string; + first_name: string; + username?: string; + language_code: string; + }; + auth_date: number; + hash: string; + }; + platform: string; + colorScheme: string; + themeParams: { + bg_color: string; + text_color: string; + hint_color: string; + link_color: string; + button_color: string; + button_text_color: string; + }; + isExpanded: boolean; + viewportHeight: number; + viewportStableHeight: number; + headerColor: string; + backgroundColor: string; + ready: () => void; + expand: () => void; + close: () => void; +}; + export function useTelegramWebApp() { - const [webApp, setWebApp] = useState(null); + const [webApp, setWebApp] = useState(null); const [error, setError] = useState(null); + const [isInitialized, setIsInitialized] = useState(false); useEffect(() => { const initWebApp = async () => { try { if (isDemoMode()) { - setWebApp(getDemoWebApp()); - } else if (typeof window !== 'undefined') { + const demoWebApp = getDemoWebApp(); + setWebApp(demoWebApp); + setIsInitialized(true); + return; + } + + if (typeof window !== 'undefined') { const WebAppModule = await import('@twa-dev/sdk'); - setWebApp(WebAppModule.default); + if (WebAppModule.default) { + setWebApp(WebAppModule.default); + setIsInitialized(true); + } else { + throw new Error('WebApp не найден'); + } } } catch (err) { - setError('Ошибка инициализации Telegram Web App'); console.error('WebApp initialization error:', err); + setError('Ошибка инициализации Telegram Web App'); } }; - initWebApp(); - }, []); + if (!isInitialized) { + initWebApp(); + } + }, [isInitialized]); - return { webApp, error }; + return { webApp, error, isInitialized }; } \ No newline at end of file diff --git a/app/utils/api.ts b/app/utils/api.ts index d2db4f9..308a18a 100644 --- a/app/utils/api.ts +++ b/app/utils/api.ts @@ -1,73 +1,120 @@ import axios from 'axios'; +import { isDemoMode } from './demo'; + +const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api'; const api = axios.create({ - baseURL: process.env.NEXT_PUBLIC_API_URL, - withCredentials: true, + baseURL: API_URL, headers: { 'Content-Type': 'application/json', }, - timeout: 10000, // 10 секунд таймаут }); -// Интерцептор для добавления токена к запросам +// Демо-данные +const demoData = { + user: { + _id: 'demo_user_id', + telegramId: '12345', + username: 'demo_user', + level: 1, + experience: 0, + balance: 1000, + achievements: [], + }, + shopItems: [ + { + _id: 'demo_item_1', + name: 'Демо предмет 1', + description: 'Описание демо предмета 1', + price: 100, + type: 'consumable', + }, + { + _id: 'demo_item_2', + name: 'Демо предмет 2', + description: 'Описание демо предмета 2', + price: 200, + type: 'permanent', + }, + ], +}; + +// Интерцептор для добавления токена api.interceptors.request.use((config) => { - if (typeof window !== 'undefined') { - const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; } return config; -}, (error) => { - return Promise.reject(error); }); // Интерцептор для обработки ошибок api.interceptors.response.use( (response) => response, (error) => { - if (error.response) { - // Ошибка от сервера - return Promise.reject(error.response.data); - } else if (error.request) { - // Ошибка сети - return Promise.reject({ error: 'Ошибка сети. Проверьте подключение к интернету.' }); - } else { - // Другие ошибки - return Promise.reject({ error: 'Произошла ошибка при выполнении запроса.' }); + if (error.response?.status === 401) { + localStorage.removeItem('token'); } + return Promise.reject(error); } ); export const auth = async (telegramId: string, username: string) => { - try { - const response = await api.post('/auth', { telegramId, username }); - if (typeof window !== 'undefined') { - localStorage.setItem('token', response.data.token); - } - return response.data; - } catch (error) { - console.error('Auth error:', error); - throw error; + if (isDemoMode()) { + localStorage.setItem('token', 'demo_token'); + return { token: 'demo_token', user: demoData.user }; } + + const response = await api.post('/auth', { telegramId, username }); + const { token } = response.data; + localStorage.setItem('token', token); + return response.data; }; export const getProfile = async () => { + if (isDemoMode()) { + return demoData.user; + } + const response = await api.get('/profile'); return response.data; }; export const getShopItems = async () => { + if (isDemoMode()) { + return demoData.shopItems; + } + const response = await api.get('/shop'); return response.data; }; export const purchaseItem = async (itemId: string) => { - const response = await api.post('/shop/purchase', { itemId }); + if (isDemoMode()) { + const item = demoData.shopItems.find(item => item._id === itemId); + if (!item) { + throw new Error('Предмет не найден'); + } + if (demoData.user.balance < item.price) { + throw new Error('Недостаточно средств'); + } + demoData.user.balance -= item.price; + return { user: demoData.user }; + } + + const response = await api.post(`/shop/purchase/${itemId}`); return response.data; }; -export const transferBalance = async (recipientUsername: string, amount: number) => { - const response = await api.post('/transfer', { recipientUsername, amount }); +export const transferBalance = async (username: string, amount: number) => { + if (isDemoMode()) { + if (demoData.user.balance < amount) { + throw new Error('Недостаточно средств'); + } + demoData.user.balance -= amount; + return { balance: demoData.user.balance }; + } + + const response = await api.post('/transfer', { username, amount }); return response.data; }; \ No newline at end of file diff --git a/app/utils/demo.ts b/app/utils/demo.ts index 4030711..65951bf 100644 --- a/app/utils/demo.ts +++ b/app/utils/demo.ts @@ -1,39 +1,64 @@ +'use client'; + // Демо данные для тестирования без Telegram const demoUser = { - id: 'demo_user', + id: 'demo_user_123', first_name: 'Demo', username: 'demo_user', language_code: 'ru' }; +const demoInitData = { + query_id: 'demo_query', + user: demoUser, + auth_date: Date.now(), + hash: 'demo_hash' +}; + export const isDemoMode = () => { return typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('demo'); }; -export const getDemoWebApp = () => { - return { - initData: 'demo_mode', - initDataUnsafe: { - user: demoUser, - start_param: 'demo' - }, - platform: 'demo', - colorScheme: 'light', - themeParams: { - bg_color: '#ffffff', - text_color: '#000000', - hint_color: '#999999', - link_color: '#2481cc', - button_color: '#2481cc', - button_text_color: '#ffffff' - }, - isExpanded: true, - viewportHeight: window.innerHeight, - viewportStableHeight: window.innerHeight, - headerColor: '#ffffff', - backgroundColor: '#ffffff', - ready: () => {}, - expand: () => {}, - close: () => {} - }; -}; \ No newline at end of file +export const getDemoWebApp = () => ({ + initData: 'demo_mode', + initDataUnsafe: demoInitData, + platform: 'demo', + colorScheme: 'light', + themeParams: { + bg_color: '#ffffff', + text_color: '#000000', + hint_color: '#999999', + link_color: '#2481cc', + button_color: '#2481cc', + button_text_color: '#ffffff' + }, + isExpanded: true, + viewportHeight: typeof window !== 'undefined' ? window.innerHeight : 800, + viewportStableHeight: typeof window !== 'undefined' ? window.innerHeight : 800, + headerColor: '#ffffff', + backgroundColor: '#ffffff', + isClosingConfirmationEnabled: true, + BackButton: { + isVisible: false, + onClick: () => {}, + }, + MainButton: { + text: '', + color: '#2481cc', + textColor: '#ffffff', + isVisible: false, + isProgressVisible: false, + isActive: true, + setText: () => {}, + onClick: () => {}, + show: () => {}, + hide: () => {}, + enable: () => {}, + disable: () => {}, + showProgress: () => {}, + hideProgress: () => {}, + }, + ready: () => {}, + expand: () => {}, + close: () => {}, +}); \ No newline at end of file