✨ Setup Wizard: пошаговая настройка после активации подписки
- Выбор устройства (💻 Компьютер / 📱 Телефон) - Проверка наличия приложения - Выбор локаций для Extended тарифа (3 из списка) - Показ ссылки/QR кода - Автоматическое открытие после активации Trial - Прогресс бар для отслеживания шагов
This commit is contained in:
432
components/SetupWizard.tsx
Normal file
432
components/SetupWizard.tsx
Normal file
@@ -0,0 +1,432 @@
|
||||
// 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user