import { useRef } from 'react'; import { useTonConnectUI } from '@tonconnect/ui-react'; import { useMutation } from 'react-query'; import { request } from '~/shared/libs'; import { useWebApp } from '@vkruglikov/react-telegram-web-app'; import { appendReferral } from '~/shared/utils/start-payload'; const sessionStorageKey = 'auth_v1_token'; const payloadTTLMS = 1000 * 60 * 20; export const useAuth = () => { const WebApp = useWebApp(); const [tonConnectUI] = useTonConnectUI(); const interval = useRef | undefined>(); const makeAuthRequest = async (params: { twa_data: string; ton_proof?: { account: any; ton_proof: any; }; }) => { try { const res = await request.post<{ connected_wallet: null | { version: string; address: string; ton_balance: string; }; auth_v1_token: string; }>('/auth.twa', appendReferral(params)); if (res?.data?.auth_v1_token) { localStorage.setItem(sessionStorageKey, res.data.auth_v1_token); // If we sent a proof, it was accepted, so keep record of that if (params.ton_proof) { console.log('DEBUG: Auth with proof successful'); } } else { throw new Error('Failed to get auth token'); } return res; } catch (error) { throw error; } }; const makeSelectWalletRequest = async (params: { wallet_address: string }) => { try { const res = await request.post('/auth.selectWallet', params); return res; } catch (error: any) { // Check for 404 error (wallet not found or invalid) if (error.response?.status === 404) { console.log('DEBUG: Wallet selection failed with 404, disconnecting'); await tonConnectUI.disconnect(); localStorage.removeItem(sessionStorageKey); } throw error; } }; // Helper to prepare the connection parameters with proof requirements const prepareConnectParams = async () => { console.log('DEBUG: Preparing connect parameters'); // Set to loading state first tonConnectUI.setConnectRequestParameters({ state: 'loading' }); try { // Get the payload/token from backend const value = await request.post<{ auth_v1_token: string }>('/auth.twa', appendReferral({ twa_data: WebApp.initData, })); if (value?.data?.auth_v1_token) { console.log('DEBUG: Got token for connect params'); // Set the parameters to ready with tonProof requirement tonConnectUI.setConnectRequestParameters({ state: 'ready', value: { tonProof: value.data.auth_v1_token }, }); return true; } else { console.log('DEBUG: No token received for connect params'); tonConnectUI.setConnectRequestParameters(null); return false; } } catch (error) { console.error('DEBUG: Error preparing connect params:', error); tonConnectUI.setConnectRequestParameters(null); return false; } }; // Helper to check if connection is capable of transactions const isConnectionValid = () => { if (!tonConnectUI.connected || !tonConnectUI.wallet) { return false; } return true; }; // Helper для ожидания подключения с таймаутом const waitForConnection = async (timeoutMs = 30000, checkIntervalMs = 500) => { console.log('DEBUG: Waiting for wallet connection'); const startTime = Date.now(); while (Date.now() - startTime < timeoutMs) { if (tonConnectUI.connected) { console.log('DEBUG: Connection detected'); // Даем дополнительное время для инициализации connectItems await new Promise((resolve) => setTimeout(resolve, 1000)); return true; } // Проверяем статус каждые checkIntervalMs await new Promise((resolve) => setTimeout(resolve, checkIntervalMs)); } console.log('DEBUG: Connection wait timed out'); return false; }; return useMutation(['auth'], async () => { clearInterval(interval.current); let authResult; console.log('DEBUG: Starting auth flow'); try { // Case 1: Not connected - need to connect and get proof if (!tonConnectUI.connected) { console.log('DEBUG: No wallet connection, starting flow'); localStorage.removeItem(sessionStorageKey); // Prepare connection parameters (this sets up the proof requirement) const prepared = await prepareConnectParams(); if (!prepared) { throw new Error('Failed to prepare connection parameters'); } // Start periodic refresh of the payload interval.current = setInterval(prepareConnectParams, payloadTTLMS); // Open the modal and wait for connection try { console.log('DEBUG: Opening wallet connect modal'); tonConnectUI.openModal(); // Ждем подключения кошелька const connected = await waitForConnection(); if (!connected || !tonConnectUI.connected) { console.log('DEBUG: Connection cancelled or failed after waiting'); throw new Error('Wallet connection cancelled or failed'); } // Даем дополнительное время для полной инициализации wallet if (!tonConnectUI.wallet || !tonConnectUI.wallet.connectItems) { console.log('DEBUG: Wallet object not fully initialized, waiting...'); await new Promise((resolve) => setTimeout(resolve, 1500)); } console.log('DEBUG: Connection successful, wallet details:', { connected: tonConnectUI.connected, hasWallet: !!tonConnectUI.wallet, hasConnectItems: !!tonConnectUI.wallet?.connectItems, hasTonProof: !!tonConnectUI.wallet?.connectItems?.tonProof, }); } catch (error) { console.error('DEBUG: Error during wallet connection:', error); throw new Error('Wallet connection process failed'); } // Check if we have a proof after connection if ( tonConnectUI.wallet?.connectItems?.tonProof && !('error' in tonConnectUI.wallet.connectItems.tonProof) ) { console.log('DEBUG: Got proof after connection'); try { // Try auth with the proof authResult = await makeAuthRequest({ twa_data: WebApp.initData, ton_proof: { account: tonConnectUI.wallet.account, ton_proof: tonConnectUI.wallet.connectItems.tonProof.proof, }, }); } catch (error) { // If auth with proof fails, throw the error console.error('DEBUG: Auth with fresh proof failed:', error); throw error; } } else { console.log('DEBUG: No proof available after connection'); // If we can't get proof but we're connected, try auth without it authResult = await makeAuthRequest({ twa_data: WebApp.initData, }); } } else { // Case 2: Already connected console.log('DEBUG: Already connected'); // TonConnect proofs are meant for initial wallet binding; reusing old proofs // commonly fails server-side (replay/unknown payload). Use TWA auth without proof. authResult = await makeAuthRequest({ twa_data: WebApp.initData, }); } // Always try to select wallet after auth (this validates the connection) if (tonConnectUI.wallet?.account?.address) { console.log('DEBUG: Selecting wallet', tonConnectUI.wallet.account.address); try { await makeSelectWalletRequest({ wallet_address: tonConnectUI.wallet.account.address, }); // Additional validation check if (!isConnectionValid()) { console.log('DEBUG: Connection validation failed, disconnecting'); await tonConnectUI.disconnect(); localStorage.removeItem(sessionStorageKey); throw new Error('Connection validation failed'); } } catch (error) { // Errors from makeSelectWalletRequest are already handled console.error('DEBUG: Wallet selection failed:', error); throw error; } } return authResult; } catch (error) { console.error('DEBUG: Auth flow failed:', error); throw error; } finally { clearInterval(interval.current); } }); };