Files
app_umbrix/components/SetupWizard.tsx

509 lines
24 KiB
TypeScript
Raw Normal View History

// Setup Wizard - пошаговая настройка после создания подписки
'use client';
import { useState, useEffect } 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 [qrDataUrl, setQrDataUrl] = useState<string>('');
// Генерация QR кода при открытии финального шага
useEffect(() => {
if (step === 'final' && subscriptionUrl && !qrDataUrl) {
import('qrcode').then((QRCode) => {
QRCode.toDataURL(subscriptionUrl, {
width: 300,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF',
},
errorCorrectionLevel: 'H',
})
.then((dataUrl) => {
setQrDataUrl(dataUrl);
})
.catch((err) => {
console.error('Failed to generate QR code:', err);
});
});
}
}, [step, subscriptionUrl, qrDataUrl]);
// Локации для выбора (для тарифа "Расширенный")
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/90 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="rounded-2xl max-w-md w-full relative border shadow-2xl" style={{ background: 'var(--bg-card)', borderColor: 'var(--border)' }}>
{/* Header */}
<div className="p-6 border-b" style={{ borderColor: 'var(--border)' }}>
<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="transition-colors"
style={{ color: 'var(--text-primary)' }}
>
{step === 'device' ? <X className="h-6 w-6" /> : <ChevronLeft className="h-6 w-6" />}
</button>
<h2 className="text-xl font-bold" style={{ color: 'var(--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"
style={{
backgroundColor: i <= currentStepIndex ? 'var(--primary)' : 'var(--border)'
}}
/>
))}
</div>
</div>
{/* Content */}
<div className="p-6">
{/* Step 1: Device Selection */}
{step === 'device' && (
<div className="space-y-4">
<p className="text-center mb-6" style={{ color: 'var(--text-primary)' }}>
{expiryDate ? `Действует до ${expiryDate}` : 'Осталось 2 шага до подключения'}
</p>
<p className="font-medium text-center mb-4" style={{ color: 'var(--text-white)' }}>
Какое у вас устройство?
</p>
<div className="grid grid-cols-2 gap-4">
<button
onClick={() => handleDeviceSelect('desktop')}
className="p-6 rounded-xl transition-all group border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<Laptop className="h-12 w-12 mx-auto mb-3 group-hover:scale-110 transition-transform" style={{ color: 'var(--primary)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Компьютер</p>
</button>
<button
onClick={() => handleDeviceSelect('mobile')}
className="p-6 rounded-xl transition-all group border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<Smartphone className="h-12 w-12 mx-auto mb-3 group-hover:scale-110 transition-transform" style={{ color: 'var(--primary)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Телефон</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 rounded-xl transition-all border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<Check className="h-12 w-12 mx-auto mb-3" style={{ color: 'var(--success)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Да</p>
</button>
<button
onClick={() => handleAppCheck(false)}
className="p-6 rounded-xl transition-all border-2"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
>
<X className="h-12 w-12 mx-auto mb-3" style={{ color: 'var(--error)' }} />
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Нет</p>
</button>
</div>
{hasApp === false && (
<div className="mt-6 p-4 rounded-xl" style={{ background: 'rgba(47, 190, 165, 0.1)', borderWidth: '1px', borderStyle: 'solid', borderColor: 'var(--primary)' }}>
<p style={{ color: 'var(--text-white)' }} className="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 rounded-lg font-medium transition-colors text-center"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
🪟 Windows
</a>
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg font-medium transition-colors text-center"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
🍎 macOS
</a>
<a
href="https://umbrix.net/download"
target="_blank"
rel="noopener noreferrer"
className="px-4 py-2 rounded-lg font-medium transition-colors text-center"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
🐧 Linux
</a>
</div>
<button
onClick={() => setStep(needsLocationSelection ? 'location' : 'final')}
className="w-full mt-4 px-4 py-2 rounded-lg font-medium transition-colors"
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--border)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
>
Уже установил, далее
</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 rounded-xl border-2 transition-all"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--success)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="text-4xl mb-3">🤖</div>
<p className="font-medium" style={{ color: 'var(--text-white)' }}>Android</p>
</button>
<button
onClick={() => {
setMobileOS('ios');
setHasApp(null);
}}
className="p-6 rounded-xl border-2 transition-all"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--text-white)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="text-4xl mb-3">🍎</div>
<p className="font-medium" style={{ color: 'var(--text-white)' }}>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={() => setHasApp(false)}
className="px-4 py-3 rounded-lg font-medium transition-colors"
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--border)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
>
Нет
</button>
</div>
{hasApp === false && (
<div className="p-4 rounded-xl" style={{ background: 'rgba(47, 190, 165, 0.1)', borderWidth: '1px', borderStyle: 'solid', borderColor: 'var(--primary)' }}>
<p className="text-sm mb-2" style={{ color: 'var(--text-primary)' }}>Рекомендуем:</p>
{mobileOS === 'android' ? (
<div className="space-y-2">
<div style={{ color: 'var(--text-white)' }}> V2RayNG</div>
<div style={{ color: 'var(--text-white)' }}> Hiddify</div>
<div style={{ color: 'var(--text-white)' }}> v2rayTun</div>
</div>
) : (
<div className="space-y-2">
<div style={{ color: 'var(--text-white)' }}> Shadowrocket (AppStore)</div>
<div style={{ color: 'var(--text-white)' }}> Hiddify (AppStore)</div>
</div>
)}
<button
onClick={() => handleAppCheck(true)}
className="w-full mt-4 px-4 py-2 rounded-lg font-medium transition-colors"
style={{ background: 'var(--primary)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--primary-dark)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--primary)'}
>
Уже установил, далее
</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"
style={{
background: isSelected ? 'rgba(47, 190, 165, 0.2)' : canSelect ? 'var(--bg-elevated)' : 'var(--bg-card)',
borderColor: isSelected ? 'var(--primary)' : 'var(--border)',
opacity: canSelect || isSelected ? 1 : 0.5,
cursor: canSelect || isSelected ? 'pointer' : 'not-allowed'
}}
onMouseEnter={e => { if (canSelect && !isSelected) e.currentTarget.style.borderColor = 'var(--text-primary)' }}
onMouseLeave={e => { if (!isSelected) e.currentTarget.style.borderColor = 'var(--border)' }}
>
<div className="flex items-center gap-3">
<div className="w-5 h-5 rounded border-2 flex items-center justify-center" style={{
background: isSelected ? 'var(--primary)' : 'transparent',
borderColor: isSelected ? 'var(--primary)' : 'var(--border)'
}}>
{isSelected && <Check className="h-4 w-4" style={{ color: 'var(--text-white)' }} />}
</div>
<span className="font-medium" style={{ color: 'var(--text-white)' }}>{location.name}</span>
</div>
<span className="text-sm" style={{ color: 'var(--text-primary)' }}>{location.ping}</span>
</button>
);
})}
</div>
<button
onClick={() => setStep('final')}
disabled={!canProceedFromLocation}
className="w-full py-3 rounded-xl font-medium transition-colors"
style={{
background: canProceedFromLocation ? 'var(--primary)' : 'var(--bg-elevated)',
color: canProceedFromLocation ? 'var(--text-white)' : 'var(--text-primary)',
cursor: canProceedFromLocation ? 'pointer' : 'not-allowed',
opacity: canProceedFromLocation ? 1 : 0.5
}}
onMouseEnter={e => { if (canProceedFromLocation) e.currentTarget.style.background = 'var(--primary-dark)' }}
onMouseLeave={e => { if (canProceedFromLocation) e.currentTarget.style.background = 'var(--primary)' }}
>
{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-sm" style={{ color: 'var(--text-primary)' }}>
{deviceType === 'desktop' ? 'Скопируйте ссылку и вставьте в Umbrix' : 'Отсканируйте QR код или скопируйте ссылку'}
</p>
</div>
<button
onClick={handleCopy}
className="w-full p-4 rounded-xl border-2 transition-all group"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="flex items-center justify-between">
<span className="font-medium" style={{ color: 'var(--text-white)' }}>
{copied ? '✅ Скопировано!' : '📋 Скопировать ссылку'}
</span>
<Copy className="h-5 w-5 transition-colors" style={{ color: copied ? 'var(--success)' : 'var(--text-primary)' }} />
</div>
</button>
{deviceType === 'mobile' && (
<button
onClick={() => setShowQR(!showQR)}
className="w-full p-4 rounded-xl border-2 transition-all group"
style={{ background: 'var(--bg-elevated)', borderColor: 'var(--border)' }}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--primary)'}
onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--border)'}
>
<div className="flex items-center justify-between">
<span className="font-medium" style={{ color: 'var(--text-white)' }}>
{showQR ? '📱 Скрыть QR код' : '📱 Показать QR код'}
</span>
<QrCode className="h-5 w-5 transition-colors" style={{ color: 'var(--text-primary)' }} />
</div>
</button>
)}
{showQR && qrDataUrl && (
<div className="p-6 rounded-xl" style={{ background: 'var(--text-white)' }}>
<img
src={qrDataUrl}
alt="QR код подписки"
className="w-full h-auto"
/>
<div className="mt-3 text-center text-xs break-all" style={{ color: 'var(--bg-card)' }}>
{subscriptionUrl}
</div>
</div>
)}
<div className="pt-4 mt-4" style={{ borderTop: '1px solid var(--border)' }}>
<button
onClick={onClose}
className="w-full p-4 rounded-xl transition-colors font-medium"
style={{ background: 'var(--bg-elevated)', color: 'var(--text-white)' }}
onMouseEnter={e => e.currentTarget.style.background = 'var(--border)'}
onMouseLeave={e => e.currentTarget.style.background = 'var(--bg-elevated)'}
>
📖 Нужна помощь? Открыть инструкцию
</button>
</div>
</div>
)}
</div>
</div>
</div>
);
}