mirror of
https://github.com/pezkuwichain/pezkuwi-p2p-mobile.git
synced 2026-06-14 00:41:02 +00:00
5236f8c470
- loginViaParams now accepts session_token from URL - Removes insecure from_miniapp parameter - Aligns with telegram-auth security update
287 lines
8.1 KiB
TypeScript
287 lines
8.1 KiB
TypeScript
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
|
import { supabase } from '@/lib/supabase';
|
|
import { setCurrentUserId } from '@/lib/p2p-fiat';
|
|
|
|
// Telegram WebApp types
|
|
declare global {
|
|
interface Window {
|
|
Telegram?: {
|
|
WebApp: {
|
|
initData: string;
|
|
initDataUnsafe: {
|
|
user?: {
|
|
id: number;
|
|
first_name: string;
|
|
last_name?: string;
|
|
username?: string;
|
|
language_code?: string;
|
|
photo_url?: string;
|
|
};
|
|
auth_date: number;
|
|
hash: string;
|
|
};
|
|
ready: () => void;
|
|
expand: () => void;
|
|
close: () => void;
|
|
MainButton: {
|
|
text: string;
|
|
show: () => void;
|
|
hide: () => void;
|
|
onClick: (callback: () => void) => void;
|
|
};
|
|
HapticFeedback: {
|
|
impactOccurred: (style: 'light' | 'medium' | 'heavy' | 'rigid' | 'soft') => void;
|
|
notificationOccurred: (type: 'error' | 'success' | 'warning') => void;
|
|
selectionChanged: () => void;
|
|
};
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
interface TelegramUser {
|
|
id: number;
|
|
first_name: string;
|
|
last_name?: string;
|
|
username?: string;
|
|
photo_url?: string;
|
|
}
|
|
|
|
interface User {
|
|
id: string; // Supabase user ID
|
|
telegram_id: number;
|
|
telegram_username?: string;
|
|
display_name: string;
|
|
avatar_url?: string;
|
|
wallet_address?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
interface AuthContextType {
|
|
user: User | null;
|
|
telegramUser: TelegramUser | null;
|
|
isLoading: boolean;
|
|
isAuthenticated: boolean;
|
|
error: string | null;
|
|
login: () => Promise<void>;
|
|
logout: () => void;
|
|
linkWallet: (address: string) => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextType | null>(null);
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [telegramUser, setTelegramUser] = useState<TelegramUser | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
// Get Telegram user from WebApp
|
|
const getTelegramUser = useCallback((): TelegramUser | null => {
|
|
const tg = window.Telegram?.WebApp;
|
|
if (!tg?.initDataUnsafe?.user) {
|
|
return null;
|
|
}
|
|
return tg.initDataUnsafe.user;
|
|
}, []);
|
|
|
|
// Login with Telegram
|
|
const login = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const tg = window.Telegram?.WebApp;
|
|
|
|
if (!tg?.initData) {
|
|
throw new Error('Telegram WebApp not available. Open from Telegram.');
|
|
}
|
|
|
|
// Call Supabase Edge Function to verify initData and get/create user
|
|
const { data, error: fnError } = await supabase.functions.invoke('telegram-auth', {
|
|
body: { initData: tg.initData }
|
|
});
|
|
|
|
if (fnError) throw fnError;
|
|
|
|
if (!data?.user) {
|
|
throw new Error('Authentication failed');
|
|
}
|
|
|
|
setUser(data.user);
|
|
// Use auth_user_id for P2P operations (balance queries, etc.)
|
|
// This is the auth.users ID used by p2p_deposit_withdraw_requests FK
|
|
const p2pUserId = data.auth_user_id || data.user.id;
|
|
setCurrentUserId(p2pUserId);
|
|
setTelegramUser(getTelegramUser());
|
|
|
|
// Store session token and auth_user_id if provided
|
|
if (data.session_token) {
|
|
localStorage.setItem('p2p_session', data.session_token);
|
|
}
|
|
if (data.auth_user_id) {
|
|
localStorage.setItem('p2p_auth_user_id', data.auth_user_id);
|
|
}
|
|
|
|
window.Telegram?.WebApp.HapticFeedback.notificationOccurred('success');
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : 'Login failed';
|
|
setError(message);
|
|
window.Telegram?.WebApp.HapticFeedback.notificationOccurred('error');
|
|
console.error('Login error:', err);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [getTelegramUser]);
|
|
|
|
// Logout
|
|
const logout = useCallback(() => {
|
|
setUser(null);
|
|
setCurrentUserId(null); // Clear user ID for p2p-fiat functions
|
|
localStorage.removeItem('p2p_session');
|
|
window.Telegram?.WebApp.HapticFeedback.impactOccurred('medium');
|
|
}, []);
|
|
|
|
// Link wallet address
|
|
const linkWallet = useCallback(async (address: string) => {
|
|
if (!user) throw new Error('Not authenticated');
|
|
|
|
const { error: updateError } = await supabase
|
|
.from('p2p_users')
|
|
.update({ wallet_address: address })
|
|
.eq('telegram_id', user.telegram_id);
|
|
|
|
if (updateError) throw updateError;
|
|
|
|
setUser(prev => prev ? { ...prev, wallet_address: address } : null);
|
|
window.Telegram?.WebApp.HapticFeedback.notificationOccurred('success');
|
|
}, [user]);
|
|
|
|
// Login via URL params (from mini-app redirect with session_token)
|
|
const loginViaParams = useCallback(async () => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const sessionToken = params.get('session_token');
|
|
const from = params.get('from');
|
|
|
|
// Check if coming from miniapp with session_token
|
|
if (!sessionToken || from !== 'miniapp') {
|
|
return false;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
|
|
try {
|
|
// Verify session token with backend
|
|
const { data, error: fnError } = await supabase.functions.invoke('telegram-auth', {
|
|
body: { sessionToken }
|
|
});
|
|
|
|
if (fnError) throw fnError;
|
|
|
|
if (!data?.user) {
|
|
throw new Error('Authentication failed');
|
|
}
|
|
|
|
setUser(data.user);
|
|
// Use auth_user_id for P2P operations
|
|
const p2pUserId = data.auth_user_id || data.user.id;
|
|
setCurrentUserId(p2pUserId);
|
|
|
|
// Store session token and auth_user_id
|
|
if (data.session_token) {
|
|
localStorage.setItem('p2p_session', data.session_token);
|
|
}
|
|
if (data.auth_user_id) {
|
|
localStorage.setItem('p2p_auth_user_id', data.auth_user_id);
|
|
}
|
|
|
|
// Clear URL params after successful login
|
|
window.history.replaceState({}, '', window.location.pathname);
|
|
|
|
return true;
|
|
} catch (err) {
|
|
console.error('Session token login error:', err);
|
|
return false;
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
// Auto-login on mount
|
|
useEffect(() => {
|
|
const initAuth = async () => {
|
|
const tg = window.Telegram?.WebApp;
|
|
|
|
// Check for existing session first
|
|
const sessionToken = localStorage.getItem('p2p_session');
|
|
const storedAuthUserId = localStorage.getItem('p2p_auth_user_id');
|
|
if (sessionToken) {
|
|
try {
|
|
const { data, error } = await supabase.functions.invoke('telegram-auth', {
|
|
body: { sessionToken }
|
|
});
|
|
if (!error && data?.user) {
|
|
setUser(data.user);
|
|
// Use stored or returned auth_user_id for P2P operations
|
|
const p2pUserId = data.auth_user_id || storedAuthUserId || data.user.id;
|
|
setCurrentUserId(p2pUserId);
|
|
if (data.auth_user_id) {
|
|
localStorage.setItem('p2p_auth_user_id', data.auth_user_id);
|
|
}
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
} catch {
|
|
localStorage.removeItem('p2p_session');
|
|
localStorage.removeItem('p2p_auth_user_id');
|
|
}
|
|
}
|
|
|
|
// Try Telegram WebApp auth
|
|
if (tg?.initData) {
|
|
tg.ready();
|
|
tg.expand();
|
|
setTelegramUser(getTelegramUser());
|
|
await login();
|
|
return;
|
|
}
|
|
|
|
// Try URL params auth (from mini-app redirect with session_token)
|
|
const params = new URLSearchParams(window.location.search);
|
|
if (params.get('from') === 'miniapp' && params.get('session_token')) {
|
|
const success = await loginViaParams();
|
|
if (success) return;
|
|
}
|
|
|
|
setIsLoading(false);
|
|
};
|
|
|
|
initAuth();
|
|
}, [getTelegramUser, login, loginViaParams]);
|
|
|
|
const value: AuthContextType = {
|
|
user,
|
|
telegramUser,
|
|
isLoading,
|
|
isAuthenticated: !!user,
|
|
error,
|
|
login,
|
|
logout,
|
|
linkWallet
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={value}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
const context = useContext(AuthContext);
|
|
if (!context) {
|
|
throw new Error('useAuth must be used within AuthProvider');
|
|
}
|
|
return context;
|
|
}
|