mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
Merge pull request #152 from BEDOLAGA-DEV/feat/crypto-links-connection-page
Feat/crypto links connection page
This commit is contained in:
File diff suppressed because it is too large
Load Diff
148
src/pages/Connection.tsx
Normal file
148
src/pages/Connection.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user