Merge pull request #152 from BEDOLAGA-DEV/feat/crypto-links-connection-page

Feat/crypto links connection page
This commit is contained in:
c0mrade
2026-02-05 22:23:50 +03:00
committed by GitHub
33 changed files with 1396 additions and 2596 deletions

File diff suppressed because it is too large Load Diff

148
src/pages/Connection.tsx Normal file
View File

@@ -0,0 +1,148 @@
import { useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
import { subscriptionApi } from '../api/subscription';
import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
import { useBackButton, useHaptic } from '@/platform';
import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
import { useAuthStore } from '../store/auth';
import type { AppConfig, LocalizedText } from '../types';
import InstallationGuide from '../components/connection/InstallationGuide';
export default function Connection() {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { user } = useAuthStore();
const { isTelegramWebApp } = useTelegramWebApp();
const { impact: hapticImpact } = useHaptic();
const hapticRef = useRef(hapticImpact);
hapticRef.current = hapticImpact;
const {
data: appConfig,
isLoading,
error,
} = useQuery<AppConfig>({
queryKey: ['appConfig'],
queryFn: () => subscriptionApi.getAppConfig(),
});
const handleGoBack = useCallback(() => {
navigate(-1);
}, [navigate]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
handleGoBack();
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleGoBack]);
const handleBackButton = useCallback(() => {
hapticRef.current('light');
handleGoBack();
}, [handleGoBack]);
useBackButton(handleBackButton);
const resolveUrl = useCallback(
(url: string): string => {
if (!hasTemplates(url) || !appConfig?.subscriptionUrl) return url;
return resolveTemplate(url, {
subscriptionUrl: appConfig.subscriptionUrl,
username: user?.username ?? undefined,
});
},
[appConfig?.subscriptionUrl, user?.username],
);
const openDeepLink = useCallback(
(deepLink: string) => {
let resolved = deepLink;
if (hasTemplates(resolved)) {
resolved = resolveUrl(resolved);
}
const finalUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(resolved)}&lang=${i18n.language || 'en'}`;
if (isTelegramWebApp) {
try {
sdkOpenLink(finalUrl, { tryInstantView: false });
return;
} catch {
// SDK not available, fallback
}
}
window.location.href = finalUrl;
},
[isTelegramWebApp, i18n.language, resolveUrl],
);
const getLocalizedText = (text: LocalizedText | undefined): string => {
if (!text) return '';
const lang = i18n.language || 'en';
return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || '';
};
const getBaseTranslation = (key: string, i18nKey: string): string => {
const bt = appConfig?.baseTranslations;
if (bt && key in bt) {
const text = getLocalizedText(bt[key as keyof typeof bt] as LocalizedText);
if (text) return text;
}
return t(i18nKey);
};
// Loading
if (isLoading) {
return (
<div className="flex flex-1 items-center justify-center py-20">
<div className="h-10 w-10 animate-spin rounded-full border-[3px] border-accent-500/30 border-t-accent-500" />
</div>
);
}
// Error
if (error || !appConfig) {
return (
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
<p className="mb-4 text-lg text-dark-300">{t('common.error')}</p>
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
{getBaseTranslation('close', 'common.close')}
</button>
</div>
);
}
// No subscription
if (!appConfig.hasSubscription) {
return (
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
<h3 className="mb-2 text-xl font-bold text-dark-100">
{t('subscription.connection.title')}
</h3>
<p className="mb-4 text-dark-400">{t('subscription.connection.noSubscription')}</p>
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
{getBaseTranslation('close', 'common.close')}
</button>
</div>
);
}
return (
<InstallationGuide
appConfig={appConfig}
onOpenDeepLink={openDeepLink}
isTelegramWebApp={isTelegramWebApp}
onGoBack={handleGoBack}
/>
);
}

View File

@@ -1,13 +1,12 @@
import { useState, useEffect, useMemo, useRef } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/auth';
import { subscriptionApi } from '../api/subscription';
import { referralApi } from '../api/referral';
import { balanceApi } from '../api/balance';
import { wheelApi } from '../api/wheel';
import ConnectionModal from '../components/ConnectionModal';
import Onboarding, { useOnboarding } from '../components/Onboarding';
import PromoOffersSection from '../components/PromoOffersSection';
import { useCurrency } from '../hooks/useCurrency';
@@ -49,9 +48,9 @@ export default function Dashboard() {
const { t } = useTranslation();
const { user, refreshUser } = useAuthStore();
const queryClient = useQueryClient();
const navigate = useNavigate();
const { formatAmount, currencySymbol, formatPositive } = useCurrency();
const [trialError, setTrialError] = useState<string | null>(null);
const [showConnectionModal, setShowConnectionModal] = useState(false);
const { isCompleted: isOnboardingCompleted, complete: completeOnboarding } = useOnboarding();
const [showOnboarding, setShowOnboarding] = useState(false);
@@ -410,7 +409,7 @@ export default function Dashboard() {
</Link>
{subscription.subscription_url && (
<button
onClick={() => setShowConnectionModal(true)}
onClick={() => navigate('/connection')}
className="btn-secondary py-2.5 text-sm"
data-onboarding="connect-devices"
>
@@ -635,9 +634,6 @@ export default function Dashboard() {
</Link>
)}
{/* Connection Modal */}
{showConnectionModal && <ConnectionModal onClose={() => setShowConnectionModal(false)} />}
{/* Onboarding Tutorial */}
{showOnboarding && (
<Onboarding

View File

@@ -1,7 +1,7 @@
import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-router-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { AxiosError } from 'axios';
import { subscriptionApi } from '../api/subscription';
import { promoApi } from '../api/promo';
@@ -12,7 +12,6 @@ import type {
TariffPeriod,
ClassicPurchaseOptions,
} from '../types';
import ConnectionModal from '../components/ConnectionModal';
import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt';
import { useCurrency } from '../hooks/useCurrency';
import { useCloseOnSuccessNotification } from '../store/successNotification';
@@ -83,9 +82,9 @@ export default function Subscription() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const location = useLocation();
const navigate = useNavigate();
const { formatAmount, currencySymbol } = useCurrency();
const [copied, setCopied] = useState(false);
const [showConnectionModal, setShowConnectionModal] = useState(false);
// Helper to format price from kopeks
const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`;
@@ -309,7 +308,6 @@ export default function Subscription() {
// Auto-close all modals/forms when success notification appears (e.g., subscription purchased via WebSocket)
const handleCloseAllModals = useCallback(() => {
setShowConnectionModal(false);
setShowPurchaseForm(false);
setShowTariffPurchase(false);
setShowDeviceTopup(false);
@@ -737,7 +735,7 @@ export default function Subscription() {
{/* Get Config Button */}
<button
onClick={() => setShowConnectionModal(true)}
onClick={() => navigate('/connection')}
className="btn-primary mb-3 flex w-full items-center justify-center gap-2 py-3"
>
<svg
@@ -1135,7 +1133,7 @@ export default function Subscription() {
{!showDeviceTopup ? (
<button
onClick={() => setShowDeviceTopup(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 p-4 text-left transition-colors hover:border-dark-600"
>
<div className="flex items-center justify-between">
<div>
@@ -1160,7 +1158,7 @@ export default function Subscription() {
</div>
</button>
) : (
<div className="rounded-xl border border-dark-700/50 bg-dark-800/30 p-5">
<div className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-5">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-medium text-dark-100">{t('subscription.buyDevices')}</h3>
<button
@@ -1327,7 +1325,7 @@ export default function Subscription() {
{!showDeviceReduction ? (
<button
onClick={() => setShowDeviceReduction(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 p-4 text-left transition-colors hover:border-dark-600"
>
<div className="flex items-center justify-between">
<div>
@@ -1350,7 +1348,7 @@ export default function Subscription() {
</div>
</button>
) : (
<div className="rounded-xl border border-dark-700/50 bg-dark-800/30 p-5">
<div className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-5">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-medium text-dark-100">
{t('subscription.additionalOptions.reduceDevicesTitle')}
@@ -1496,7 +1494,7 @@ export default function Subscription() {
{!showTrafficTopup ? (
<button
onClick={() => setShowTrafficTopup(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 p-4 text-left transition-colors hover:border-dark-600"
>
<div className="flex items-center justify-between">
<div>
@@ -1522,7 +1520,7 @@ export default function Subscription() {
</div>
</button>
) : (
<div className="rounded-xl border border-dark-700/50 bg-dark-800/30 p-5">
<div className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-5">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-medium text-dark-100">
{t('subscription.additionalOptions.buyTrafficTitle')}
@@ -1556,7 +1554,7 @@ export default function Subscription() {
className={`rounded-xl border p-4 text-center transition-all ${
selectedTrafficPackage === pkg.gb
? 'border-accent-500 bg-accent-500/10'
: 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
: 'border-dark-700/50 bg-dark-800/50 hover:border-dark-600'
}`}
>
<div className="text-lg font-semibold text-dark-100">
@@ -1656,7 +1654,7 @@ export default function Subscription() {
{!showServerManagement ? (
<button
onClick={() => setShowServerManagement(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 p-4 text-left transition-colors hover:border-dark-600"
>
<div className="flex items-center justify-between">
<div>
@@ -1679,7 +1677,7 @@ export default function Subscription() {
</div>
</button>
) : (
<div className="rounded-xl border border-dark-700/50 bg-dark-800/30 p-5">
<div className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-5">
<div className="mb-4 flex items-center justify-between">
<h3 className="font-medium text-dark-100">
{t('subscription.additionalOptions.manageServersTitle')}
@@ -1743,7 +1741,7 @@ export default function Subscription() {
: 'border-accent-500 bg-accent-500/10'
: willBeRemoved
? 'border-error-500/50 bg-error-500/5'
: 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
: 'border-dark-700/50 bg-dark-800/50 hover:border-dark-600'
} ${!country.is_available && !isCurrentlyConnected ? 'cursor-not-allowed opacity-50' : ''}`}
>
<div className="flex items-center gap-3">
@@ -1956,7 +1954,7 @@ export default function Subscription() {
{devicesData.devices.map((device) => (
<div
key={device.hwid}
className="flex items-center justify-between rounded-xl border border-dark-700/50 bg-dark-800/30 p-4"
className="flex items-center justify-between rounded-xl border border-dark-700/50 bg-dark-800/50 p-4"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-dark-700">
@@ -2577,7 +2575,7 @@ export default function Subscription() {
</div>
{/* Tariff Info */}
<div className="rounded-xl bg-dark-800/30 p-4">
<div className="rounded-xl bg-dark-800/50 p-4">
<div className="flex flex-wrap gap-4 text-sm">
<div>
<span className="text-dark-500">{t('subscription.traffic')}:</span>
@@ -2741,7 +2739,7 @@ export default function Subscription() {
className={`relative rounded-xl border p-4 text-left transition-all ${
selectedTariffPeriod?.days === period.days && !useCustomDays
? 'border-accent-500 bg-accent-500/10'
: 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
: 'border-dark-700/50 bg-dark-800/50 hover:border-dark-600'
}`}
>
{displayDiscount && displayDiscount > 0 && (
@@ -2778,7 +2776,7 @@ export default function Subscription() {
{/* Custom days option */}
{selectedTariff.custom_days_enabled &&
(selectedTariff.price_per_day_kopeks ?? 0) > 0 && (
<div className="rounded-xl border border-dark-700/50 bg-dark-800/30 p-4">
<div className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="font-medium text-dark-200">
{t('subscription.customDays.title')}
@@ -2878,7 +2876,7 @@ export default function Subscription() {
<div className="mb-3 text-sm text-dark-400">
{t('subscription.customTraffic.label')}
</div>
<div className="rounded-xl border border-dark-700/50 bg-dark-800/30 p-4">
<div className="rounded-xl border border-dark-700/50 bg-dark-800/50 p-4">
<div className="mb-3 flex items-center justify-between">
<span className="font-medium text-dark-200">
{t('subscription.customTraffic.selectVolume')}
@@ -3349,7 +3347,7 @@ export default function Subscription() {
selectedServers.includes(server.uuid)
? 'border-accent-500 bg-accent-500/10'
: server.is_available
? 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
? 'border-dark-700/50 bg-dark-800/50 hover:border-dark-600'
: 'cursor-not-allowed border-dark-800/30 bg-dark-900/30 opacity-50'
}`}
>
@@ -3592,9 +3590,6 @@ export default function Subscription() {
)}
</div>
)}
{/* Connection Modal */}
{showConnectionModal && <ConnectionModal onClose={() => setShowConnectionModal(false)} />}
</div>
);
}