mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Merge pull request #143 from BEDOLAGA-DEV/fix/popup-double-open-guard
Fix/popup double open guard
This commit is contained in:
25
package-lock.json
generated
25
package-lock.json
generated
@@ -24,7 +24,6 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.4",
|
"@radix-ui/react-visually-hidden": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.8.0",
|
"@tanstack/react-query": "^5.8.0",
|
||||||
"@telegram-apps/react-router-integration": "^1.0.1",
|
|
||||||
"@telegram-apps/sdk-react": "^3.3.9",
|
"@telegram-apps/sdk-react": "^3.3.9",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -2640,30 +2639,6 @@
|
|||||||
"integrity": "sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==",
|
"integrity": "sha512-bqNgF/J8Po7ZtsELm3E1a6aPr7awwxO3sIqD8J6u12urOlGoW5+1KxKKbkPa58mgXuQdsltd8apI+OVy0IYiUA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@telegram-apps/react-router-integration": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@telegram-apps/react-router-integration/-/react-router-integration-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-q5fC0ihXyUDqrSdLQ3u3gypmjI/rMU2mTHeAz9IU1F1bl5X+J17LxWmgLp+OlD0Y97bq2XYmc+UbKrYh+eN3Wg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"@telegram-apps/sdk": "^1",
|
|
||||||
"@types/react": "^17.0.0 || ^18.0.0",
|
|
||||||
"react": "^17.0.0 || ^18.0.0",
|
|
||||||
"react-router-dom": "^6.22.3"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@telegram-apps/sdk": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@telegram-apps/sdk/-/sdk-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-gvcXNttbCouDNTxjDUbO5FS/BdaHYO0ibSddQxiYitShPIDZDxGon0Eh3OSsgUIVPSQSNdXqkXaeqYgA+jeY0A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/@telegram-apps/sdk-react": {
|
"node_modules/@telegram-apps/sdk-react": {
|
||||||
"version": "3.3.9",
|
"version": "3.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@telegram-apps/sdk-react/-/sdk-react-3.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@telegram-apps/sdk-react/-/sdk-react-3.3.9.tgz",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.4",
|
"@radix-ui/react-visually-hidden": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.8.0",
|
"@tanstack/react-query": "^5.8.0",
|
||||||
"@telegram-apps/react-router-integration": "^1.0.1",
|
|
||||||
"@telegram-apps/sdk-react": "^3.3.9",
|
"@telegram-apps/sdk-react": "^3.3.9",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -1,85 +1,71 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
import { BrowserRouter, Router } from 'react-router-dom';
|
import { BrowserRouter, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { useIntegration } from '@telegram-apps/react-router-integration';
|
import {
|
||||||
import { initNavigator } from '@telegram-apps/sdk';
|
showBackButton,
|
||||||
|
hideBackButton,
|
||||||
|
onBackButtonClick,
|
||||||
|
offBackButtonClick,
|
||||||
|
} from '@telegram-apps/sdk-react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import { PlatformProvider } from './platform/PlatformProvider';
|
import { PlatformProvider } from './platform/PlatformProvider';
|
||||||
import { ThemeColorsProvider } from './providers/ThemeColorsProvider';
|
import { ThemeColorsProvider } from './providers/ThemeColorsProvider';
|
||||||
import { WebSocketProvider } from './providers/WebSocketProvider';
|
import { WebSocketProvider } from './providers/WebSocketProvider';
|
||||||
import { ToastProvider } from './components/Toast';
|
import { ToastProvider } from './components/Toast';
|
||||||
import { TooltipProvider } from './components/primitives/Tooltip';
|
import { TooltipProvider } from './components/primitives/Tooltip';
|
||||||
|
import { isInTelegramWebApp } from './hooks/useTelegramSDK';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if running inside Telegram Mini App
|
* Manages Telegram BackButton visibility based on navigation location.
|
||||||
* Uses multiple checks to reliably detect Telegram environment
|
* Shows back button on non-root routes, hides on root.
|
||||||
*/
|
*/
|
||||||
function isTelegramMiniApp(): boolean {
|
function TelegramBackButton() {
|
||||||
if (typeof window === 'undefined') return false;
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const navigateRef = useRef(navigate);
|
||||||
|
navigateRef.current = navigate;
|
||||||
|
|
||||||
const webApp = window.Telegram?.WebApp;
|
useEffect(() => {
|
||||||
if (!webApp) return false;
|
const isRoot = location.pathname === '/' || location.pathname === '';
|
||||||
|
try {
|
||||||
|
if (isRoot) {
|
||||||
|
hideBackButton();
|
||||||
|
} else {
|
||||||
|
showBackButton();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Back button not mounted
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
// Check 1: initDataUnsafe should have user data in real Telegram
|
// Stable handler — ref prevents re-subscription on every render
|
||||||
const hasUserData = webApp.initDataUnsafe?.user?.id !== undefined;
|
const handler = useCallback(() => {
|
||||||
|
navigateRef.current(-1);
|
||||||
// Check 2: Platform should not be 'unknown' (which is default in browser)
|
}, []);
|
||||||
const validPlatform = webApp.platform !== 'unknown' && webApp.platform !== '';
|
|
||||||
|
|
||||||
// Check 3: Version should be present (SDK loads in Telegram only)
|
|
||||||
const hasVersion = webApp.version !== undefined && webApp.version !== '';
|
|
||||||
|
|
||||||
return hasUserData || (validPlatform && hasVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component wrapper for Telegram navigator setup.
|
|
||||||
* Integrates Telegram Mini Apps navigator with React Router to provide
|
|
||||||
* automatic BackButton management based on navigation history.
|
|
||||||
* Falls back to BrowserRouter when not in Telegram Mini App.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Navigator-based router for Telegram Mini App
|
|
||||||
*/
|
|
||||||
function TelegramRouter({ children }: { children: React.ReactNode }) {
|
|
||||||
const navigator = useMemo(() => initNavigator('app-navigation-state', { hashMode: null }), []);
|
|
||||||
const [location, reactNavigator] = useIntegration(navigator);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
navigator.attach();
|
onBackButtonClick(handler);
|
||||||
|
} catch {
|
||||||
|
// Back button not mounted
|
||||||
|
}
|
||||||
return () => {
|
return () => {
|
||||||
try {
|
try {
|
||||||
navigator.detach();
|
offBackButtonClick(handler);
|
||||||
} catch (err) {
|
} catch {
|
||||||
console.warn('Failed to detach navigator:', err);
|
// Back button not mounted
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (err) {
|
}, [handler]);
|
||||||
console.warn('Failed to attach navigator:', err);
|
|
||||||
}
|
|
||||||
}, [navigator]);
|
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<Router location={location} navigator={reactNavigator}>
|
|
||||||
{children}
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppWithNavigator() {
|
export function AppWithNavigator() {
|
||||||
const isTelegram = useMemo(() => {
|
const isTelegram = isInTelegramWebApp();
|
||||||
const result = isTelegramMiniApp();
|
|
||||||
console.log('[AppWithNavigator] Platform detection:', {
|
|
||||||
isTelegram: result,
|
|
||||||
platform: window.Telegram?.WebApp?.platform,
|
|
||||||
hasUser: window.Telegram?.WebApp?.initDataUnsafe?.user?.id !== undefined,
|
|
||||||
version: window.Telegram?.WebApp?.version,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Common app content
|
return (
|
||||||
const appContent = (
|
<BrowserRouter>
|
||||||
|
{isTelegram && <TelegramBackButton />}
|
||||||
<PlatformProvider>
|
<PlatformProvider>
|
||||||
<ThemeColorsProvider>
|
<ThemeColorsProvider>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -91,12 +77,6 @@ export function AppWithNavigator() {
|
|||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</ThemeColorsProvider>
|
</ThemeColorsProvider>
|
||||||
</PlatformProvider>
|
</PlatformProvider>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use Telegram navigator in Mini App, BrowserRouter elsewhere
|
|
||||||
if (isTelegram) {
|
|
||||||
return <TelegramRouter>{appContent}</TelegramRouter>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <BrowserRouter>{appContent}</BrowserRouter>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
|
||||||
|
import { retrieveRawInitData } from '@telegram-apps/sdk-react';
|
||||||
import {
|
import {
|
||||||
tokenStorage,
|
tokenStorage,
|
||||||
isTokenExpired,
|
isTokenExpired,
|
||||||
@@ -41,10 +42,14 @@ function ensureCsrfToken(): string {
|
|||||||
const getTelegramInitData = (): string | null => {
|
const getTelegramInitData = (): string | null => {
|
||||||
if (typeof window === 'undefined') return null;
|
if (typeof window === 'undefined') return null;
|
||||||
|
|
||||||
const initData = window.Telegram?.WebApp?.initData;
|
try {
|
||||||
if (initData) {
|
const raw = retrieveRawInitData();
|
||||||
tokenStorage.setTelegramInitData(initData);
|
if (raw) {
|
||||||
return initData;
|
tokenStorage.setTelegramInitData(raw);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not in Telegram or SDK not initialized
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokenStorage.getTelegramInitData();
|
return tokenStorage.getTelegramInitData();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { isInTelegramWebApp } from '@/hooks/useTelegramSDK';
|
||||||
|
|
||||||
interface ColorPickerProps {
|
interface ColorPickerProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -9,11 +10,6 @@ interface ColorPickerProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if running in Telegram WebApp
|
|
||||||
const isTelegramWebApp = (): boolean => {
|
|
||||||
return !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert hex to RGB
|
// Convert hex to RGB
|
||||||
const hexToRgb = (hex: string): { r: number; g: number; b: number } => {
|
const hexToRgb = (hex: string): { r: number; g: number; b: number } => {
|
||||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||||
@@ -117,7 +113,7 @@ export function ColorPicker({ value, onChange, label, description, disabled }: C
|
|||||||
const pickerRef = useRef<HTMLDivElement>(null);
|
const pickerRef = useRef<HTMLDivElement>(null);
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null);
|
const colorInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const isTelegram = useMemo(() => isTelegramWebApp(), []);
|
const isTelegram = useMemo(() => isInTelegramWebApp(), []);
|
||||||
|
|
||||||
// Sync with external value
|
// Sync with external value
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
|
||||||
import { subscriptionApi } from '../api/subscription';
|
import { subscriptionApi } from '../api/subscription';
|
||||||
import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
|
import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
|
||||||
import { useBackButton, useHaptic } from '@/platform';
|
import { useBackButton, useHaptic } from '@/platform';
|
||||||
@@ -300,19 +301,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
// actual deep link URL. This works for both http(s) and custom schemes.
|
// actual deep link URL. This works for both http(s) and custom schemes.
|
||||||
const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(deepLink)}&lang=${i18n.language || 'en'}`;
|
const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(deepLink)}&lang=${i18n.language || 'en'}`;
|
||||||
|
|
||||||
const tg = (
|
|
||||||
window as unknown as {
|
|
||||||
Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } };
|
|
||||||
}
|
|
||||||
).Telegram?.WebApp;
|
|
||||||
|
|
||||||
if (tg?.openLink) {
|
|
||||||
try {
|
try {
|
||||||
tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true });
|
sdkOpenLink(redirectUrl, { tryInstantView: false });
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch {
|
||||||
/* fallback */
|
// SDK not available, fallback
|
||||||
}
|
|
||||||
}
|
}
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { createPortal } from 'react-dom';
|
|
||||||
import { promoApi, PromoOffer } from '../api/promo';
|
import { promoApi, PromoOffer } from '../api/promo';
|
||||||
import { ClockIcon, CheckIcon } from './icons';
|
import { ClockIcon, CheckIcon } from './icons';
|
||||||
import { useTelegramSDK } from '../hooks/useTelegramSDK';
|
import { usePlatform } from '@/platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const formatTimeLeft = (
|
const formatTimeLeft = (
|
||||||
@@ -81,140 +80,6 @@ const XCircleIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
const WarningIcon = () => (
|
|
||||||
<svg
|
|
||||||
className="h-12 w-12"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth={1.5}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
// ------- Confirmation Modal -------
|
|
||||||
|
|
||||||
interface DeactivateConfirmModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
discountPercent: number;
|
|
||||||
isDeactivating: boolean;
|
|
||||||
onConfirm: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DeactivateConfirmModal({
|
|
||||||
isOpen,
|
|
||||||
discountPercent,
|
|
||||||
isDeactivating,
|
|
||||||
onConfirm,
|
|
||||||
onCancel,
|
|
||||||
}: DeactivateConfirmModalProps) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
// Escape key to close
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Escape' && !isDeactivating) {
|
|
||||||
e.preventDefault();
|
|
||||||
onCancel();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown);
|
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
||||||
}, [isOpen, isDeactivating, onCancel]);
|
|
||||||
|
|
||||||
// Scroll lock
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isOpen) return;
|
|
||||||
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
};
|
|
||||||
}, [isOpen]);
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
const modalContent = (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-[100] flex items-center justify-center"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
aria-labelledby="deactivate-modal-title"
|
|
||||||
>
|
|
||||||
{/* Backdrop */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 bg-black/70 backdrop-blur-sm"
|
|
||||||
onClick={isDeactivating ? undefined : onCancel}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Modal */}
|
|
||||||
<div
|
|
||||||
className="relative mx-4 w-full max-w-sm overflow-hidden rounded-2xl border border-dark-700/50 bg-dark-900 shadow-2xl"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{/* Warning header */}
|
|
||||||
<div className="flex flex-col items-center bg-gradient-to-br from-warning-500/20 to-red-500/20 px-6 pb-6 pt-8">
|
|
||||||
<div className="mb-3 text-warning-400">
|
|
||||||
<WarningIcon />
|
|
||||||
</div>
|
|
||||||
<h2 id="deactivate-modal-title" className="text-center text-lg font-bold text-dark-100">
|
|
||||||
{t('promo.deactivate.confirmTitle')}
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-center text-sm text-dark-300">
|
|
||||||
{t('promo.deactivate.confirmDescription', { percent: discountPercent })}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Warning text */}
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
<div className="rounded-lg border border-warning-500/20 bg-warning-500/10 px-4 py-3">
|
|
||||||
<p className="text-center text-sm text-warning-400">{t('promo.deactivate.warning')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action buttons */}
|
|
||||||
<div className="flex gap-3 px-6 pb-6">
|
|
||||||
<button
|
|
||||||
onClick={onCancel}
|
|
||||||
disabled={isDeactivating}
|
|
||||||
className="flex-1 rounded-xl bg-dark-800 py-3 font-semibold text-dark-300 transition-colors hover:bg-dark-700 hover:text-dark-100 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{t('common.cancel')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={onConfirm}
|
|
||||||
disabled={isDeactivating}
|
|
||||||
className="flex-1 rounded-xl bg-gradient-to-r from-red-500 to-red-600 py-3 font-semibold text-white shadow-lg shadow-red-500/25 transition-all hover:from-red-400 hover:to-red-500 active:from-red-600 active:to-red-700 disabled:opacity-50"
|
|
||||||
>
|
|
||||||
{isDeactivating ? (
|
|
||||||
<span className="flex items-center justify-center gap-2">
|
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
||||||
{t('promo.deactivate.deactivating')}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
t('promo.deactivate.confirm')
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
return createPortal(modalContent, document.body);
|
|
||||||
}
|
|
||||||
return modalContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------- Main Component -------
|
// ------- Main Component -------
|
||||||
|
|
||||||
interface PromoOffersSectionProps {
|
interface PromoOffersSectionProps {
|
||||||
@@ -225,11 +90,10 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { isTelegramWebApp } = useTelegramSDK();
|
const { dialog, capabilities } = usePlatform();
|
||||||
const [claimingId, setClaimingId] = useState<number | null>(null);
|
const [claimingId, setClaimingId] = useState<number | null>(null);
|
||||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
|
||||||
|
|
||||||
// Fetch available offers
|
// Fetch available offers
|
||||||
const { data: offers = [], isLoading: offersLoading } = useQuery({
|
const { data: offers = [], isLoading: offersLoading } = useQuery({
|
||||||
@@ -272,14 +136,14 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
queryClient.invalidateQueries({ queryKey: ['promo-offers'] });
|
queryClient.invalidateQueries({ queryKey: ['promo-offers'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
queryClient.invalidateQueries({ queryKey: ['subscription'] });
|
||||||
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
queryClient.invalidateQueries({ queryKey: ['purchase-options'] });
|
||||||
setShowConfirmModal(false);
|
|
||||||
setSuccessMessage(t('promo.deactivate.success'));
|
setSuccessMessage(t('promo.deactivate.success'));
|
||||||
setTimeout(() => setSuccessMessage(null), 5000);
|
setTimeout(() => setSuccessMessage(null), 5000);
|
||||||
},
|
},
|
||||||
onError: (error: unknown) => {
|
onError: (error: unknown) => {
|
||||||
const axiosErr = error as { response?: { data?: { detail?: string } } };
|
const axiosErr = error as { response?: { data?: { detail?: string } } };
|
||||||
setErrorMessage(axiosErr.response?.data?.detail || t('promo.deactivate.error'));
|
setErrorMessage(axiosErr.response?.data?.detail || t('promo.deactivate.error'));
|
||||||
setShowConfirmModal(false);
|
|
||||||
setTimeout(() => setErrorMessage(null), 5000);
|
setTimeout(() => setErrorMessage(null), 5000);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -299,38 +163,32 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
setErrorMessage(null);
|
setErrorMessage(null);
|
||||||
setSuccessMessage(null);
|
setSuccessMessage(null);
|
||||||
|
|
||||||
// Use Telegram native popup in Mini App
|
if (capabilities.hasNativeDialogs) {
|
||||||
if (isTelegramWebApp && window.Telegram?.WebApp?.showPopup) {
|
dialog
|
||||||
window.Telegram.WebApp.showPopup(
|
.popup({
|
||||||
{
|
|
||||||
title: t('promo.deactivate.confirmTitle'),
|
title: t('promo.deactivate.confirmTitle'),
|
||||||
message: t('promo.deactivate.confirmDescription', {
|
message: t('promo.deactivate.confirmDescription', {
|
||||||
percent: activeDiscount?.discount_percent || 0,
|
percent: activeDiscount?.discount_percent || 0,
|
||||||
}),
|
}),
|
||||||
buttons: [
|
buttons: [
|
||||||
{ id: 'cancel', type: 'cancel' },
|
{ id: 'cancel', type: 'cancel', text: '' },
|
||||||
{ id: 'confirm', type: 'destructive', text: t('promo.deactivate.confirm') },
|
{ id: 'confirm', type: 'destructive', text: t('promo.deactivate.confirm') },
|
||||||
],
|
],
|
||||||
},
|
})
|
||||||
(button_id: string) => {
|
.then((buttonId) => {
|
||||||
if (button_id === 'confirm') {
|
if (buttonId === 'confirm') {
|
||||||
deactivateMutation.mutate();
|
deactivateMutation.mutate();
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
// Fallback to modal for web
|
const confirmed = window.confirm(
|
||||||
setShowConfirmModal(true);
|
t('promo.deactivate.confirmDescription', {
|
||||||
}
|
percent: activeDiscount?.discount_percent || 0,
|
||||||
};
|
}),
|
||||||
|
);
|
||||||
const handleConfirmDeactivate = () => {
|
if (confirmed) {
|
||||||
deactivateMutation.mutate();
|
deactivateMutation.mutate();
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleCloseConfirmModal = () => {
|
|
||||||
if (!deactivateMutation.isPending) {
|
|
||||||
setShowConfirmModal(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -485,17 +343,6 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Deactivation Confirmation Modal */}
|
|
||||||
{activeDiscount && (
|
|
||||||
<DeactivateConfirmModal
|
|
||||||
isOpen={showConfirmModal}
|
|
||||||
discountPercent={activeDiscount.discount_percent || 0}
|
|
||||||
isDeactivating={deactivateMutation.isPending}
|
|
||||||
onConfirm={handleConfirmDeactivate}
|
|
||||||
onCancel={handleCloseConfirmModal}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Link, useLocation } from 'react-router-dom';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { initDataUser } from '@telegram-apps/sdk-react';
|
||||||
|
|
||||||
import { useAuthStore } from '@/store/auth';
|
import { useAuthStore } from '@/store/auth';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
@@ -113,17 +114,12 @@ export function AppHeader({
|
|||||||
// Get user photo from Telegram
|
// Get user photo from Telegram
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const tg = (
|
const user = initDataUser();
|
||||||
window as unknown as {
|
if (user?.photo_url) {
|
||||||
Telegram?: { WebApp?: { initDataUnsafe?: { user?: { photo_url?: string } } } };
|
setUserPhotoUrl(user.photo_url);
|
||||||
}
|
}
|
||||||
).Telegram?.WebApp;
|
} catch {
|
||||||
const photoUrl = tg?.initDataUnsafe?.user?.photo_url;
|
// Not in Telegram or init data not available
|
||||||
if (photoUrl) {
|
|
||||||
setUserPhotoUrl(photoUrl);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Failed to get Telegram user photo:', e);
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
useSignal,
|
||||||
|
isFullscreen as isFullscreenSignal,
|
||||||
|
viewportHeight as viewportHeightSignal,
|
||||||
|
viewportStableHeight as viewportStableHeightSignal,
|
||||||
|
isViewportExpanded as isViewportExpandedSignal,
|
||||||
|
viewportSafeAreaInsets,
|
||||||
|
viewportContentSafeAreaInsets,
|
||||||
|
requestFullscreen as sdkRequestFullscreen,
|
||||||
|
exitFullscreen as sdkExitFullscreen,
|
||||||
|
disableVerticalSwipes as sdkDisableVerticalSwipes,
|
||||||
|
enableVerticalSwipes as sdkEnableVerticalSwipes,
|
||||||
|
expandViewport,
|
||||||
|
retrieveLaunchParams,
|
||||||
|
retrieveRawInitData,
|
||||||
|
} from '@telegram-apps/sdk-react';
|
||||||
|
|
||||||
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
|
const FULLSCREEN_CACHE_KEY = 'cabinet_fullscreen_enabled';
|
||||||
|
|
||||||
@@ -24,60 +40,47 @@ export const setCachedFullscreenEnabled = (enabled: boolean) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cached detection result (evaluated once at module load)
|
||||||
|
let _isInTelegram: boolean | null = null;
|
||||||
|
function detectTelegram(): boolean {
|
||||||
|
if (_isInTelegram === null) {
|
||||||
|
try {
|
||||||
|
retrieveLaunchParams();
|
||||||
|
_isInTelegram = true;
|
||||||
|
} catch {
|
||||||
|
_isInTelegram = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _isInTelegram;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we're actually running inside Telegram Mini App
|
* Check if we're actually running inside Telegram Mini App
|
||||||
*/
|
*/
|
||||||
export function isInTelegramWebApp(): boolean {
|
export function isInTelegramWebApp(): boolean {
|
||||||
const webApp = window.Telegram?.WebApp;
|
return detectTelegram();
|
||||||
return Boolean(webApp?.initData && webApp.initData.length > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if running on mobile Telegram client (iOS/Android)
|
* Check if running on mobile Telegram client (iOS/Android)
|
||||||
*/
|
*/
|
||||||
export function isTelegramMobile(): boolean {
|
export function isTelegramMobile(): boolean {
|
||||||
const webApp = window.Telegram?.WebApp;
|
try {
|
||||||
if (!webApp?.platform) return false;
|
const { tgWebAppPlatform } = retrieveLaunchParams();
|
||||||
return webApp.platform === 'ios' || webApp.platform === 'android';
|
return tgWebAppPlatform === 'ios' || tgWebAppPlatform === 'android';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Telegram init data for authentication
|
* Get Telegram init data for authentication
|
||||||
*/
|
*/
|
||||||
export function getTelegramInitData(): string | null {
|
export function getTelegramInitData(): string | null {
|
||||||
const webApp = window.Telegram?.WebApp;
|
try {
|
||||||
return webApp?.initData || null;
|
return retrieveRawInitData() || null;
|
||||||
}
|
} catch {
|
||||||
|
return null;
|
||||||
/**
|
|
||||||
* Initialize Telegram WebApp (basic setup without SDK)
|
|
||||||
*/
|
|
||||||
export function initTelegramSDK() {
|
|
||||||
if (!isInTelegramWebApp()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tg = window.Telegram?.WebApp;
|
|
||||||
if (!tg) return;
|
|
||||||
|
|
||||||
// Basic initialization
|
|
||||||
tg.ready();
|
|
||||||
tg.expand();
|
|
||||||
|
|
||||||
// Disable closing confirmation by default
|
|
||||||
tg.disableClosingConfirmation?.();
|
|
||||||
|
|
||||||
// Disable vertical swipes to prevent accidental closures
|
|
||||||
tg.disableVerticalSwipes?.();
|
|
||||||
|
|
||||||
// Auto-enter fullscreen if enabled in settings (mobile only)
|
|
||||||
const fullscreenEnabled = getCachedFullscreenEnabled();
|
|
||||||
if (fullscreenEnabled && isTelegramMobile() && tg.requestFullscreen) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!tg.isFullscreen) {
|
|
||||||
tg.requestFullscreen?.();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,54 +103,72 @@ const defaultInsets = { top: 0, bottom: 0, left: 0, right: 0 };
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for Telegram WebApp integration
|
* Hook for Telegram WebApp integration
|
||||||
* Uses native window.Telegram.WebApp API
|
* Uses @telegram-apps/sdk-react v3 signals
|
||||||
*/
|
*/
|
||||||
export function useTelegramSDK() {
|
export function useTelegramSDK() {
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = detectTelegram();
|
||||||
const tg = window.Telegram?.WebApp;
|
|
||||||
|
|
||||||
const platform = useMemo<TelegramPlatform>(() => {
|
const platform = useMemo<TelegramPlatform>(() => {
|
||||||
if (!inTelegram) return undefined;
|
try {
|
||||||
return tg?.platform as TelegramPlatform;
|
return retrieveLaunchParams().tgWebAppPlatform as TelegramPlatform;
|
||||||
}, [inTelegram, tg?.platform]);
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const isMobile = platform === 'ios' || platform === 'android';
|
const isMobile = platform === 'ios' || platform === 'android';
|
||||||
|
|
||||||
// Safe area insets from Telegram WebApp
|
// Always call useSignal unconditionally (Rules of Hooks).
|
||||||
|
// When not in Telegram, the signals will have their default values.
|
||||||
|
const fullscreenValue = useSignal(isFullscreenSignal);
|
||||||
|
const heightValue = useSignal(viewportHeightSignal);
|
||||||
|
const stableHeightValue = useSignal(viewportStableHeightSignal);
|
||||||
|
const expandedValue = useSignal(isViewportExpandedSignal);
|
||||||
|
const safeInsets = useSignal(viewportSafeAreaInsets);
|
||||||
|
const contentSafeInsets = useSignal(viewportContentSafeAreaInsets);
|
||||||
|
|
||||||
|
const isFullscreen = inTelegram ? (fullscreenValue ?? false) : false;
|
||||||
|
const viewportHeight = inTelegram ? (heightValue ?? 0) : 0;
|
||||||
|
const viewportStableHeight = inTelegram ? (stableHeightValue ?? 0) : 0;
|
||||||
|
const isExpanded = inTelegram ? (expandedValue ?? true) : true;
|
||||||
|
|
||||||
const safeAreaInset = useMemo(() => {
|
const safeAreaInset = useMemo(() => {
|
||||||
if (!inTelegram || !tg?.safeAreaInset) return defaultInsets;
|
if (!inTelegram || !safeInsets) return defaultInsets;
|
||||||
return {
|
return {
|
||||||
top: tg.safeAreaInset.top || 0,
|
top: safeInsets.top || 0,
|
||||||
bottom: tg.safeAreaInset.bottom || 0,
|
bottom: safeInsets.bottom || 0,
|
||||||
left: tg.safeAreaInset.left || 0,
|
left: safeInsets.left || 0,
|
||||||
right: tg.safeAreaInset.right || 0,
|
right: safeInsets.right || 0,
|
||||||
};
|
};
|
||||||
}, [inTelegram, tg?.safeAreaInset]);
|
}, [inTelegram, safeInsets]);
|
||||||
|
|
||||||
const contentSafeAreaInset = useMemo(() => {
|
const contentSafeAreaInset = useMemo(() => {
|
||||||
if (!inTelegram || !tg?.contentSafeAreaInset) return defaultInsets;
|
if (!inTelegram || !contentSafeInsets) return defaultInsets;
|
||||||
return {
|
return {
|
||||||
top: tg.contentSafeAreaInset.top || 0,
|
top: contentSafeInsets.top || 0,
|
||||||
bottom: tg.contentSafeAreaInset.bottom || 0,
|
bottom: contentSafeInsets.bottom || 0,
|
||||||
left: tg.contentSafeAreaInset.left || 0,
|
left: contentSafeInsets.left || 0,
|
||||||
right: tg.contentSafeAreaInset.right || 0,
|
right: contentSafeInsets.right || 0,
|
||||||
};
|
};
|
||||||
}, [inTelegram, tg?.contentSafeAreaInset]);
|
}, [inTelegram, contentSafeInsets]);
|
||||||
|
|
||||||
const isFullscreen = tg?.isFullscreen ?? false;
|
|
||||||
const viewportHeight = tg?.viewportHeight ?? 0;
|
|
||||||
const viewportStableHeight = tg?.viewportStableHeight ?? 0;
|
|
||||||
const isExpanded = tg?.isExpanded ?? true;
|
|
||||||
|
|
||||||
const requestFullscreen = useCallback(() => {
|
const requestFullscreen = useCallback(() => {
|
||||||
if (!inTelegram || !tg?.requestFullscreen) return;
|
if (!inTelegram) return;
|
||||||
tg.requestFullscreen();
|
try {
|
||||||
}, [inTelegram, tg]);
|
sdkRequestFullscreen();
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
|
}
|
||||||
|
}, [inTelegram]);
|
||||||
|
|
||||||
const exitFullscreen = useCallback(() => {
|
const exitFullscreen = useCallback(() => {
|
||||||
if (!inTelegram || !tg?.exitFullscreen) return;
|
if (!inTelegram) return;
|
||||||
tg.exitFullscreen();
|
try {
|
||||||
}, [inTelegram, tg]);
|
sdkExitFullscreen();
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
|
}
|
||||||
|
}, [inTelegram]);
|
||||||
|
|
||||||
const toggleFullscreen = useCallback(() => {
|
const toggleFullscreen = useCallback(() => {
|
||||||
if (isFullscreen) {
|
if (isFullscreen) {
|
||||||
@@ -158,21 +179,33 @@ export function useTelegramSDK() {
|
|||||||
}, [isFullscreen, requestFullscreen, exitFullscreen]);
|
}, [isFullscreen, requestFullscreen, exitFullscreen]);
|
||||||
|
|
||||||
const expand = useCallback(() => {
|
const expand = useCallback(() => {
|
||||||
if (!inTelegram || !tg?.expand) return;
|
if (!inTelegram) return;
|
||||||
tg.expand();
|
try {
|
||||||
}, [inTelegram, tg]);
|
expandViewport();
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
|
}
|
||||||
|
}, [inTelegram]);
|
||||||
|
|
||||||
const disableVerticalSwipes = useCallback(() => {
|
const disableVerticalSwipes = useCallback(() => {
|
||||||
if (!inTelegram || !tg?.disableVerticalSwipes) return;
|
if (!inTelegram) return;
|
||||||
tg.disableVerticalSwipes();
|
try {
|
||||||
}, [inTelegram, tg]);
|
sdkDisableVerticalSwipes();
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
|
}
|
||||||
|
}, [inTelegram]);
|
||||||
|
|
||||||
const enableVerticalSwipes = useCallback(() => {
|
const enableVerticalSwipes = useCallback(() => {
|
||||||
if (!inTelegram || !tg?.enableVerticalSwipes) return;
|
if (!inTelegram) return;
|
||||||
tg.enableVerticalSwipes();
|
try {
|
||||||
}, [inTelegram, tg]);
|
sdkEnableVerticalSwipes();
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
|
}
|
||||||
|
}, [inTelegram]);
|
||||||
|
|
||||||
const isFullscreenSupported = inTelegram && typeof tg?.requestFullscreen === 'function';
|
const isFullscreenSupported = inTelegram;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isTelegramWebApp: inTelegram,
|
isTelegramWebApp: inTelegram,
|
||||||
@@ -182,7 +215,7 @@ export function useTelegramSDK() {
|
|||||||
contentSafeAreaInset,
|
contentSafeAreaInset,
|
||||||
viewportHeight,
|
viewportHeight,
|
||||||
viewportStableHeight,
|
viewportStableHeight,
|
||||||
viewportWidth: 0, // Not available in native API
|
viewportWidth: 0,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
platform,
|
platform,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export {
|
|||||||
setCachedFullscreenEnabled,
|
setCachedFullscreenEnabled,
|
||||||
isInTelegramWebApp,
|
isInTelegramWebApp,
|
||||||
isTelegramMobile,
|
isTelegramMobile,
|
||||||
initTelegramSDK as initTelegramWebApp, // Alias for backward compatibility
|
|
||||||
} from './useTelegramSDK';
|
} from './useTelegramSDK';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +31,6 @@ export function useTelegramWebApp() {
|
|||||||
toggleFullscreen: sdk.toggleFullscreen,
|
toggleFullscreen: sdk.toggleFullscreen,
|
||||||
disableVerticalSwipes: sdk.disableVerticalSwipes,
|
disableVerticalSwipes: sdk.disableVerticalSwipes,
|
||||||
enableVerticalSwipes: sdk.enableVerticalSwipes,
|
enableVerticalSwipes: sdk.enableVerticalSwipes,
|
||||||
// For backward compatibility, expose webApp as the raw Telegram object
|
webApp: null,
|
||||||
webApp: window.Telegram?.WebApp ?? null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
89
src/main.tsx
89
src/main.tsx
@@ -1,14 +1,97 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
init,
|
||||||
|
restoreInitData,
|
||||||
|
mountMiniApp,
|
||||||
|
miniAppReady,
|
||||||
|
mountThemeParams,
|
||||||
|
mountViewport,
|
||||||
|
expandViewport,
|
||||||
|
mountSwipeBehavior,
|
||||||
|
disableVerticalSwipes,
|
||||||
|
mountClosingBehavior,
|
||||||
|
disableClosingConfirmation,
|
||||||
|
mountBackButton,
|
||||||
|
mountMainButton,
|
||||||
|
bindThemeParamsCssVars,
|
||||||
|
bindViewportCssVars,
|
||||||
|
requestFullscreen,
|
||||||
|
isFullscreen,
|
||||||
|
} from '@telegram-apps/sdk-react';
|
||||||
import { AppWithNavigator } from './AppWithNavigator';
|
import { AppWithNavigator } from './AppWithNavigator';
|
||||||
import { initLogoPreload } from './api/branding';
|
import { initLogoPreload } from './api/branding';
|
||||||
import { initTelegramSDK } from './hooks/useTelegramSDK';
|
import { getCachedFullscreenEnabled, isTelegramMobile } from './hooks/useTelegramSDK';
|
||||||
import './i18n';
|
import './i18n';
|
||||||
import './styles/globals.css';
|
import './styles/globals.css';
|
||||||
|
|
||||||
// Initialize Telegram SDK (init, viewport mount, CSS vars binding, swipe control)
|
// HMR guard — prevent double init when Vite hot-reloads the module
|
||||||
initTelegramSDK();
|
const HMR_KEY = '__tg_sdk_initialized';
|
||||||
|
const alreadyInitialized = (window as unknown as Record<string, unknown>)[HMR_KEY] === true;
|
||||||
|
|
||||||
|
if (!alreadyInitialized) {
|
||||||
|
(window as unknown as Record<string, unknown>)[HMR_KEY] = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
init();
|
||||||
|
restoreInitData();
|
||||||
|
|
||||||
|
// Mount components — each in its own try/catch so one failure doesn't block others
|
||||||
|
try {
|
||||||
|
mountMiniApp();
|
||||||
|
} catch {
|
||||||
|
/* already mounted */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mountThemeParams();
|
||||||
|
bindThemeParamsCssVars();
|
||||||
|
} catch {
|
||||||
|
/* already mounted */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mountSwipeBehavior();
|
||||||
|
disableVerticalSwipes();
|
||||||
|
} catch {
|
||||||
|
/* already mounted */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mountClosingBehavior();
|
||||||
|
disableClosingConfirmation();
|
||||||
|
} catch {
|
||||||
|
/* already mounted */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mountBackButton();
|
||||||
|
} catch {
|
||||||
|
/* already mounted */
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
mountMainButton();
|
||||||
|
} catch {
|
||||||
|
/* already mounted */
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viewport — async, fullscreen зависит от смонтированного viewport
|
||||||
|
mountViewport()
|
||||||
|
.then(() => {
|
||||||
|
bindViewportCssVars();
|
||||||
|
expandViewport();
|
||||||
|
|
||||||
|
// Auto-enter fullscreen if enabled in settings (mobile only)
|
||||||
|
if (getCachedFullscreenEnabled() && isTelegramMobile()) {
|
||||||
|
if (!isFullscreen()) {
|
||||||
|
requestFullscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
|
miniAppReady();
|
||||||
|
} catch {
|
||||||
|
// Not in Telegram — ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preload logo from cache immediately on page load
|
// Preload logo from cache immediately on page load
|
||||||
initLogoPreload();
|
initLogoPreload();
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export default function Login() {
|
|||||||
const initData = getTelegramInitData();
|
const initData = getTelegramInitData();
|
||||||
if (isInTelegramWebApp() && initData) {
|
if (isInTelegramWebApp() && initData) {
|
||||||
setIsTelegramWebApp(true);
|
setIsTelegramWebApp(true);
|
||||||
// Note: ready() and expand() are already called by initTelegramSDK in main.tsx
|
// Note: ready() and expand() are already called by SDK init in main.tsx
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await loginWithTelegram(initData);
|
await loginWithTelegram(initData);
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default function TelegramRedirect() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: ready(), expand(), and theme CSS vars are already handled by initTelegramSDK in main.tsx
|
// Note: ready(), expand(), and theme CSS vars are already handled by SDK init in main.tsx
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loginWithTelegram(initData);
|
await loginWithTelegram(initData);
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
import { isInTelegramWebApp } from '@/hooks/useTelegramSDK';
|
import { isInTelegramWebApp } from '@/hooks/useTelegramSDK';
|
||||||
|
import {
|
||||||
|
showBackButton,
|
||||||
|
hideBackButton,
|
||||||
|
onBackButtonClick,
|
||||||
|
offBackButtonClick,
|
||||||
|
isBackButtonVisible,
|
||||||
|
setMainButtonParams,
|
||||||
|
onMainButtonClick,
|
||||||
|
offMainButtonClick,
|
||||||
|
hapticFeedbackImpactOccurred,
|
||||||
|
hapticFeedbackNotificationOccurred,
|
||||||
|
hapticFeedbackSelectionChanged,
|
||||||
|
showPopup,
|
||||||
|
setMiniAppHeaderColor,
|
||||||
|
setMiniAppBottomBarColor,
|
||||||
|
themeParamsState,
|
||||||
|
getCloudStorageItem,
|
||||||
|
setCloudStorageItem,
|
||||||
|
deleteCloudStorageItem,
|
||||||
|
getCloudStorageKeys,
|
||||||
|
openInvoice,
|
||||||
|
openLink,
|
||||||
|
openTelegramLink,
|
||||||
|
shareURL,
|
||||||
|
enableClosingConfirmation,
|
||||||
|
disableClosingConfirmation,
|
||||||
|
} from '@telegram-apps/sdk-react';
|
||||||
import type {
|
import type {
|
||||||
PlatformContext,
|
PlatformContext,
|
||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
@@ -15,319 +42,342 @@ import type {
|
|||||||
HapticNotificationType,
|
HapticNotificationType,
|
||||||
} from '@/platform/types';
|
} from '@/platform/types';
|
||||||
|
|
||||||
// Use raw Telegram WebApp API directly - SDK has initialization issues
|
|
||||||
function getTelegram(): TelegramWebApp | null {
|
|
||||||
return window.Telegram?.WebApp ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCapabilities(): PlatformCapabilities {
|
function createCapabilities(): PlatformCapabilities {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hasBackButton: inTelegram && !!tg?.BackButton,
|
hasBackButton: inTelegram,
|
||||||
hasMainButton: inTelegram && !!tg?.MainButton,
|
hasMainButton: inTelegram,
|
||||||
hasHapticFeedback: inTelegram && !!tg?.HapticFeedback,
|
hasHapticFeedback: inTelegram,
|
||||||
hasNativeDialogs: inTelegram && !!tg?.showPopup,
|
hasNativeDialogs: inTelegram,
|
||||||
hasThemeSync: inTelegram,
|
hasThemeSync: inTelegram,
|
||||||
hasInvoice: !!tg?.openInvoice,
|
hasInvoice: inTelegram,
|
||||||
hasCloudStorage: inTelegram && !!tg?.CloudStorage,
|
hasCloudStorage: inTelegram,
|
||||||
hasShare: true,
|
hasShare: true,
|
||||||
version: tg?.version,
|
version: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createBackButtonController(): BackButtonController {
|
function createBackButtonController(): BackButtonController {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
let currentCallback: (() => void) | null = null;
|
let currentCallback: (() => void) | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
if (!inTelegram || !tg?.BackButton) return false;
|
if (!inTelegram) return false;
|
||||||
return tg.BackButton.isVisible;
|
try {
|
||||||
|
return isBackButtonVisible();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
show(onClick: () => void) {
|
show(onClick: () => void) {
|
||||||
if (!inTelegram || !tg?.BackButton) return;
|
if (!inTelegram) return;
|
||||||
|
|
||||||
// Remove previous callback if exists
|
|
||||||
if (currentCallback) {
|
if (currentCallback) {
|
||||||
tg.BackButton.offClick(currentCallback);
|
try {
|
||||||
|
offBackButtonClick(currentCallback);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCallback = onClick;
|
currentCallback = onClick;
|
||||||
tg.BackButton.onClick(onClick);
|
try {
|
||||||
tg.BackButton.show();
|
onBackButtonClick(onClick);
|
||||||
|
showBackButton();
|
||||||
|
} catch {
|
||||||
|
// Back button not mounted
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
if (!inTelegram || !tg?.BackButton) return;
|
if (!inTelegram) return;
|
||||||
|
|
||||||
if (currentCallback) {
|
if (currentCallback) {
|
||||||
tg.BackButton.offClick(currentCallback);
|
try {
|
||||||
|
offBackButtonClick(currentCallback);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
currentCallback = null;
|
currentCallback = null;
|
||||||
}
|
}
|
||||||
tg.BackButton.hide();
|
try {
|
||||||
|
hideBackButton();
|
||||||
|
} catch {
|
||||||
|
// Back button not mounted
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainButtonController(): MainButtonController {
|
function createMainButtonController(): MainButtonController {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
let currentCallback: (() => void) | null = null;
|
let currentCallback: (() => void) | null = null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get isVisible() {
|
get isVisible() {
|
||||||
if (!inTelegram || !tg?.MainButton) return false;
|
return false; // SDK v3 doesn't expose this as a simple getter
|
||||||
return tg.MainButton.isVisible;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
show(config: MainButtonConfig) {
|
show(config: MainButtonConfig) {
|
||||||
if (!inTelegram || !tg?.MainButton) return;
|
if (!inTelegram) return;
|
||||||
|
|
||||||
// Remove previous callback if exists
|
|
||||||
if (currentCallback) {
|
if (currentCallback) {
|
||||||
tg.MainButton.offClick(currentCallback);
|
try {
|
||||||
|
offMainButtonClick(currentCallback);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set button parameters
|
|
||||||
tg.MainButton.setText(config.text);
|
|
||||||
if (config.color) {
|
|
||||||
tg.MainButton.color = config.color;
|
|
||||||
}
|
|
||||||
if (config.textColor) {
|
|
||||||
tg.MainButton.textColor = config.textColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.isActive === false) {
|
|
||||||
tg.MainButton.disable();
|
|
||||||
} else {
|
|
||||||
tg.MainButton.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.isLoading) {
|
|
||||||
tg.MainButton.showProgress();
|
|
||||||
} else {
|
|
||||||
tg.MainButton.hideProgress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
currentCallback = config.onClick;
|
currentCallback = config.onClick;
|
||||||
tg.MainButton.onClick(config.onClick);
|
|
||||||
tg.MainButton.show();
|
try {
|
||||||
|
setMainButtonParams({
|
||||||
|
text: config.text,
|
||||||
|
isVisible: true,
|
||||||
|
isEnabled: config.isActive !== false,
|
||||||
|
isLoaderVisible: config.isLoading ?? false,
|
||||||
|
backgroundColor: config.color as `#${string}` | undefined,
|
||||||
|
textColor: config.textColor as `#${string}` | undefined,
|
||||||
|
});
|
||||||
|
onMainButtonClick(config.onClick);
|
||||||
|
} catch {
|
||||||
|
// Main button not mounted
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hide() {
|
hide() {
|
||||||
if (!inTelegram || !tg?.MainButton) return;
|
if (!inTelegram) return;
|
||||||
|
|
||||||
if (currentCallback) {
|
if (currentCallback) {
|
||||||
tg.MainButton.offClick(currentCallback);
|
try {
|
||||||
|
offMainButtonClick(currentCallback);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
currentCallback = null;
|
currentCallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
tg.MainButton.hideProgress();
|
try {
|
||||||
tg.MainButton.hide();
|
setMainButtonParams({ isVisible: false, isLoaderVisible: false });
|
||||||
|
} catch {
|
||||||
|
// Main button not mounted
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
showProgress(show: boolean) {
|
showProgress(show: boolean) {
|
||||||
if (!inTelegram || !tg?.MainButton) return;
|
if (!inTelegram) return;
|
||||||
|
try {
|
||||||
if (show) {
|
setMainButtonParams({ isLoaderVisible: show });
|
||||||
tg.MainButton.showProgress();
|
} catch {
|
||||||
} else {
|
// Main button not mounted
|
||||||
tg.MainButton.hideProgress();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setText(text: string) {
|
setText(text: string) {
|
||||||
if (!inTelegram || !tg?.MainButton) return;
|
if (!inTelegram) return;
|
||||||
tg.MainButton.setText(text);
|
try {
|
||||||
|
setMainButtonParams({ text });
|
||||||
|
} catch {
|
||||||
|
// Main button not mounted
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setActive(active: boolean) {
|
setActive(active: boolean) {
|
||||||
if (!inTelegram || !tg?.MainButton) return;
|
if (!inTelegram) return;
|
||||||
|
try {
|
||||||
if (active) {
|
setMainButtonParams({ isEnabled: active });
|
||||||
tg.MainButton.enable();
|
} catch {
|
||||||
} else {
|
// Main button not mounted
|
||||||
tg.MainButton.disable();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createHapticController(): HapticController {
|
function createHapticController(): HapticController {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
const haptic = tg?.HapticFeedback;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
impact(style: HapticImpactStyle = 'medium') {
|
impact(style: HapticImpactStyle = 'medium') {
|
||||||
if (!inTelegram || !haptic) return;
|
if (!inTelegram) return;
|
||||||
haptic.impactOccurred(style);
|
try {
|
||||||
|
hapticFeedbackImpactOccurred(style);
|
||||||
|
} catch {
|
||||||
|
// Haptic not available
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
notification(type: HapticNotificationType) {
|
notification(type: HapticNotificationType) {
|
||||||
if (!inTelegram || !haptic) return;
|
if (!inTelegram) return;
|
||||||
haptic.notificationOccurred(type);
|
try {
|
||||||
|
hapticFeedbackNotificationOccurred(type);
|
||||||
|
} catch {
|
||||||
|
// Haptic not available
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
selection() {
|
selection() {
|
||||||
if (!inTelegram || !haptic) return;
|
if (!inTelegram) return;
|
||||||
haptic.selectionChanged();
|
try {
|
||||||
|
hapticFeedbackSelectionChanged();
|
||||||
|
} catch {
|
||||||
|
// Haptic not available
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDialogController(): DialogController {
|
function createDialogController(): DialogController {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alert(message: string, _title?: string): Promise<void> {
|
async alert(message: string, _title?: string): Promise<void> {
|
||||||
return new Promise((resolve) => {
|
if (!inTelegram) {
|
||||||
if (inTelegram && tg?.showPopup) {
|
|
||||||
tg.showPopup({ message, buttons: [{ type: 'ok' }] }, () => resolve());
|
|
||||||
} else {
|
|
||||||
window.alert(message);
|
window.alert(message);
|
||||||
resolve();
|
return;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await showPopup({
|
||||||
|
message,
|
||||||
|
buttons: [{ type: 'ok', id: 'ok' }],
|
||||||
});
|
});
|
||||||
|
} catch {
|
||||||
|
window.alert(message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
confirm(message: string, _title?: string): Promise<boolean> {
|
async confirm(message: string, _title?: string): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
if (!inTelegram) {
|
||||||
if (inTelegram && tg?.showPopup) {
|
return window.confirm(message);
|
||||||
tg.showPopup(
|
}
|
||||||
{
|
try {
|
||||||
|
const buttonId = await showPopup({
|
||||||
message,
|
message,
|
||||||
buttons: [
|
buttons: [
|
||||||
{ id: 'ok', type: 'ok' },
|
{ type: 'ok', id: 'ok' },
|
||||||
{ id: 'cancel', type: 'cancel' },
|
{ type: 'cancel', id: 'cancel' },
|
||||||
],
|
],
|
||||||
},
|
|
||||||
(buttonId) => resolve(buttonId === 'ok'),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
resolve(window.confirm(message));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return buttonId === 'ok';
|
||||||
|
} catch {
|
||||||
|
return window.confirm(message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
popup(options: PopupOptions): Promise<string | null> {
|
async popup(options: PopupOptions): Promise<string | null> {
|
||||||
return new Promise((resolve) => {
|
if (!inTelegram) {
|
||||||
if (inTelegram && tg?.showPopup) {
|
return window.confirm(options.message) ? 'ok' : null;
|
||||||
tg.showPopup(
|
}
|
||||||
{
|
try {
|
||||||
|
const buttons = options.buttons?.map((btn) => {
|
||||||
|
// For 'ok', 'close', 'cancel' types: do NOT include text
|
||||||
|
// For 'default', 'destructive' types: text is required
|
||||||
|
if (btn.type === 'ok' || btn.type === 'close' || btn.type === 'cancel') {
|
||||||
|
return { type: btn.type, id: btn.id };
|
||||||
|
}
|
||||||
|
return { type: btn.type ?? ('default' as const), id: btn.id, text: btn.text };
|
||||||
|
});
|
||||||
|
|
||||||
|
const buttonId = await showPopup({
|
||||||
title: options.title,
|
title: options.title,
|
||||||
message: options.message,
|
message: options.message,
|
||||||
buttons: options.buttons?.map((btn) => ({
|
buttons: buttons as Parameters<typeof showPopup>[0]['buttons'],
|
||||||
id: btn.id,
|
|
||||||
type: btn.type,
|
|
||||||
text: btn.text,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
(buttonId) => resolve(buttonId ?? null),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const confirmed = window.confirm(options.message);
|
|
||||||
resolve(confirmed ? 'ok' : null);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return buttonId || null;
|
||||||
|
} catch {
|
||||||
|
return window.confirm(options.message) ? 'ok' : null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createThemeController(): ThemeController {
|
function createThemeController(): ThemeController {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setHeaderColor(color: string) {
|
setHeaderColor(color: string) {
|
||||||
if (!inTelegram || !tg?.setHeaderColor) return;
|
if (!inTelegram) return;
|
||||||
tg.setHeaderColor(color as `#${string}`);
|
try {
|
||||||
|
setMiniAppHeaderColor(color as `#${string}`);
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setBottomBarColor(color: string) {
|
setBottomBarColor(color: string) {
|
||||||
if (!inTelegram || !tg?.setBottomBarColor) return;
|
if (!inTelegram) return;
|
||||||
try {
|
try {
|
||||||
tg.setBottomBarColor(color as `#${string}`);
|
setMiniAppBottomBarColor(color as `#${string}`);
|
||||||
} catch {
|
} catch {
|
||||||
// Not supported in this version
|
// Not supported
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getThemeParams() {
|
getThemeParams() {
|
||||||
if (!inTelegram || !tg?.themeParams) return null;
|
if (!inTelegram) return null;
|
||||||
const params = tg.themeParams;
|
try {
|
||||||
|
const params = themeParamsState();
|
||||||
|
if (!params) return null;
|
||||||
|
// SDK v3 uses camelCase — convert to snake_case for our interface
|
||||||
return {
|
return {
|
||||||
bg_color: params.bg_color,
|
bg_color: params.bgColor,
|
||||||
text_color: params.text_color,
|
text_color: params.textColor,
|
||||||
hint_color: params.hint_color,
|
hint_color: params.hintColor,
|
||||||
link_color: params.link_color,
|
link_color: params.linkColor,
|
||||||
button_color: params.button_color,
|
button_color: params.buttonColor,
|
||||||
button_text_color: params.button_text_color,
|
button_text_color: params.buttonTextColor,
|
||||||
secondary_bg_color: params.secondary_bg_color,
|
secondary_bg_color: params.secondaryBgColor,
|
||||||
header_bg_color: params.header_bg_color,
|
header_bg_color: params.headerBgColor,
|
||||||
bottom_bar_bg_color: params.bottom_bar_bg_color,
|
bottom_bar_bg_color: params.bottomBarBgColor,
|
||||||
accent_text_color: params.accent_text_color,
|
accent_text_color: params.accentTextColor,
|
||||||
section_bg_color: params.section_bg_color,
|
section_bg_color: params.sectionBgColor,
|
||||||
section_header_text_color: params.section_header_text_color,
|
section_header_text_color: params.sectionHeaderTextColor,
|
||||||
subtitle_text_color: params.subtitle_text_color,
|
subtitle_text_color: params.subtitleTextColor,
|
||||||
destructive_text_color: params.destructive_text_color,
|
destructive_text_color: params.destructiveTextColor,
|
||||||
};
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCloudStorageController(): CloudStorageController | null {
|
function createCloudStorageController(): CloudStorageController | null {
|
||||||
const tg = getTelegram();
|
|
||||||
const inTelegram = isInTelegramWebApp();
|
const inTelegram = isInTelegramWebApp();
|
||||||
const storage = tg?.CloudStorage;
|
if (!inTelegram) return null;
|
||||||
|
|
||||||
if (!inTelegram || !storage) return null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async getItem(key: string): Promise<string | null> {
|
async getItem(key: string): Promise<string | null> {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
storage.getItem(key, (error, value) => {
|
const value = await getCloudStorageItem(key);
|
||||||
resolve(error ? null : value || null);
|
return value === '' ? null : value;
|
||||||
});
|
} catch {
|
||||||
});
|
return null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async setItem(key: string, value: string): Promise<void> {
|
async setItem(key: string, value: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
await setCloudStorageItem(key, value);
|
||||||
storage.setItem(key, value, (error) => {
|
|
||||||
if (error) reject(new Error(String(error)));
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async removeItem(key: string): Promise<void> {
|
async removeItem(key: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
await deleteCloudStorageItem(key);
|
||||||
storage.removeItem(key, (error) => {
|
|
||||||
if (error) reject(new Error(String(error)));
|
|
||||||
else resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getKeys(): Promise<string[]> {
|
async getKeys(): Promise<string[]> {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
storage.getKeys((error, keys) => {
|
return await getCloudStorageKeys();
|
||||||
resolve(error ? [] : keys || []);
|
} catch {
|
||||||
});
|
return [];
|
||||||
});
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createTelegramAdapter(): PlatformContext {
|
export function createTelegramAdapter(): PlatformContext {
|
||||||
const tg = getTelegram();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
platform: 'telegram',
|
platform: 'telegram',
|
||||||
capabilities: createCapabilities(),
|
capabilities: createCapabilities(),
|
||||||
@@ -339,28 +389,26 @@ export function createTelegramAdapter(): PlatformContext {
|
|||||||
cloudStorage: createCloudStorageController(),
|
cloudStorage: createCloudStorageController(),
|
||||||
|
|
||||||
openInvoice(url: string): Promise<InvoiceStatus> {
|
openInvoice(url: string): Promise<InvoiceStatus> {
|
||||||
return new Promise((resolve) => {
|
try {
|
||||||
if (tg?.openInvoice) {
|
return openInvoice(url, 'url') as Promise<InvoiceStatus>;
|
||||||
tg.openInvoice(url, (status) => resolve(status));
|
} catch {
|
||||||
} else {
|
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
resolve('pending');
|
return Promise.resolve('pending');
|
||||||
}
|
}
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
openLink(url: string, options?: { tryInstantView?: boolean }) {
|
openLink(url: string, options?: { tryInstantView?: boolean }) {
|
||||||
if (tg?.openLink) {
|
try {
|
||||||
tg.openLink(url, { try_instant_view: options?.tryInstantView });
|
openLink(url, { tryInstantView: options?.tryInstantView });
|
||||||
} else {
|
} catch {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
openTelegramLink(url: string) {
|
openTelegramLink(url: string) {
|
||||||
if (tg?.openTelegramLink) {
|
try {
|
||||||
tg.openTelegramLink(url);
|
openTelegramLink(url);
|
||||||
} else {
|
} catch {
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -377,24 +425,24 @@ export function createTelegramAdapter(): PlatformContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME;
|
try {
|
||||||
if (botUsername && tg?.openTelegramLink) {
|
shareURL(url || shareText, text);
|
||||||
const encoded = encodeURIComponent(shareText);
|
|
||||||
tg.openTelegramLink(`https://t.me/share/url?url=${encoded}`);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
} catch {
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setClosingConfirmation(enabled: boolean) {
|
setClosingConfirmation(enabled: boolean) {
|
||||||
|
try {
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
tg?.enableClosingConfirmation?.();
|
enableClosingConfirmation();
|
||||||
} else {
|
} else {
|
||||||
tg?.disableClosingConfirmation?.();
|
disableClosingConfirmation();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not supported
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
telegram: tg,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -271,7 +271,5 @@ export function createWebAdapter(): PlatformContext {
|
|||||||
window.onbeforeunload = null;
|
window.onbeforeunload = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
telegram: null,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,10 +131,4 @@ export interface PlatformContext {
|
|||||||
|
|
||||||
// Closing confirmation
|
// Closing confirmation
|
||||||
setClosingConfirmation: (enabled: boolean) => void;
|
setClosingConfirmation: (enabled: boolean) => void;
|
||||||
|
|
||||||
// Raw Telegram WebApp access (null in web)
|
|
||||||
telegram: TelegramWebApp | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: TelegramWebApp types are defined in vite-env.d.ts
|
|
||||||
// This file only re-exports the interface from the global scope
|
|
||||||
|
|||||||
369
src/vite-env.d.ts
vendored
369
src/vite-env.d.ts
vendored
@@ -10,372 +10,3 @@ interface ImportMetaEnv {
|
|||||||
interface ImportMeta {
|
interface ImportMeta {
|
||||||
readonly env: ImportMetaEnv;
|
readonly env: ImportMetaEnv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Telegram WebApp types - comprehensive type definitions for Bot API 8.0+
|
|
||||||
interface TelegramWebApp {
|
|
||||||
// Init data
|
|
||||||
initData: string;
|
|
||||||
initDataUnsafe: {
|
|
||||||
query_id?: string;
|
|
||||||
user?: {
|
|
||||||
id: number;
|
|
||||||
is_bot?: boolean;
|
|
||||||
first_name: string;
|
|
||||||
last_name?: string;
|
|
||||||
username?: string;
|
|
||||||
language_code?: string;
|
|
||||||
is_premium?: boolean;
|
|
||||||
added_to_attachment_menu?: boolean;
|
|
||||||
allows_write_to_pm?: boolean;
|
|
||||||
photo_url?: string;
|
|
||||||
};
|
|
||||||
receiver?: {
|
|
||||||
id: number;
|
|
||||||
is_bot?: boolean;
|
|
||||||
first_name: string;
|
|
||||||
last_name?: string;
|
|
||||||
username?: string;
|
|
||||||
language_code?: string;
|
|
||||||
is_premium?: boolean;
|
|
||||||
added_to_attachment_menu?: boolean;
|
|
||||||
allows_write_to_pm?: boolean;
|
|
||||||
photo_url?: string;
|
|
||||||
};
|
|
||||||
chat?: {
|
|
||||||
id: number;
|
|
||||||
type: 'group' | 'supergroup' | 'channel';
|
|
||||||
title: string;
|
|
||||||
username?: string;
|
|
||||||
photo_url?: string;
|
|
||||||
};
|
|
||||||
chat_type?: 'sender' | 'private' | 'group' | 'supergroup' | 'channel';
|
|
||||||
chat_instance?: string;
|
|
||||||
start_param?: string;
|
|
||||||
can_send_after?: number;
|
|
||||||
auth_date: number;
|
|
||||||
hash: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Platform info
|
|
||||||
version: string;
|
|
||||||
platform: string;
|
|
||||||
colorScheme: 'light' | 'dark';
|
|
||||||
|
|
||||||
// App state
|
|
||||||
isExpanded: boolean;
|
|
||||||
isClosingConfirmationEnabled: boolean;
|
|
||||||
isVerticalSwipesEnabled: boolean;
|
|
||||||
isFullscreen: boolean;
|
|
||||||
isOrientationLocked: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
|
|
||||||
// Viewport
|
|
||||||
viewportHeight: number;
|
|
||||||
viewportStableHeight: number;
|
|
||||||
|
|
||||||
// Safe areas
|
|
||||||
safeAreaInset: { top: number; bottom: number; left: number; right: number };
|
|
||||||
contentSafeAreaInset: { top: number; bottom: number; left: number; right: number };
|
|
||||||
|
|
||||||
// Theme colors (settable)
|
|
||||||
headerColor: string;
|
|
||||||
backgroundColor: string;
|
|
||||||
bottomBarColor?: string;
|
|
||||||
|
|
||||||
// Theme params (from Telegram)
|
|
||||||
themeParams: {
|
|
||||||
bg_color?: string;
|
|
||||||
text_color?: string;
|
|
||||||
hint_color?: string;
|
|
||||||
link_color?: string;
|
|
||||||
button_color?: string;
|
|
||||||
button_text_color?: string;
|
|
||||||
secondary_bg_color?: string;
|
|
||||||
header_bg_color?: string;
|
|
||||||
bottom_bar_bg_color?: string;
|
|
||||||
accent_text_color?: string;
|
|
||||||
section_bg_color?: string;
|
|
||||||
section_header_text_color?: string;
|
|
||||||
section_separator_color?: string;
|
|
||||||
subtitle_text_color?: string;
|
|
||||||
destructive_text_color?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Lifecycle methods
|
|
||||||
ready: () => void;
|
|
||||||
expand: () => void;
|
|
||||||
close: () => void;
|
|
||||||
|
|
||||||
// Links
|
|
||||||
openLink: (url: string, options?: { try_instant_view?: boolean; try_browser?: boolean }) => void;
|
|
||||||
openTelegramLink: (url: string) => void;
|
|
||||||
|
|
||||||
// Invoice
|
|
||||||
openInvoice: (
|
|
||||||
url: string,
|
|
||||||
callback?: (status: 'paid' | 'cancelled' | 'failed' | 'pending') => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
// Fullscreen API (Bot API 8.0+)
|
|
||||||
requestFullscreen: () => void;
|
|
||||||
exitFullscreen: () => void;
|
|
||||||
lockOrientation: () => void;
|
|
||||||
unlockOrientation: () => void;
|
|
||||||
|
|
||||||
// Vertical swipes control
|
|
||||||
disableVerticalSwipes: () => void;
|
|
||||||
enableVerticalSwipes: () => void;
|
|
||||||
|
|
||||||
// Closing confirmation
|
|
||||||
enableClosingConfirmation: () => void;
|
|
||||||
disableClosingConfirmation: () => void;
|
|
||||||
|
|
||||||
// Theme color control
|
|
||||||
setHeaderColor: (color: string) => void;
|
|
||||||
setBackgroundColor: (color: string) => void;
|
|
||||||
setBottomBarColor?: (color: string) => void;
|
|
||||||
|
|
||||||
// Event handlers
|
|
||||||
onEvent: (eventType: string, callback: (...args: unknown[]) => void) => void;
|
|
||||||
offEvent: (eventType: string, callback: (...args: unknown[]) => void) => void;
|
|
||||||
|
|
||||||
// Data sending
|
|
||||||
sendData: (data: string) => void;
|
|
||||||
|
|
||||||
// QR Scanner
|
|
||||||
showScanQrPopup: (params: { text?: string }, callback?: (text: string) => boolean | void) => void;
|
|
||||||
closeScanQrPopup: () => void;
|
|
||||||
|
|
||||||
// Clipboard
|
|
||||||
readTextFromClipboard: (callback?: (text: string | null) => void) => void;
|
|
||||||
|
|
||||||
// Write access
|
|
||||||
requestWriteAccess: (callback?: (granted: boolean) => void) => void;
|
|
||||||
|
|
||||||
// Contact
|
|
||||||
requestContact: (callback?: (granted: boolean) => void) => void;
|
|
||||||
|
|
||||||
// Emoji status (Bot API 8.0+)
|
|
||||||
requestEmojiStatusAccess: (callback?: (granted: boolean) => void) => void;
|
|
||||||
setEmojiStatus: (
|
|
||||||
customEmojiId: string,
|
|
||||||
params?: { duration?: number },
|
|
||||||
callback?: (success: boolean) => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
// Download file (Bot API 8.0+)
|
|
||||||
downloadFile: (
|
|
||||||
params: { url: string; file_name: string },
|
|
||||||
callback?: (success: boolean) => void,
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
// Share story (Bot API 7.8+)
|
|
||||||
shareToStory?: (
|
|
||||||
mediaUrl: string,
|
|
||||||
params?: { text?: string; widget_link?: { url: string; name?: string } },
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
// Version check helper
|
|
||||||
isVersionAtLeast: (version: string) => boolean;
|
|
||||||
|
|
||||||
// Main Button
|
|
||||||
MainButton: {
|
|
||||||
text: string;
|
|
||||||
color: string;
|
|
||||||
textColor: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
isProgressVisible: boolean;
|
|
||||||
show: () => void;
|
|
||||||
hide: () => void;
|
|
||||||
enable: () => void;
|
|
||||||
disable: () => void;
|
|
||||||
showProgress: (leaveActive?: boolean) => void;
|
|
||||||
hideProgress: () => void;
|
|
||||||
setText: (text: string) => void;
|
|
||||||
setParams: (params: {
|
|
||||||
text?: string;
|
|
||||||
color?: string;
|
|
||||||
text_color?: string;
|
|
||||||
has_shine_effect?: boolean;
|
|
||||||
is_active?: boolean;
|
|
||||||
is_visible?: boolean;
|
|
||||||
}) => void;
|
|
||||||
onClick: (callback: () => void) => void;
|
|
||||||
offClick: (callback: () => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Secondary Button (Bot API 7.10+)
|
|
||||||
SecondaryButton?: {
|
|
||||||
text: string;
|
|
||||||
color: string;
|
|
||||||
textColor: string;
|
|
||||||
isVisible: boolean;
|
|
||||||
isActive: boolean;
|
|
||||||
isProgressVisible: boolean;
|
|
||||||
position: 'left' | 'right' | 'top' | 'bottom';
|
|
||||||
show: () => void;
|
|
||||||
hide: () => void;
|
|
||||||
enable: () => void;
|
|
||||||
disable: () => void;
|
|
||||||
showProgress: (leaveActive?: boolean) => void;
|
|
||||||
hideProgress: () => void;
|
|
||||||
setText: (text: string) => void;
|
|
||||||
setParams: (params: {
|
|
||||||
text?: string;
|
|
||||||
color?: string;
|
|
||||||
text_color?: string;
|
|
||||||
has_shine_effect?: boolean;
|
|
||||||
is_active?: boolean;
|
|
||||||
is_visible?: boolean;
|
|
||||||
position?: 'left' | 'right' | 'top' | 'bottom';
|
|
||||||
}) => void;
|
|
||||||
onClick: (callback: () => void) => void;
|
|
||||||
offClick: (callback: () => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Back Button
|
|
||||||
BackButton: {
|
|
||||||
isVisible: boolean;
|
|
||||||
show: () => void;
|
|
||||||
hide: () => void;
|
|
||||||
onClick: (callback: () => void) => void;
|
|
||||||
offClick: (callback: () => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Settings Button (Bot API 7.0+)
|
|
||||||
SettingsButton?: {
|
|
||||||
isVisible: boolean;
|
|
||||||
show: () => void;
|
|
||||||
hide: () => void;
|
|
||||||
onClick: (callback: () => void) => void;
|
|
||||||
offClick: (callback: () => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Haptic Feedback (Bot API 6.1+)
|
|
||||||
HapticFeedback: {
|
|
||||||
impactOccurred: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => void;
|
|
||||||
notificationOccurred: (type: 'success' | 'warning' | 'error') => void;
|
|
||||||
selectionChanged: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Popups (Bot API 6.2+)
|
|
||||||
showPopup: (
|
|
||||||
params: {
|
|
||||||
title?: string;
|
|
||||||
message: string;
|
|
||||||
buttons?: Array<{
|
|
||||||
id?: string;
|
|
||||||
type?: 'default' | 'ok' | 'close' | 'cancel' | 'destructive';
|
|
||||||
text?: string;
|
|
||||||
}>;
|
|
||||||
},
|
|
||||||
callback?: (buttonId: string) => void,
|
|
||||||
) => void;
|
|
||||||
showAlert: (message: string, callback?: () => void) => void;
|
|
||||||
showConfirm: (message: string, callback?: (confirmed: boolean) => void) => void;
|
|
||||||
|
|
||||||
// Cloud Storage (Bot API 6.9+)
|
|
||||||
CloudStorage: {
|
|
||||||
setItem: (
|
|
||||||
key: string,
|
|
||||||
value: string,
|
|
||||||
callback?: (error: Error | null, stored: boolean) => void,
|
|
||||||
) => void;
|
|
||||||
getItem: (key: string, callback: (error: Error | null, value: string) => void) => void;
|
|
||||||
getItems: (
|
|
||||||
keys: string[],
|
|
||||||
callback: (error: Error | null, values: Record<string, string>) => void,
|
|
||||||
) => void;
|
|
||||||
removeItem: (key: string, callback?: (error: Error | null, removed: boolean) => void) => void;
|
|
||||||
removeItems: (
|
|
||||||
keys: string[],
|
|
||||||
callback?: (error: Error | null, removed: boolean) => void,
|
|
||||||
) => void;
|
|
||||||
getKeys: (callback: (error: Error | null, keys: string[]) => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Biometric (Bot API 7.2+)
|
|
||||||
BiometricManager?: {
|
|
||||||
isInited: boolean;
|
|
||||||
isBiometricAvailable: boolean;
|
|
||||||
biometricType: 'finger' | 'face' | 'unknown';
|
|
||||||
isAccessRequested: boolean;
|
|
||||||
isAccessGranted: boolean;
|
|
||||||
isBiometricTokenSaved: boolean;
|
|
||||||
deviceId: string;
|
|
||||||
init: (callback?: () => void) => void;
|
|
||||||
requestAccess: (params: { reason?: string }, callback?: (granted: boolean) => void) => void;
|
|
||||||
authenticate: (
|
|
||||||
params: { reason?: string },
|
|
||||||
callback?: (success: boolean, token?: string) => void,
|
|
||||||
) => void;
|
|
||||||
updateBiometricToken: (token: string, callback?: (success: boolean) => void) => void;
|
|
||||||
openSettings: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Accelerometer (Bot API 8.0+)
|
|
||||||
Accelerometer?: {
|
|
||||||
isStarted: boolean;
|
|
||||||
x: number | null;
|
|
||||||
y: number | null;
|
|
||||||
z: number | null;
|
|
||||||
start: (params?: { refresh_rate?: number }, callback?: (started: boolean) => void) => void;
|
|
||||||
stop: (callback?: (stopped: boolean) => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// DeviceOrientation (Bot API 8.0+)
|
|
||||||
DeviceOrientation?: {
|
|
||||||
isStarted: boolean;
|
|
||||||
absolute: boolean;
|
|
||||||
alpha: number | null;
|
|
||||||
beta: number | null;
|
|
||||||
gamma: number | null;
|
|
||||||
start: (
|
|
||||||
params?: { refresh_rate?: number; need_absolute?: boolean },
|
|
||||||
callback?: (started: boolean) => void,
|
|
||||||
) => void;
|
|
||||||
stop: (callback?: (stopped: boolean) => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Gyroscope (Bot API 8.0+)
|
|
||||||
Gyroscope?: {
|
|
||||||
isStarted: boolean;
|
|
||||||
x: number | null;
|
|
||||||
y: number | null;
|
|
||||||
z: number | null;
|
|
||||||
start: (params?: { refresh_rate?: number }, callback?: (started: boolean) => void) => void;
|
|
||||||
stop: (callback?: (stopped: boolean) => void) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Location Manager (Bot API 8.0+)
|
|
||||||
LocationManager?: {
|
|
||||||
isInited: boolean;
|
|
||||||
isLocationAvailable: boolean;
|
|
||||||
isAccessRequested: boolean;
|
|
||||||
isAccessGranted: boolean;
|
|
||||||
init: (callback?: () => void) => void;
|
|
||||||
getLocation: (
|
|
||||||
callback: (
|
|
||||||
location: {
|
|
||||||
latitude: number;
|
|
||||||
longitude: number;
|
|
||||||
altitude?: number;
|
|
||||||
course?: number;
|
|
||||||
speed?: number;
|
|
||||||
horizontal_accuracy?: number;
|
|
||||||
vertical_accuracy?: number;
|
|
||||||
course_accuracy?: number;
|
|
||||||
speed_accuracy?: number;
|
|
||||||
} | null,
|
|
||||||
) => void,
|
|
||||||
) => void;
|
|
||||||
openSettings: () => void;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Window {
|
|
||||||
Telegram?: {
|
|
||||||
WebApp: TelegramWebApp;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -49,11 +49,7 @@ export default defineConfig({
|
|||||||
'@radix-ui/react-visually-hidden',
|
'@radix-ui/react-visually-hidden',
|
||||||
],
|
],
|
||||||
'vendor-dnd': ['@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities'],
|
'vendor-dnd': ['@dnd-kit/core', '@dnd-kit/sortable', '@dnd-kit/utilities'],
|
||||||
'vendor-telegram': [
|
'vendor-telegram': ['@telegram-apps/sdk-react'],
|
||||||
'@telegram-apps/sdk',
|
|
||||||
'@telegram-apps/sdk-react',
|
|
||||||
'@telegram-apps/react-router-integration',
|
|
||||||
],
|
|
||||||
'vendor-utils': [
|
'vendor-utils': [
|
||||||
'axios',
|
'axios',
|
||||||
'zustand',
|
'zustand',
|
||||||
|
|||||||
Reference in New Issue
Block a user