mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-07-02 08:37:23 +00:00
feat(telegram): add Telegram Mini App for Pezkuwichain
- Add @twa-dev/sdk dependency for Telegram WebApp integration - Create useTelegram hook for Telegram SDK integration (haptics, popups, etc.) - Create usePezkuwiApi hook for blockchain API connection - Add Discord-like Sidebar with 5 sections navigation - Add Announcements section with like/dislike reactions - Add Forum section with thread creation and replies - Add Rewards section with referral program and epoch claims - Add APK section for Pezwallet download with changelog - Add Wallet section with balance, staking info, and transactions - Create main TelegramApp component with routing - Add /telegram route to App.tsx UI Structure: - Left: Discord-style icon sidebar (Announcements, Forum, Rewards, APK, Wallet) - Right: Active section content area - Mobile-first responsive design with Telegram theme integration Integrations: - Uses existing shared/lib functions for referral, staking, scores - Supports Telegram startParam for referral codes - Haptic feedback for native Telegram experience - Telegram Main/Back button integration
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export { useTelegram } from './useTelegram';
|
||||
export type { TelegramUser, TelegramTheme } from './useTelegram';
|
||||
|
||||
export { usePezkuwiApi, getApiInstance } from './usePezkuwiApi';
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
|
||||
// RPC endpoint - uses environment variable or falls back to mainnet
|
||||
const RPC_ENDPOINT = import.meta.env.VITE_WS_ENDPOINT || 'wss://rpc.pezkuwichain.io:9944';
|
||||
const FALLBACK_ENDPOINTS = [
|
||||
RPC_ENDPOINT,
|
||||
import.meta.env.VITE_WS_ENDPOINT_FALLBACK_1,
|
||||
import.meta.env.VITE_WS_ENDPOINT_FALLBACK_2,
|
||||
].filter(Boolean) as string[];
|
||||
|
||||
interface UsePezkuwiApiReturn {
|
||||
api: ApiPromise | null;
|
||||
isReady: boolean;
|
||||
isConnecting: boolean;
|
||||
error: string | null;
|
||||
reconnect: () => Promise<void>;
|
||||
}
|
||||
|
||||
// Singleton API instance to avoid multiple connections
|
||||
let globalApi: ApiPromise | null = null;
|
||||
let connectionPromise: Promise<ApiPromise> | null = null;
|
||||
|
||||
async function createApiConnection(): Promise<ApiPromise> {
|
||||
// Return existing connection promise if one is in progress
|
||||
if (connectionPromise) {
|
||||
return connectionPromise;
|
||||
}
|
||||
|
||||
// Return existing API if already connected
|
||||
if (globalApi && globalApi.isConnected) {
|
||||
return globalApi;
|
||||
}
|
||||
|
||||
// Create new connection
|
||||
connectionPromise = (async () => {
|
||||
for (const endpoint of FALLBACK_ENDPOINTS) {
|
||||
try {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[PezkuwiApi] Connecting to:', endpoint);
|
||||
}
|
||||
|
||||
const provider = new WsProvider(endpoint);
|
||||
const api = await ApiPromise.create({ provider });
|
||||
await api.isReady;
|
||||
|
||||
globalApi = api;
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const [chain, nodeName, nodeVersion] = await Promise.all([
|
||||
api.rpc.system.chain(),
|
||||
api.rpc.system.name(),
|
||||
api.rpc.system.version(),
|
||||
]);
|
||||
console.log(`[PezkuwiApi] Connected to ${chain} (${nodeName} v${nodeVersion})`);
|
||||
}
|
||||
|
||||
return api;
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(`[PezkuwiApi] Failed to connect to ${endpoint}:`, err);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Failed to connect to any endpoint');
|
||||
})();
|
||||
|
||||
try {
|
||||
return await connectionPromise;
|
||||
} finally {
|
||||
connectionPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function usePezkuwiApi(): UsePezkuwiApiReturn {
|
||||
const [api, setApi] = useState<ApiPromise | null>(globalApi);
|
||||
const [isReady, setIsReady] = useState(globalApi?.isConnected || false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const connect = async () => {
|
||||
if (isConnecting) return;
|
||||
|
||||
setIsConnecting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const apiInstance = await createApiConnection();
|
||||
setApi(apiInstance);
|
||||
setIsReady(true);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Connection failed';
|
||||
setError(errorMessage);
|
||||
setIsReady(false);
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const reconnect = async () => {
|
||||
// Disconnect existing connection
|
||||
if (globalApi) {
|
||||
await globalApi.disconnect();
|
||||
globalApi = null;
|
||||
}
|
||||
await connect();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// If we already have a global API, use it
|
||||
if (globalApi && globalApi.isConnected) {
|
||||
setApi(globalApi);
|
||||
setIsReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, establish connection
|
||||
connect();
|
||||
|
||||
// Cleanup on unmount - don't disconnect global API, just clean up local state
|
||||
return () => {
|
||||
// Note: We don't disconnect globalApi here to maintain connection across components
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Handle disconnection events
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
|
||||
const handleDisconnected = () => {
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[PezkuwiApi] Disconnected, attempting to reconnect...');
|
||||
}
|
||||
setIsReady(false);
|
||||
reconnect();
|
||||
};
|
||||
|
||||
api.on('disconnected', handleDisconnected);
|
||||
|
||||
return () => {
|
||||
api.off('disconnected', handleDisconnected);
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
return {
|
||||
api,
|
||||
isReady,
|
||||
isConnecting,
|
||||
error,
|
||||
reconnect,
|
||||
};
|
||||
}
|
||||
|
||||
// Export helper to get the global API instance (for non-hook usage)
|
||||
export function getApiInstance(): ApiPromise | null {
|
||||
return globalApi;
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import WebApp from '@twa-dev/sdk';
|
||||
|
||||
export interface TelegramUser {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name?: string;
|
||||
username?: string;
|
||||
language_code?: string;
|
||||
is_premium?: boolean;
|
||||
photo_url?: string;
|
||||
}
|
||||
|
||||
export interface TelegramTheme {
|
||||
bg_color: string;
|
||||
text_color: string;
|
||||
hint_color: string;
|
||||
link_color: string;
|
||||
button_color: string;
|
||||
button_text_color: string;
|
||||
secondary_bg_color: string;
|
||||
}
|
||||
|
||||
interface UseTelegramReturn {
|
||||
// State
|
||||
isReady: boolean;
|
||||
isTelegram: boolean;
|
||||
user: TelegramUser | null;
|
||||
startParam: string | null;
|
||||
theme: TelegramTheme | null;
|
||||
colorScheme: 'light' | 'dark';
|
||||
viewportHeight: number;
|
||||
viewportStableHeight: number;
|
||||
isExpanded: boolean;
|
||||
|
||||
// Actions
|
||||
ready: () => void;
|
||||
expand: () => void;
|
||||
close: () => void;
|
||||
showAlert: (message: string) => void;
|
||||
showConfirm: (message: string) => Promise<boolean>;
|
||||
showPopup: (params: { title?: string; message: string; buttons?: Array<{ id: string; type?: string; text: string }> }) => Promise<string>;
|
||||
openLink: (url: string, options?: { try_instant_view?: boolean }) => void;
|
||||
openTelegramLink: (url: string) => void;
|
||||
sendData: (data: string) => void;
|
||||
enableClosingConfirmation: () => void;
|
||||
disableClosingConfirmation: () => void;
|
||||
setHeaderColor: (color: string) => void;
|
||||
setBackgroundColor: (color: string) => void;
|
||||
|
||||
// Main Button
|
||||
showMainButton: (text: string, onClick: () => void) => void;
|
||||
hideMainButton: () => void;
|
||||
setMainButtonLoading: (loading: boolean) => void;
|
||||
|
||||
// Back Button
|
||||
showBackButton: (onClick: () => void) => void;
|
||||
hideBackButton: () => void;
|
||||
|
||||
// Haptic Feedback
|
||||
hapticImpact: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => void;
|
||||
hapticNotification: (type: 'error' | 'success' | 'warning') => void;
|
||||
hapticSelection: () => void;
|
||||
}
|
||||
|
||||
export function useTelegram(): UseTelegramReturn {
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
const [isTelegram, setIsTelegram] = useState(false);
|
||||
const [user, setUser] = useState<TelegramUser | null>(null);
|
||||
const [startParam, setStartParam] = useState<string | null>(null);
|
||||
const [theme, setTheme] = useState<TelegramTheme | null>(null);
|
||||
const [colorScheme, setColorScheme] = useState<'light' | 'dark'>('dark');
|
||||
const [viewportHeight, setViewportHeight] = useState(window.innerHeight);
|
||||
const [viewportStableHeight, setViewportStableHeight] = useState(window.innerHeight);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Initialize Telegram WebApp
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Check if running in Telegram WebApp environment
|
||||
const tg = WebApp;
|
||||
|
||||
if (tg && tg.initData) {
|
||||
setIsTelegram(true);
|
||||
|
||||
// Get user info
|
||||
if (tg.initDataUnsafe?.user) {
|
||||
setUser(tg.initDataUnsafe.user as TelegramUser);
|
||||
}
|
||||
|
||||
// Get start parameter (referral code, etc.)
|
||||
if (tg.initDataUnsafe?.start_param) {
|
||||
setStartParam(tg.initDataUnsafe.start_param);
|
||||
}
|
||||
|
||||
// Get theme
|
||||
if (tg.themeParams) {
|
||||
setTheme(tg.themeParams as TelegramTheme);
|
||||
}
|
||||
|
||||
// Get color scheme
|
||||
setColorScheme(tg.colorScheme as 'light' | 'dark' || 'dark');
|
||||
|
||||
// Get viewport
|
||||
setViewportHeight(tg.viewportHeight || window.innerHeight);
|
||||
setViewportStableHeight(tg.viewportStableHeight || window.innerHeight);
|
||||
setIsExpanded(tg.isExpanded || false);
|
||||
|
||||
// Listen for viewport changes
|
||||
tg.onEvent('viewportChanged', (event: { isStateStable: boolean }) => {
|
||||
setViewportHeight(tg.viewportHeight);
|
||||
if (event.isStateStable) {
|
||||
setViewportStableHeight(tg.viewportStableHeight);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for theme changes
|
||||
tg.onEvent('themeChanged', () => {
|
||||
setTheme(tg.themeParams as TelegramTheme);
|
||||
setColorScheme(tg.colorScheme as 'light' | 'dark' || 'dark');
|
||||
});
|
||||
|
||||
// Signal that app is ready
|
||||
tg.ready();
|
||||
setIsReady(true);
|
||||
|
||||
// Expand by default for better UX
|
||||
tg.expand();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[Telegram] Mini App initialized');
|
||||
console.log('[Telegram] User:', tg.initDataUnsafe?.user);
|
||||
console.log('[Telegram] Start param:', tg.initDataUnsafe?.start_param);
|
||||
}
|
||||
} else {
|
||||
// Not running in Telegram, but still mark as ready
|
||||
setIsReady(true);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[Telegram] Not running in Telegram WebApp environment');
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Telegram] Initialization error:', err);
|
||||
setIsReady(true); // Mark as ready even on error for graceful fallback
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Actions
|
||||
const ready = useCallback(() => {
|
||||
if (isTelegram) WebApp.ready();
|
||||
}, [isTelegram]);
|
||||
|
||||
const expand = useCallback(() => {
|
||||
if (isTelegram) {
|
||||
WebApp.expand();
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (isTelegram) WebApp.close();
|
||||
}, [isTelegram]);
|
||||
|
||||
const showAlert = useCallback((message: string) => {
|
||||
if (isTelegram) {
|
||||
WebApp.showAlert(message);
|
||||
} else {
|
||||
alert(message);
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
const showConfirm = useCallback((message: string): Promise<boolean> => {
|
||||
return new Promise((resolve) => {
|
||||
if (isTelegram) {
|
||||
WebApp.showConfirm(message, (confirmed) => {
|
||||
resolve(confirmed);
|
||||
});
|
||||
} else {
|
||||
resolve(confirm(message));
|
||||
}
|
||||
});
|
||||
}, [isTelegram]);
|
||||
|
||||
const showPopup = useCallback((params: { title?: string; message: string; buttons?: Array<{ id: string; type?: string; text: string }> }): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
if (isTelegram) {
|
||||
WebApp.showPopup(params, (buttonId) => {
|
||||
resolve(buttonId || '');
|
||||
});
|
||||
} else {
|
||||
// Fallback for non-Telegram environment
|
||||
const result = confirm(params.message);
|
||||
resolve(result ? 'ok' : 'cancel');
|
||||
}
|
||||
});
|
||||
}, [isTelegram]);
|
||||
|
||||
const openLink = useCallback((url: string, options?: { try_instant_view?: boolean }) => {
|
||||
if (isTelegram) {
|
||||
WebApp.openLink(url, options);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
const openTelegramLink = useCallback((url: string) => {
|
||||
if (isTelegram) {
|
||||
WebApp.openTelegramLink(url);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
const sendData = useCallback((data: string) => {
|
||||
if (isTelegram) WebApp.sendData(data);
|
||||
}, [isTelegram]);
|
||||
|
||||
const enableClosingConfirmation = useCallback(() => {
|
||||
if (isTelegram) WebApp.enableClosingConfirmation();
|
||||
}, [isTelegram]);
|
||||
|
||||
const disableClosingConfirmation = useCallback(() => {
|
||||
if (isTelegram) WebApp.disableClosingConfirmation();
|
||||
}, [isTelegram]);
|
||||
|
||||
const setHeaderColor = useCallback((color: string) => {
|
||||
if (isTelegram) WebApp.setHeaderColor(color as `#${string}`);
|
||||
}, [isTelegram]);
|
||||
|
||||
const setBackgroundColor = useCallback((color: string) => {
|
||||
if (isTelegram) WebApp.setBackgroundColor(color as `#${string}`);
|
||||
}, [isTelegram]);
|
||||
|
||||
// Main Button
|
||||
const showMainButton = useCallback((text: string, onClick: () => void) => {
|
||||
if (isTelegram) {
|
||||
WebApp.MainButton.setText(text);
|
||||
WebApp.MainButton.onClick(onClick);
|
||||
WebApp.MainButton.show();
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
const hideMainButton = useCallback(() => {
|
||||
if (isTelegram) WebApp.MainButton.hide();
|
||||
}, [isTelegram]);
|
||||
|
||||
const setMainButtonLoading = useCallback((loading: boolean) => {
|
||||
if (isTelegram) {
|
||||
if (loading) {
|
||||
WebApp.MainButton.showProgress();
|
||||
} else {
|
||||
WebApp.MainButton.hideProgress();
|
||||
}
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
// Back Button
|
||||
const showBackButton = useCallback((onClick: () => void) => {
|
||||
if (isTelegram) {
|
||||
WebApp.BackButton.onClick(onClick);
|
||||
WebApp.BackButton.show();
|
||||
}
|
||||
}, [isTelegram]);
|
||||
|
||||
const hideBackButton = useCallback(() => {
|
||||
if (isTelegram) WebApp.BackButton.hide();
|
||||
}, [isTelegram]);
|
||||
|
||||
// Haptic Feedback
|
||||
const hapticImpact = useCallback((style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => {
|
||||
if (isTelegram) WebApp.HapticFeedback.impactOccurred(style);
|
||||
}, [isTelegram]);
|
||||
|
||||
const hapticNotification = useCallback((type: 'error' | 'success' | 'warning') => {
|
||||
if (isTelegram) WebApp.HapticFeedback.notificationOccurred(type);
|
||||
}, [isTelegram]);
|
||||
|
||||
const hapticSelection = useCallback(() => {
|
||||
if (isTelegram) WebApp.HapticFeedback.selectionChanged();
|
||||
}, [isTelegram]);
|
||||
|
||||
return {
|
||||
isReady,
|
||||
isTelegram,
|
||||
user,
|
||||
startParam,
|
||||
theme,
|
||||
colorScheme,
|
||||
viewportHeight,
|
||||
viewportStableHeight,
|
||||
isExpanded,
|
||||
ready,
|
||||
expand,
|
||||
close,
|
||||
showAlert,
|
||||
showConfirm,
|
||||
showPopup,
|
||||
openLink,
|
||||
openTelegramLink,
|
||||
sendData,
|
||||
enableClosingConfirmation,
|
||||
disableClosingConfirmation,
|
||||
setHeaderColor,
|
||||
setBackgroundColor,
|
||||
showMainButton,
|
||||
hideMainButton,
|
||||
setMainButtonLoading,
|
||||
showBackButton,
|
||||
hideBackButton,
|
||||
hapticImpact,
|
||||
hapticNotification,
|
||||
hapticSelection,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user