web2-client/src/shared/services/auth/index.ts

247 lines
10 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.

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);
}
});
};