Files
pwap/web/src/pages/Login.tsx
T
pezkuwichain 9babb94e07 fix(auth): add pexsecBot for Telegram login on app.pezkuwichain.io
- pex.mom uses @PexMomBOT (8690398980)
- app.pezkuwichain.io uses @pexsecBot (8754021997)
- Edge function selects token based on bot_id from request
2026-05-01 23:32:25 +03:00

541 lines
22 KiB
TypeScript

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '@/contexts/AuthContext';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { supabase } from '@/lib/supabase';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Separator } from '@/components/ui/separator';
import { Checkbox } from '@/components/ui/checkbox';
import { Eye, EyeOff, Wallet, Mail, Lock, User, AlertCircle, ArrowLeft, UserPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
const Login: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const { connectWallet, selectedAccount } = usePezkuwi();
const { signIn, signUp } = useAuth();
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
// Detect embedded WebView (DApps browser) - hide Google OAuth there
const isWebView = /wv|WebView/i.test(navigator.userAgent) ||
(/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/i.test(navigator.userAgent)) ||
(/Android.*Version\/[\d.]+.*Chrome\/[\d.]+ Mobile/i.test(navigator.userAgent) && !/Chrome\/[\d.]+ Mobile Safari/i.test(navigator.userAgent));
const [loginData, setLoginData] = useState({
email: '',
password: ''
});
const [signupData, setSignupData] = useState({
name: '',
email: '',
password: '',
confirmPassword: '',
referralCode: ''
});
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const { error } = await signIn(loginData.email, loginData.password, rememberMe);
if (error) {
if (error.message?.includes('Invalid login credentials')) {
setError('Email or password is incorrect. Please try again.');
} else {
setError(error instanceof Error ? error.message : 'Login failed. Please try again.');
}
} else {
navigate('/');
}
} catch {
setError('Login failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
if (signupData.password !== signupData.confirmPassword) {
setError('Passwords do not match');
setLoading(false);
return;
}
if (signupData.password.length < 8) {
setError('Password must be at least 8 characters');
setLoading(false);
return;
}
const { error } = await signUp(
signupData.email,
signupData.password,
signupData.name,
signupData.referralCode
);
if (error) {
setError(error.message);
} else {
// Redirect to email verification page
navigate('/email-verification', { state: { email: signupData.email } });
}
} catch {
setError('Signup failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleWalletConnect = async () => {
setLoading(true);
setError('');
try {
await connectWallet();
if (selectedAccount) {
navigate('/');
} else {
setError('Please select an account from your Pezkuwi.js extension');
}
} catch (err) {
if (import.meta.env.DEV) console.error('Wallet connection failed:', err);
const errorMsg = err instanceof Error ? err.message : '';
if (errorMsg?.includes('extension')) {
setError('Pezkuwi.js extension not found. Please install it first.');
} else {
setError('Failed to connect wallet. Please try again.');
}
} finally {
setLoading(false);
}
};
const handleGoogleSignIn = async () => {
setError('');
setLoading(true);
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: { redirectTo: window.location.origin + '/' },
});
if (error) setError(error.message);
} catch {
setError('Google sign-in failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleXSignIn = async () => {
setError('');
setLoading(true);
try {
const { error } = await supabase.auth.signInWithOAuth({
provider: 'twitter',
options: { redirectTo: window.location.origin + '/' },
});
if (error) setError(error.message);
} catch {
setError('X sign-in failed. Please try again.');
} finally {
setLoading(false);
}
};
const handleTelegramSignIn = () => {
setError('');
setLoading(true);
const BOT_ID = window.location.hostname === 'pex.mom' ? '8690398980' : '8754021997';
const origin = window.location.origin;
const popup = window.open(
`https://oauth.telegram.org/auth?bot_id=${BOT_ID}&origin=${encodeURIComponent(origin)}&embed=1&request_access=write`,
'TelegramLogin',
'width=550,height=470,left=400,top=200'
);
if (!popup) {
setError('Popup blocked. Please allow popups for this site.');
setLoading(false);
return;
}
const onMessage = async (event: MessageEvent) => {
if (event.origin !== 'https://oauth.telegram.org') return;
if (!event.data || event.data.event !== 'auth_result') return;
window.removeEventListener('message', onMessage);
popup.close();
try {
const tgData = event.data.result;
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
const res = await fetch(`${supabaseUrl}/functions/v1/telegram-auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'apikey': supabaseKey,
'Authorization': `Bearer ${supabaseKey}`,
},
body: JSON.stringify({ ...tgData, bot_id: BOT_ID }),
});
const json = await res.json();
if (!res.ok) throw new Error(json.error || 'Telegram auth failed');
const { error: otpError } = await supabase.auth.verifyOtp({
token_hash: json.token_hash,
type: 'magiclink',
});
if (otpError) throw otpError;
navigate('/');
} catch (err) {
setError(err instanceof Error ? err.message : 'Telegram sign-in failed.');
} finally {
setLoading(false);
}
};
window.addEventListener('message', onMessage);
// Cleanup if popup is closed without completing auth
const pollClosed = setInterval(() => {
if (popup.closed) {
clearInterval(pollClosed);
window.removeEventListener('message', onMessage);
setLoading(false);
}
}, 500);
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-900 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-[url('/grid.svg')] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]"></div>
<Card className="w-full max-w-md relative z-10 bg-gray-900/90 backdrop-blur-xl border-gray-800">
<CardHeader className="space-y-1">
<button
onClick={() => navigate('/')}
className="absolute top-4 left-4 text-gray-400 hover:text-white transition-colors"
>
<ArrowLeft className="w-5 h-5" />
</button>
<CardTitle className="text-2xl font-bold text-center bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
PezkuwiChain
</CardTitle>
<CardDescription className="text-center text-gray-400">
{t('login.subtitle', 'Access your governance account')}
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-gray-800">
<TabsTrigger value="login">{t('login.signin', 'Sign In')}</TabsTrigger>
<TabsTrigger value="signup">{t('login.signup', 'Sign Up')}</TabsTrigger>
</TabsList>
<TabsContent value="login" className="space-y-4">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="email" className="text-gray-300">
{t('login.email', 'Email')}
</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="email"
type="email"
placeholder="name@example.com"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={loginData.email}
onChange={(e) => setLoginData({...loginData, email: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password" className="text-gray-300">
{t('login.password', 'Password')}
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
className="pl-10 pr-10 bg-gray-800 border-gray-700 text-white"
value={loginData.password}
onChange={(e) => setLoginData({...loginData, password: e.target.value})}
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-500 hover:text-gray-300"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Checkbox
id="remember"
checked={rememberMe}
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
/>
<Label htmlFor="remember" className="text-sm text-gray-400 cursor-pointer">
{t('login.rememberMe', 'Remember me')}
</Label>
</div>
<button
type="button"
className="text-sm text-green-500 hover:text-green-400"
onClick={() => navigate('/reset-password')}
>
{t('login.forgotPassword', 'Forgot password?')}
</button>
</div>
{error && (
<Alert className="bg-red-900/20 border-red-800">
<AlertCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-400">{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full bg-gradient-to-r from-green-600 to-green-500 hover:from-green-500 hover:to-green-400"
disabled={loading}
>
{loading ? t('login.signingIn', 'Signing in...') : t('login.signin', 'Sign In')}
</Button>
</form>
</TabsContent>
<TabsContent value="signup" className="space-y-4">
<form onSubmit={handleSignup} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name" className="text-gray-300">
{t('login.fullName', 'Full Name')}
</Label>
<div className="relative">
<User className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="name"
type="text"
placeholder="John Doe"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.name}
onChange={(e) => setSignupData({...signupData, name: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="signup-email" className="text-gray-300">
{t('login.email', 'Email')}
</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="signup-email"
type="email"
placeholder="name@example.com"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.email}
onChange={(e) => setSignupData({...signupData, email: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="signup-password" className="text-gray-300">
{t('login.password', 'Password')}
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="signup-password"
type={showPassword ? 'text' : 'password'}
placeholder="••••••••"
className="pl-10 pr-10 bg-gray-800 border-gray-700 text-white"
value={signupData.password}
onChange={(e) => setSignupData({...signupData, password: e.target.value})}
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-gray-500 hover:text-gray-300"
>
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
</button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password" className="text-gray-300">
{t('login.confirmPassword', 'Confirm Password')}
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="confirm-password"
type="password"
placeholder="••••••••"
className="pl-10 bg-gray-800 border-gray-700 text-white"
value={signupData.confirmPassword}
onChange={(e) => setSignupData({...signupData, confirmPassword: e.target.value})}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="referral-code" className="text-gray-300">
{t('login.referralCode', 'Referral Code')}
<span className="text-gray-500 text-xs ml-1">({t('login.optional', 'Optional')})</span>
</Label>
<div className="relative">
<UserPlus className="absolute left-3 top-3 w-4 h-4 text-gray-500" />
<Input
id="referral-code"
type="text"
placeholder={t('login.enterReferralCode', 'Referral code (optional)')}
className="pl-10 bg-gray-800 border-gray-700 text-white placeholder:text-gray-500 placeholder:opacity-50"
value={signupData.referralCode}
onChange={(e) => setSignupData({...signupData, referralCode: e.target.value})}
/>
</div>
<p className="text-xs text-gray-500">
{t('login.referralDescription', 'If someone referred you, enter their code here')}
</p>
</div>
{error && (
<Alert className="bg-red-900/20 border-red-800">
<AlertCircle className="h-4 w-4 text-red-500" />
<AlertDescription className="text-red-400">{error}</AlertDescription>
</Alert>
)}
<Button
type="submit"
className="w-full bg-gradient-to-r from-yellow-600 to-yellow-500 hover:from-yellow-500 hover:to-yellow-400"
disabled={loading}
>
{loading ? t('login.creatingAccount', 'Creating account...') : t('login.createAccount', 'Create Account')}
</Button>
</form>
</TabsContent>
</Tabs>
<div className="mt-6">
<Separator className="bg-gray-800" />
<div className="relative -top-3 text-center">
<span className="bg-gray-900 px-2 text-sm text-gray-500">
{t('login.or', 'Or continue with')}
</span>
</div>
</div>
<div className="space-y-3">
{!isWebView && (
<Button
variant="outline"
className="w-full border-gray-700 bg-gray-800 hover:bg-gray-700 text-white"
onClick={handleGoogleSignIn}
disabled={loading}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
</svg>
{t('login.googleSignIn', 'Continue with Google')}
</Button>
)}
<Button
variant="outline"
className="w-full border-gray-700 bg-gray-800 hover:bg-gray-700 text-white"
onClick={handleXSignIn}
disabled={loading}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
{t('login.xSignIn', 'Continue with X')}
</Button>
<Button
variant="outline"
className="w-full border-gray-700 bg-gray-800 hover:bg-gray-700 text-white"
onClick={handleTelegramSignIn}
disabled={loading}
>
<svg className="mr-2 h-4 w-4" viewBox="0 0 24 24" fill="#26A5E4">
<path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm5.562 8.248-1.97 9.28c-.145.658-.537.818-1.084.508l-3-2.21-1.447 1.394c-.16.16-.295.295-.605.295l.213-3.053 5.56-5.023c.242-.213-.054-.333-.373-.12L6.12 14.26l-2.96-.924c-.643-.204-.657-.643.136-.953l11.57-4.461c.537-.194 1.006.131.696.326z"/>
</svg>
{t('login.telegramSignIn', 'Continue with Telegram')}
</Button>
<Button
variant="outline"
className="w-full border-gray-700 bg-gray-800 hover:bg-gray-700 text-white"
onClick={handleWalletConnect}
disabled={loading}
>
<Wallet className="mr-2 h-4 w-4" />
{t('login.connectWallet', 'Connect with Pezkuwi.js')}
</Button>
</div>
</CardContent>
<CardFooter className="text-center text-sm text-gray-500">
<p>
{t('login.terms', 'By continuing, you agree to our')}{' '}
<a href="#" className="text-green-500 hover:text-green-400">
{t('login.termsOfService', 'Terms of Service')}
</a>{' '}
{t('login.and', 'and')}{' '}
<a href="#" className="text-green-500 hover:text-green-400">
{t('login.privacyPolicy', 'Privacy Policy')}
</a>
</p>
</CardFooter>
</Card>
</div>
);
};
export default Login;