Files
app_umbrix/components/OnboardingFlow.tsx

359 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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.
// OnboardingFlow.tsx - Полный onboarding с интеграцией Telegram WebApp SDK
// Steps: Welcome → Language → Plan → Create Account
// Uses: Telegram.WebApp.MainButton, .BackButton, .themeParams
'use client';
import { useState, useEffect } from 'react';
import { CheckCircle, Globe, Shield, Zap, DollarSign } from 'lucide-react';
// Типы
type OnboardingStep = 'welcome' | 'language' | 'plan' | 'creating';
interface Plan {
id: string;
name: string;
price: number;
features: string[];
icon: any;
}
interface OnboardingFlowProps {
referrerId?: string; // From URL ?ref=username
onComplete: (username: string, token: string) => void;
}
const PLANS: Plan[] = [
{
id: 'basic',
name: 'Старт',
price: 99,
features: ['Безлимитный трафик', 'До 3 устройств', 'Стандартная скорость', 'Email поддержка'],
icon: Shield,
},
{
id: 'plus',
name: 'Плюс',
price: 249,
features: ['Безлимитный трафик', 'До 5 устройств', 'Высокая скорость', 'Выбор локации', 'Приоритетная поддержка'],
icon: Zap,
},
{
id: 'max',
name: 'Макс',
price: 350,
features: ['Безлимитный трафик', 'До 10 устройств', 'Максимальная скорость', 'Все локации', '24/7 поддержка'],
icon: DollarSign,
},
];
const LANGUAGES = [
{ code: 'ru', name: 'Русский', flag: '🇷🇺' },
{ code: 'en', name: 'English', flag: '🇬🇧' },
{ code: 'uz', name: 'Oʻzbekcha', flag: '🇺🇿' },
];
export default function OnboardingFlow({ referrerId, onComplete }: OnboardingFlowProps) {
const [step, setStep] = useState<OnboardingStep>('welcome');
const [selectedLanguage, setSelectedLanguage] = useState('ru');
const [selectedPlan, setSelectedPlan] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
// Telegram WebApp SDK
const [telegramWebApp, setTelegramWebApp] = useState<any>(null);
const [telegramUser, setTelegramUser] = useState<any>(null);
useEffect(() => {
// Initialize Telegram WebApp SDK
if (typeof window !== 'undefined') {
const tg = (window as any).Telegram?.WebApp;
if (tg) {
setTelegramWebApp(tg);
setTelegramUser(tg.initDataUnsafe?.user);
tg.ready();
tg.expand();
// Тема применяется через CSS variables в globals.css
// НЕ переопределяем --tg-theme-* — таких переменных нет в проекте
}
}
}, []);
// Handle MainButton clicks based on current step
useEffect(() => {
if (!telegramWebApp) return;
const handleMainButtonClick = () => {
if (step === 'welcome') {
setStep('language');
} else if (step === 'language' && selectedLanguage) {
setStep('plan');
} else if (step === 'plan' && selectedPlan) {
handleCreateAccount();
}
};
telegramWebApp.MainButton.onClick(handleMainButtonClick);
// Update MainButton based on step
if (step === 'welcome') {
telegramWebApp.MainButton.text = '🚀 Начать';
telegramWebApp.MainButton.show();
telegramWebApp.BackButton.hide();
} else if (step === 'language') {
telegramWebApp.MainButton.text = selectedLanguage ? '➡️ Далее' : '⚠️ Выберите язык';
telegramWebApp.MainButton.show();
telegramWebApp.BackButton.show();
} else if (step === 'plan') {
telegramWebApp.MainButton.text = selectedPlan ? '✅ Создать аккаунт' : '⚠️ Выберите тариф';
telegramWebApp.MainButton.show();
telegramWebApp.BackButton.show();
} else if (step === 'creating') {
telegramWebApp.MainButton.hide();
telegramWebApp.BackButton.hide();
}
return () => {
telegramWebApp.MainButton.offClick(handleMainButtonClick);
};
}, [step, selectedLanguage, selectedPlan, telegramWebApp]);
// Handle BackButton
useEffect(() => {
if (!telegramWebApp) return;
const handleBackButtonClick = () => {
if (step === 'language') {
setStep('welcome');
} else if (step === 'plan') {
setStep('language');
}
};
telegramWebApp.BackButton.onClick(handleBackButtonClick);
return () => {
telegramWebApp.BackButton.offClick(handleBackButtonClick);
};
}, [step, telegramWebApp]);
const handleCreateAccount = async () => {
if (!telegramUser) {
setError('❌ Telegram user data not available');
return;
}
setStep('creating');
setIsCreating(true);
setError(null);
try {
// Генерируем username из Telegram данных
const username = telegramUser.username || `user_${telegramUser.id}`;
// Создаем аккаунт через API
const response = await fetch('/api/create-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username,
telegramId: telegramUser.id,
telegramUsername: telegramUser.username,
firstName: telegramUser.first_name,
lastName: telegramUser.last_name,
plan: selectedPlan,
language: selectedLanguage,
referrerId: referrerId || null,
}),
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to create account');
}
// Track referral if exists
if (referrerId) {
await fetch('/api/referral/track', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: data.username,
referrer_username: referrerId,
}),
});
}
// Success!
onComplete(data.username, data.token);
} catch (err: any) {
console.error('Create account error:', err);
setError(err.message);
setStep('plan'); // Вернуться к выбору тарифа
setIsCreating(false);
}
};
// Render Welcome Step
if (step === 'welcome') {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white p-6 flex flex-col items-center justify-center">
<div className="max-w-md w-full text-center space-y-6">
<div className="text-6xl mb-4">🚀</div>
<h1 className="text-3xl font-bold">Добро пожаловать в Umbrix VPN!</h1>
{referrerId && (
<div className="bg-blue-600/20 border border-blue-500 rounded-lg p-4">
<p className="text-lg">
🎁 Вы перешли по ссылке от <span className="font-bold">{referrerId}</span>!
</p>
<p className="text-sm text-slate-300 mt-2">
Оба получите <span className="font-bold text-green-400">+7 дней бесплатно</span> при регистрации!
</p>
</div>
)}
<div className="space-y-3 text-left">
<div className="flex items-start gap-3">
<CheckCircle className="w-6 h-6 text-green-500 mt-1 flex-shrink-0" />
<div>
<p className="font-semibold">Надежная защита</p>
<p className="text-sm text-slate-400">Ваши данные в безопасности</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle className="w-6 h-6 text-green-500 mt-1 flex-shrink-0" />
<div>
<p className="font-semibold">Высокая скорость</p>
<p className="text-sm text-slate-400">Без ограничений трафика</p>
</div>
</div>
<div className="flex items-start gap-3">
<CheckCircle className="w-6 h-6 text-green-500 mt-1 flex-shrink-0" />
<div>
<p className="font-semibold">Простая настройка</p>
<p className="text-sm text-slate-400">Работает за 2 минуты</p>
</div>
</div>
</div>
<p className="text-sm text-slate-400">
Нажмите <span className="font-bold">&quot;Начать&quot;</span> для создания аккаунта
</p>
</div>
</div>
);
}
// Render Language Selection Step
if (step === 'language') {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white p-6 flex flex-col items-center justify-center">
<div className="max-w-md w-full space-y-6">
<div className="text-center">
<Globe className="w-16 h-16 mx-auto mb-4 text-blue-500" />
<h2 className="text-2xl font-bold">Выберите язык</h2>
<p className="text-slate-400 mt-2">Choose your language</p>
</div>
<div className="space-y-3">
{LANGUAGES.map((lang) => (
<button
key={lang.code}
onClick={() => setSelectedLanguage(lang.code)}
className={`w-full p-4 rounded-lg border-2 transition-all ${
selectedLanguage === lang.code
? 'bg-blue-600 border-blue-500'
: 'bg-slate-800/50 border-slate-700 hover:bg-slate-700'
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-3xl">{lang.flag}</span>
<span className="font-semibold">{lang.name}</span>
</div>
{selectedLanguage === lang.code && (
<CheckCircle className="w-6 h-6 text-white" />
)}
</div>
</button>
))}
</div>
</div>
</div>
);
}
// Render Plan Selection Step
if (step === 'plan') {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white p-6 flex flex-col items-center justify-center">
<div className="max-w-4xl w-full space-y-6">
<div className="text-center">
<h2 className="text-2xl font-bold">Выберите тарифный план</h2>
<p className="text-slate-400 mt-2">7 дней бесплатно на любом тарифе!</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{PLANS.map((plan) => {
const Icon = plan.icon;
return (
<button
key={plan.id}
onClick={() => setSelectedPlan(plan.id)}
className={`p-6 rounded-lg border-2 transition-all text-left ${
selectedPlan === plan.id
? 'bg-blue-600 border-blue-500 scale-105'
: 'bg-slate-800/50 border-slate-700 hover:bg-slate-700'
}`}
>
<div className="flex items-center justify-between mb-4">
<Icon className="w-8 h-8" />
{selectedPlan === plan.id && (
<CheckCircle className="w-6 h-6 text-white" />
)}
</div>
<h3 className="text-xl font-bold mb-2">{plan.name}</h3>
<p className="text-3xl font-bold mb-4">
{plan.price} <span className="text-base font-normal text-slate-400">/мес</span>
</p>
<ul className="space-y-2">
{plan.features.map((feature, i) => (
<li key={i} className="flex items-start gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-400 mt-0.5 flex-shrink-0" />
<span>{feature}</span>
</li>
))}
</ul>
</button>
);
})}
</div>
</div>
</div>
);
}
// Render Creating Account Step
if (step === 'creating') {
return (
<div className="min-h-screen bg-gradient-to-b from-slate-900 via-slate-800 to-slate-900 text-white flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-16 w-16 border-b-4 border-blue-500 mx-auto mb-6"></div>
<h2 className="text-2xl font-bold mb-2">Создаем ваш аккаунт...</h2>
<p className="text-slate-400">Подождите несколько секунд</p>
{error && (
<div className="mt-4 p-4 bg-red-600/20 border border-red-500 rounded-lg">
<p className="text-red-400">{error}</p>
</div>
)}
</div>
</div>
);
}
return null;
}