Files
app_umbrix/components/SetupWizard.tsx
Umbrix Dev 6cb9335955 Setup Wizard: пошаговая настройка после активации подписки
- Выбор устройства (💻 Компьютер / 📱 Телефон)
- Проверка наличия приложения
- Выбор локаций для Extended тарифа (3 из списка)
- Показ ссылки/QR кода
- Автоматическое открытие после активации Trial
- Прогресс бар для отслеживания шагов
2026-02-05 12:11:55 +03:00

433 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Setup Wizard - пошаговая настройка после создания подписки
'use client';
import { useState } from 'react';
import { X, ChevronLeft, Laptop, Smartphone, Check, Copy, QrCode } from 'lucide-react';
interface SetupWizardProps {
isOpen: boolean;
onClose: () => void;
subscriptionUrl: string;
username: string;
planType: 'trial' | 'basic' | 'extended' | 'premium';
expiryDate?: string;
}
type Step = 'device' | 'app-check' | 'location' | 'final';
type DeviceType = 'desktop' | 'mobile' | null;
type MobileOS = 'android' | 'ios' | null;
export default function SetupWizard({
isOpen,
onClose,
subscriptionUrl,
username,
planType,
expiryDate
}: SetupWizardProps) {
const [step, setStep] = useState<Step>('device');
const [deviceType, setDeviceType] = useState<DeviceType>(null);
const [mobileOS, setMobileOS] = useState<MobileOS>(null);
const [hasApp, setHasApp] = useState<boolean | null>(null);
const [selectedLocations, setSelectedLocations] = useState<string[]>([]);
const [copied, setCopied] = useState(false);
const [showQR, setShowQR] = useState(false);
// Локации для выбора (для тарифа "Расширенный")
const locations = [
{ id: 'nl', name: '🇳🇱 Нидерланды', ping: '15ms' },
{ id: 'de', name: '🇩🇪 Германия', ping: '20ms' },
{ id: 'us', name: '🇺🇸 США', ping: '120ms' },
{ id: 'sg', name: '🇸🇬 Сингапур', ping: '180ms' },
{ id: 'jp', name: '🇯🇵 Япония', ping: '160ms' },
{ id: 'uk', name: '🇬🇧 Великобритания', ping: '35ms' },
];
const needsLocationSelection = planType === 'extended';
const maxLocations = planType === 'extended' ? 3 : 1;
const handleCopy = async () => {
await navigator.clipboard.writeText(subscriptionUrl);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const handleDeviceSelect = (type: DeviceType) => {
setDeviceType(type);
if (type === 'desktop') {
setStep('app-check');
} else {
setStep('app-check');
}
};
const handleAppCheck = (installed: boolean) => {
setHasApp(installed);
if (needsLocationSelection) {
setStep('location');
} else {
setStep('final');
}
};
const handleLocationToggle = (locationId: string) => {
if (selectedLocations.includes(locationId)) {
setSelectedLocations(selectedLocations.filter(id => id !== locationId));
} else if (selectedLocations.length < maxLocations) {
setSelectedLocations([...selectedLocations, locationId]);
}
};
const canProceedFromLocation = selectedLocations.length === maxLocations;
const getProgressSteps = () => {
const steps = ['device', 'app-check'];
if (needsLocationSelection) steps.push('location');
steps.push('final');
return steps;
};
const currentStepIndex = getProgressSteps().indexOf(step);
const totalSteps = getProgressSteps().length;
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-gradient-to-b from-slate-800 to-slate-900 rounded-2xl max-w-md w-full relative border border-slate-700 shadow-2xl">
{/* Header */}
<div className="p-6 border-b border-slate-700">
<div className="flex items-center justify-between mb-4">
<button
onClick={() => {
if (step === 'device') {
onClose();
} else if (step === 'app-check') {
setStep('device');
} else if (step === 'location') {
setStep('app-check');
} else if (step === 'final') {
if (needsLocationSelection) {
setStep('location');
} else {
setStep('app-check');
}
}
}}
className="text-slate-400 hover:text-white transition-colors"
>
{step === 'device' ? <X className="h-6 w-6" /> : <ChevronLeft className="h-6 w-6" />}
</button>
<h2 className="text-xl font-bold text-white">
{step === 'device' && '🎉 Подписка создана!'}
{step === 'app-check' && deviceType === 'desktop' && '💻 Настройка'}
{step === 'app-check' && deviceType === 'mobile' && '📱 Настройка'}
{step === 'location' && '🌍 Выбор локаций'}
{step === 'final' && '✨ Всё готово!'}
</h2>
<div className="w-6" />
</div>
{/* Progress bar */}
<div className="flex gap-2">
{Array.from({ length: totalSteps }).map((_, i) => (
<div
key={i}
className={`h-1 flex-1 rounded-full transition-colors ${
i <= currentStepIndex ? 'bg-blue-500' : 'bg-slate-700'
}`}
/>
))}
</div>
</div>
{/* Content */}
<div className="p-6">
{/* Step 1: Device Selection */}
{step === 'device' && (
<div className="space-y-4">
<p className="text-slate-300 text-center mb-6">
{expiryDate ? `Действует до ${expiryDate}` : 'Осталось 2 шага до подключения'}
</p>
<p className="text-white font-medium text-center mb-4">
Какое у вас устройство?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleDeviceSelect('desktop')}
className="p-6 bg-slate-800/50 hover:bg-slate-700/50 border-2 border-slate-700 hover:border-blue-500 rounded-xl transition-all group"
>
<Laptop className="h-12 w-12 mx-auto mb-3 text-blue-500 group-hover:scale-110 transition-transform" />
<p className="text-white font-medium">Компьютер</p>
</button>
<button
onClick={() => handleDeviceSelect('mobile')}
className="p-6 bg-slate-800/50 hover:bg-slate-700/50 border-2 border-slate-700 hover:border-blue-500 rounded-xl transition-all group"
>
<Smartphone className="h-12 w-12 mx-auto mb-3 text-blue-500 group-hover:scale-110 transition-transform" />
<p className="text-white font-medium">Телефон</p>
</button>
</div>
</div>
)}
{/* Step 2: App Check */}
{step === 'app-check' && deviceType === 'desktop' && (
<div className="space-y-4">
<p className="text-white font-medium text-center mb-4">
У вас установлен Umbrix?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleAppCheck(true)}
className="p-6 bg-slate-800/50 hover:bg-slate-700/50 border-2 border-slate-700 hover:border-green-500 rounded-xl transition-all"
>
<Check className="h-12 w-12 mx-auto mb-3 text-green-500" />
<p className="text-white font-medium">Да</p>
</button>
<button
onClick={() => handleAppCheck(false)}
className="p-6 bg-slate-800/50 hover:bg-slate-700/50 border-2 border-slate-700 hover:border-red-500 rounded-xl transition-all"
>
<X className="h-12 w-12 mx-auto mb-3 text-red-500" />
<p className="text-white font-medium">Нет</p>
</button>
</div>
{hasApp === false && (
<div className="mt-6 p-4 bg-blue-500/10 border border-blue-500/30 rounded-xl">
<p className="text-white font-medium mb-3">Скачайте Umbrix:</p>
<div className="flex flex-col gap-2">
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white font-medium transition-colors text-center"
>
🪟 Windows
</a>
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white font-medium transition-colors text-center"
>
🍎 macOS
</a>
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white font-medium transition-colors text-center"
>
🐧 Linux
</a>
</div>
<button
onClick={() => setStep(needsLocationSelection ? 'location' : 'final')}
className="w-full mt-4 px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg text-white font-medium transition-colors"
>
Уже установил, далее
</button>
</div>
)}
</div>
)}
{step === 'app-check' && deviceType === 'mobile' && (
<div className="space-y-4">
<p className="text-white font-medium text-center mb-4">
Какая у вас система?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => {
setMobileOS('android');
setHasApp(null);
}}
className="p-6 bg-slate-800/50 hover:bg-slate-700/50 border-2 border-slate-700 hover:border-green-500 rounded-xl transition-all"
>
<div className="text-4xl mb-3">🤖</div>
<p className="text-white font-medium">Android</p>
</button>
<button
onClick={() => {
setMobileOS('ios');
setHasApp(null);
}}
className="p-6 bg-slate-800/50 hover:bg-slate-700/50 border-2 border-slate-700 hover:border-blue-500 rounded-xl transition-all"
>
<div className="text-4xl mb-3">🍎</div>
<p className="text-white font-medium">iOS</p>
</button>
</div>
{mobileOS && (
<div className="mt-6">
<p className="text-white font-medium mb-3">У вас есть приложение?</p>
<div className="grid grid-cols-2 gap-4 mb-4">
<button
onClick={() => handleAppCheck(true)}
className="px-4 py-3 bg-green-600 hover:bg-green-700 rounded-lg text-white font-medium transition-colors"
>
Да
</button>
<button
onClick={() => handleAppCheck(false)}
className="px-4 py-3 bg-slate-700 hover:bg-slate-600 rounded-lg text-white font-medium transition-colors"
>
Нет
</button>
</div>
{hasApp === false && (
<div className="p-4 bg-blue-500/10 border border-blue-500/30 rounded-xl">
<p className="text-slate-300 text-sm mb-2">Рекомендуем:</p>
{mobileOS === 'android' ? (
<div className="space-y-2">
<div className="text-white"> V2RayNG</div>
<div className="text-white"> Hiddify</div>
<div className="text-white"> v2rayTun</div>
</div>
) : (
<div className="space-y-2">
<div className="text-white"> Shadowrocket (AppStore)</div>
<div className="text-white"> Hiddify (AppStore)</div>
</div>
)}
<button
onClick={() => setStep(needsLocationSelection ? 'location' : 'final')}
className="w-full mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg text-white font-medium transition-colors"
>
Уже установил, далее
</button>
</div>
)}
</div>
)}
</div>
)}
{/* Step 3: Location Selection (только для extended тарифа) */}
{step === 'location' && (
<div className="space-y-4">
<p className="text-white font-medium text-center mb-2">
Выберите {maxLocations} локации
</p>
<p className="text-slate-400 text-sm text-center mb-4">
Выбрано: {selectedLocations.length} из {maxLocations}
</p>
<div className="space-y-2 max-h-[300px] overflow-y-auto">
{locations.map((location) => {
const isSelected = selectedLocations.includes(location.id);
const canSelect = selectedLocations.length < maxLocations;
return (
<button
key={location.id}
onClick={() => handleLocationToggle(location.id)}
disabled={!isSelected && !canSelect}
className={`w-full p-4 rounded-xl border-2 transition-all flex items-center justify-between ${
isSelected
? 'bg-blue-600/20 border-blue-500'
: canSelect
? 'bg-slate-800/50 border-slate-700 hover:border-slate-600'
: 'bg-slate-800/30 border-slate-700/50 opacity-50 cursor-not-allowed'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
isSelected ? 'bg-blue-500 border-blue-500' : 'border-slate-600'
}`}>
{isSelected && <Check className="h-4 w-4 text-white" />}
</div>
<span className="text-white font-medium">{location.name}</span>
</div>
<span className="text-slate-400 text-sm">{location.ping}</span>
</button>
);
})}
</div>
<button
onClick={() => setStep('final')}
disabled={!canProceedFromLocation}
className={`w-full py-3 rounded-xl font-medium transition-colors ${
canProceedFromLocation
? 'bg-blue-600 hover:bg-blue-700 text-white'
: 'bg-slate-700 text-slate-500 cursor-not-allowed'
}`}
>
{canProceedFromLocation ? '➡️ Продолжить' : `Выберите ещё ${maxLocations - selectedLocations.length}`}
</button>
</div>
)}
{/* Step 4: Final - Show Link/QR */}
{step === 'final' && (
<div className="space-y-4">
<div className="text-center mb-6">
<div className="text-4xl mb-3">🎉</div>
<p className="text-slate-300 text-sm">
{deviceType === 'desktop' ? 'Скопируйте ссылку и вставьте в Umbrix' : 'Отсканируйте QR код или скопируйте ссылку'}
</p>
</div>
<button
onClick={handleCopy}
className="w-full p-4 bg-slate-800/50 border-2 border-slate-700 hover:border-blue-500 rounded-xl transition-all group"
>
<div className="flex items-center justify-between">
<span className="text-white font-medium">
{copied ? '✅ Скопировано!' : '📋 Скопировать ссылку'}
</span>
<Copy className={`h-5 w-5 transition-colors ${copied ? 'text-green-500' : 'text-slate-400 group-hover:text-blue-500'}`} />
</div>
</button>
{deviceType === 'mobile' && (
<button
onClick={() => setShowQR(!showQR)}
className="w-full p-4 bg-slate-800/50 border-2 border-slate-700 hover:border-blue-500 rounded-xl transition-all group"
>
<div className="flex items-center justify-between">
<span className="text-white font-medium">
{showQR ? '📱 Скрыть QR код' : '📱 Показать QR код'}
</span>
<QrCode className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors" />
</div>
</button>
)}
{showQR && (
<div className="p-6 bg-white rounded-xl">
<div className="text-center text-slate-800 text-sm mb-2">QR код</div>
<div className="text-xs text-slate-600 break-all">{subscriptionUrl}</div>
</div>
)}
<div className="border-t border-slate-700 pt-4 mt-4">
<button
onClick={onClose}
className="w-full p-4 bg-slate-700 hover:bg-slate-600 rounded-xl transition-colors text-white font-medium"
>
📖 Нужна помощь? Открыть инструкцию
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}