247 lines
10 KiB
TypeScript
247 lines
10 KiB
TypeScript
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<ReturnType<typeof setInterval> | 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);
|
||
}
|
||
});
|
||
};
|