init param

Добавление проверки isInitialized при инициализации приложения
Проверку наличия данных пользователя в initDataUnsafe
Вызов webApp.ready() после успешной инициализации
Улучшенную обработку ошибок и состояний загрузки
This commit is contained in:
degradin 2025-03-16 12:39:38 +03:00
parent 8af9c21cb1
commit 0e304a8b2e
4 changed files with 197 additions and 72 deletions

View File

@ -17,13 +17,13 @@ export default function MainApp() {
const [user, setUser] = useState<SafeUser | null>(null);
const [shopItems, setShopItems] = useState<IShopItem[]>([]);
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 (
<Center h="100vh">
<Spinner size="xl" />

View File

@ -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<any>(null);
const [webApp, setWebApp] = useState<WebApp | null>(null);
const [error, setError] = useState<string | null>(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 };
}

View File

@ -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;
};

View File

@ -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: () => {}
};
};
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: () => {},
});