Add Node.js polyfills and enhance Telegram connection handling

- Introduced polyfills for Node.js globals (Buffer, process, util) to support Telegram package functionality in the browser.
- Updated Vite configuration to include node polyfills, ensuring compatibility with Node.js APIs.
- Enhanced TelegramConnectionModal to manage connection states, including loading and error handling, improving user experience during authentication.
- Implemented QR code generation and handling for Telegram login, with robust error management and user feedback.

These changes establish a solid foundation for Telegram integration, enhancing the application's ability to handle real-time communication and user authentication effectively.
This commit is contained in:
Steven Enamakel
2026-01-28 05:27:59 +05:30
parent d38d6d6603
commit 1495d9b72c
8 changed files with 1410 additions and 68 deletions
+8 -1
View File
@@ -15,7 +15,11 @@
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@types/react-router-dom": "^5.3.3",
"buffer": "^6.0.3",
"lottie-react": "^2.4.1",
"os-browserify": "^0.3.0",
"process": "^0.11.10",
"qrcode.react": "^4.2.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-redux": "^9.2.0",
@@ -24,12 +28,14 @@
"redux-persist": "^6.0.0",
"socket.io-client": "^4.8.3",
"telegram": "^2.26.22",
"util": "^0.12.5",
"zustand": "^5.0.10"
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tauri-apps/cli": "^2",
"@types/node": "^25.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/redux-logger": "^3.0.13",
@@ -38,6 +44,7 @@
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
"typescript": "~5.8.3",
"vite": "^7.0.4"
"vite": "^7.0.4",
"vite-plugin-node-polyfills": "^0.25.0"
}
}
+263 -52
View File
@@ -1,4 +1,16 @@
import { useState } from 'react';
import { useState, useEffect, useCallback, useRef } from 'react';
import { QRCodeSVG } from 'qrcode.react';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
initializeTelegram,
connectTelegram,
checkAuthStatus,
setAuthStatus,
setAuthError,
setConnectionStatus,
} from '../store/telegramSlice';
import { selectIsInitialized, selectConnectionStatus, selectAuthStatus } from '../store/telegramSelectors';
import { mtprotoService } from '../services/mtprotoService';
interface TelegramConnectionModalProps {
isOpen: boolean;
@@ -6,38 +18,176 @@ interface TelegramConnectionModalProps {
onComplete: () => void;
}
type ConnectionStep = 'qr' | '2fa';
type ConnectionStep = 'qr' | '2fa' | 'loading' | 'error';
const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnectionModalProps) => {
const dispatch = useAppDispatch();
const isInitialized = useAppSelector(selectIsInitialized);
const connectionStatus = useAppSelector(selectConnectionStatus);
const authStatus = useAppSelector(selectAuthStatus);
const [currentStep, setCurrentStep] = useState<ConnectionStep>('qr');
const [password, setPassword] = useState('');
const [passwordHint, setPasswordHint] = useState<string | undefined>();
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null);
const [qrCodeExpires, setQrCodeExpires] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [isAuthenticating, setIsAuthenticating] = useState(false);
// Store password promise resolver
const passwordResolverRef = useRef<((password: string) => void) | null>(null);
if (!isOpen) return null;
// Initialize and connect when modal opens
useEffect(() => {
if (!isOpen) return;
const handleQRNext = () => {
// In a real app, this would check if 2FA is required
// For now, we'll simulate it by showing 2FA screen
setCurrentStep('2fa');
};
const init = async () => {
try {
setCurrentStep('loading');
if (!isInitialized) {
await dispatch(initializeTelegram()).unwrap();
}
if (connectionStatus !== 'connected') {
await dispatch(connectTelegram()).unwrap();
}
// Check if already authenticated
const authCheck = await dispatch(checkAuthStatus()).unwrap();
if (authCheck) {
onComplete();
onClose();
return;
}
// Start QR code flow
startQrCodeFlow();
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to initialize Telegram';
setError(errorMessage);
setCurrentStep('error');
dispatch(setConnectionStatus('error'));
}
};
const handle2FASubmit = () => {
// In a real app, this would verify the password
onComplete();
onClose();
// Reset state
setCurrentStep('qr');
setPassword('');
init();
}, [isOpen, isInitialized, connectionStatus, dispatch, onComplete, onClose]);
const startQrCodeFlow = useCallback(async () => {
try {
setIsAuthenticating(true);
setError(null);
setCurrentStep('qr');
setPassword('');
setPasswordHint(undefined);
setQrCodeUrl(null);
setQrCodeExpires(null);
await mtprotoService.signInWithQrCode(
(qrCode) => {
// Convert Buffer/Uint8Array to base64url for QR code URL
let tokenBase64: string;
if (qrCode.token instanceof Uint8Array) {
// Convert Uint8Array to base64url
const binary = Array.from(qrCode.token)
.map((byte) => String.fromCharCode(byte))
.join('');
tokenBase64 = btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
} else {
// If it's a Buffer, use toString
const buffer = qrCode.token as { toString: (encoding: string) => string };
tokenBase64 = buffer.toString('base64url');
}
const url = `tg://login?token=${tokenBase64}`;
setQrCodeUrl(url);
setQrCodeExpires(qrCode.expires);
},
async (hint) => {
// 2FA password required
setPasswordHint(hint);
setCurrentStep('2fa');
setIsAuthenticating(false);
// Wait for user to enter password
return new Promise<string>((resolve) => {
passwordResolverRef.current = resolve;
});
},
async (err) => {
// Handle errors
const errorMessage = err.message || 'Authentication error';
setError(errorMessage);
dispatch(setAuthError(errorMessage));
// Check if it's a cancellation
if (errorMessage.includes('AUTH_USER_CANCEL') || errorMessage.includes('cancel')) {
setCurrentStep('qr');
setIsAuthenticating(false);
return true; // Stop authentication
}
return false; // Continue
}
);
// Authentication successful
setIsAuthenticating(false);
await dispatch(checkAuthStatus()).unwrap();
dispatch(setAuthStatus('authenticated'));
onComplete();
onClose();
} catch (err) {
setIsAuthenticating(false);
const errorMessage = err instanceof Error ? err.message : 'Authentication failed';
setError(errorMessage);
setCurrentStep('error');
dispatch(setAuthError(errorMessage));
}
}, [dispatch, onComplete, onClose]);
const handle2FASubmit = async () => {
if (!password.trim() || !passwordResolverRef.current) return;
try {
setIsAuthenticating(true);
setError(null);
// Resolve the password promise to continue authentication
passwordResolverRef.current(password);
passwordResolverRef.current = null;
} catch (err) {
setIsAuthenticating(false);
const errorMessage = err instanceof Error ? err.message : 'Password verification failed';
setError(errorMessage);
dispatch(setAuthError(errorMessage));
}
};
const handleBack = () => {
if (currentStep === '2fa') {
setCurrentStep('qr');
setPassword('');
setPasswordHint(undefined);
if (passwordResolverRef.current) {
passwordResolverRef.current('');
passwordResolverRef.current = null;
}
} else {
onClose();
}
};
const handleRetry = () => {
setError(null);
setPassword('');
setPasswordHint(undefined);
setQrCodeUrl(null);
setQrCodeExpires(null);
startQrCodeFlow();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div className="relative w-full max-w-md mx-4">
@@ -52,7 +202,39 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
</svg>
</button>
{currentStep === 'qr' ? (
{currentStep === 'loading' ? (
<div className="text-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-500 mx-auto mb-4"></div>
<p className="opacity-70">Initializing Telegram connection...</p>
</div>
) : currentStep === 'error' ? (
<>
{/* Error Screen */}
<div className="text-center">
<div className="w-16 h-16 bg-red-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<h2 className="text-2xl font-bold mb-2">Connection Error</h2>
<p className="opacity-70 text-sm mb-6">{error || 'An error occurred'}</p>
<div className="flex space-x-3">
<button
onClick={handleBack}
className="flex-1 py-2.5 px-4 bg-stone-800/50 hover:bg-stone-700/50 border border-stone-700 rounded-xl text-sm font-medium transition-all duration-200"
>
Cancel
</button>
<button
onClick={handleRetry}
className="flex-1 py-2.5 px-4 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white rounded-xl text-sm font-medium transition-all duration-200"
>
Retry
</button>
</div>
</div>
</>
) : currentStep === 'qr' ? (
<>
{/* QR Code Screen */}
<div className="text-center">
@@ -61,41 +243,54 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
{/* QR Code Container */}
<div className="flex justify-center mb-8">
<div className="bg-white p-4 rounded-2xl shadow-large">
<div className="w-64 h-64 bg-white rounded-xl flex items-center justify-center relative overflow-hidden">
{/* QR code pattern - black squares on white background */}
<div className="absolute inset-0 p-2">
<div className="w-full h-full grid grid-cols-[repeat(25,minmax(0,1fr))] gap-0">
{Array.from({ length: 625 }).map((_, i) => {
const row = Math.floor(i / 25);
const col = i % 25;
// Create QR-like pattern with finder patterns in corners
const isFinder =
(row < 7 && col < 7) || // Top-left
(row < 7 && col >= 18) || // Top-right
(row >= 18 && col < 7); // Bottom-left
// More realistic QR pattern - alternating pattern
const isBlack = isFinder || ((row + col) % 3 === 0 && Math.random() > 0.4) || (row * col % 7 < 3);
return (
<div
key={i}
className={`w-full h-full ${isBlack ? 'bg-black' : 'bg-white'}`}
/>
);
})}
{qrCodeUrl ? (
<div className="relative w-64 h-64 flex items-center justify-center">
<QRCodeSVG
value={qrCodeUrl}
size={256}
level="H"
includeMargin={true}
marginSize={1}
bgColor="#FFFFFF"
fgColor="#000000"
className="w-full h-full"
/>
{/* Telegram logo overlay */}
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<div className="w-16 h-16 bg-[#0088CC] rounded-full flex items-center justify-center shadow-lg">
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
</div>
</div>
{/* Telegram logo in center */}
<div className="absolute inset-0 flex items-center justify-center z-10 pointer-events-none">
<div className="w-16 h-16 bg-blue-500 rounded-full flex items-center justify-center shadow-lg">
<svg className="w-10 h-10 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
</svg>
</div>
) : (
<div className="w-64 h-64 bg-gray-100 rounded-xl flex items-center justify-center">
{isAuthenticating ? (
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p className="text-gray-600 text-sm">Generating QR code...</p>
</div>
) : (
<p className="text-gray-600 text-sm">Loading QR code...</p>
)}
</div>
</div>
)}
</div>
</div>
{qrCodeExpires && (
<p className="text-xs opacity-70 mb-4">
This code expires in {Math.max(0, Math.floor((qrCodeExpires * 1000 - Date.now()) / 1000))} seconds
</p>
)}
{error && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-xl">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
{/* Instructions */}
<div className="space-y-4 mb-6">
<div className="flex items-start space-x-3 text-left">
@@ -129,10 +324,11 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
Cancel
</button>
<button
onClick={handleQRNext}
className="flex-1 py-2.5 px-4 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 text-white rounded-xl text-sm font-medium transition-all duration-200"
onClick={handleRetry}
disabled={isAuthenticating}
className="flex-1 py-2.5 px-4 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl text-sm font-medium transition-all duration-200"
>
I've Scanned
{isAuthenticating ? 'Connecting...' : 'Refresh QR Code'}
</button>
</div>
</div>
@@ -143,18 +339,32 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
<div className="text-center">
<h2 className="text-2xl font-bold mb-2">Enter Your Password</h2>
<p className="opacity-70 text-sm mb-6">
Your account is protected with two-step verification. Please enter your password to continue.
{passwordHint
? `Your account is protected with two-step verification. Hint: ${passwordHint}`
: 'Your account is protected with two-step verification. Please enter your password to continue.'}
</p>
{error && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-xl">
<p className="text-red-400 text-sm">{error}</p>
</div>
)}
{/* Password input */}
<div className="mb-6">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && password.trim() && !isAuthenticating) {
handle2FASubmit();
}
}}
placeholder="Enter your password"
className="w-full px-4 py-3 bg-black/50 border border-stone-700 rounded-xl text-white placeholder-opacity-50 focus:outline-none focus:border-primary-500 transition-colors"
autoFocus
disabled={isAuthenticating}
/>
</div>
@@ -162,16 +372,17 @@ const TelegramConnectionModal = ({ isOpen, onClose, onComplete }: TelegramConnec
<div className="flex space-x-3">
<button
onClick={handleBack}
className="flex-1 py-2.5 px-4 bg-stone-800/50 hover:bg-stone-700/50 border border-stone-700 rounded-xl text-sm font-medium transition-all duration-200"
disabled={isAuthenticating}
className="flex-1 py-2.5 px-4 bg-stone-800/50 hover:bg-stone-700/50 border border-stone-700 rounded-xl text-sm font-medium transition-all duration-200 disabled:opacity-50"
>
Back
</button>
<button
onClick={handle2FASubmit}
disabled={!password.trim()}
disabled={!password.trim() || isAuthenticating}
className="flex-1 py-2.5 px-4 bg-primary-500 hover:bg-primary-600 active:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl text-sm font-medium transition-all duration-200"
>
Continue
{isAuthenticating ? 'Verifying...' : 'Continue'}
</button>
</div>
</div>
+3
View File
@@ -1,3 +1,6 @@
// IMPORTANT: Polyfills must be imported FIRST
import "./polyfills";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
+49
View File
@@ -0,0 +1,49 @@
// Polyfill Node.js globals for browser (required by telegram package)
// This must be imported FIRST before any other imports that use Node.js APIs
import { Buffer } from 'buffer';
import process from 'process';
import * as util from 'util';
// Immediately set Buffer on all global objects synchronously
// This must happen before any other code runs
(function setupNodePolyfills() {
const buffer = Buffer;
// Set Buffer on all global objects
if (typeof globalThis !== 'undefined') {
(globalThis as any).Buffer = buffer;
}
if (typeof window !== 'undefined') {
(window as any).Buffer = buffer;
}
if (typeof global !== 'undefined') {
(global as any).Buffer = buffer;
(global as any).global = globalThis;
}
if (typeof self !== 'undefined') {
(self as any).Buffer = buffer;
}
// Set process on global objects
if (typeof globalThis !== 'undefined') {
(globalThis as any).process = process;
(globalThis as any).util = util;
}
if (typeof window !== 'undefined') {
(window as any).process = process;
(window as any).util = util;
}
if (typeof global !== 'undefined') {
(global as any).process = process;
(global as any).util = util;
}
})();
// Export for use in modules
export { Buffer, process, util };
export default Buffer;
+55 -7
View File
@@ -1,6 +1,7 @@
import { TelegramClient } from 'telegram';
import { StringSession } from 'telegram/sessions';
import type { UserAuthParams, BotAuthParams } from 'telegram/client/auth';
import { TELEGRAM_API_ID, TELEGRAM_API_HASH } from '../utils/config';
type LoginOptions = UserAuthParams | BotAuthParams;
@@ -10,9 +11,17 @@ class MTProtoService {
private isInitialized = false;
private isConnected = false;
private sessionString = '';
private readonly apiId: number;
private readonly apiHash: string;
private constructor() {
// Private constructor to enforce singleton
// Load API credentials from config once
if (!TELEGRAM_API_ID || !TELEGRAM_API_HASH) {
throw new Error('TELEGRAM_API_ID and TELEGRAM_API_HASH must be configured');
}
this.apiId = TELEGRAM_API_ID;
this.apiHash = TELEGRAM_API_HASH;
}
static getInstance(): MTProtoService {
@@ -31,19 +40,13 @@ class MTProtoService {
return;
}
const apiId = import.meta.env.VITE_TELEGRAM_API_ID;
const apiHash = import.meta.env.VITE_TELEGRAM_API_HASH;
const sessionString = this.loadSession() || '';
if (!apiId || !apiHash) {
throw new Error('VITE_TELEGRAM_API_ID and VITE_TELEGRAM_API_HASH must be configured');
}
try {
const stringSession = new StringSession(sessionString);
this.sessionString = sessionString;
this.client = new TelegramClient(stringSession, Number(apiId), String(apiHash), {
this.client = new TelegramClient(stringSession, this.apiId, this.apiHash, {
connectionRetries: 5,
});
@@ -110,6 +113,51 @@ class MTProtoService {
}
}
/**
* Sign in using QR code
*/
async signInWithQrCode(
qrCodeCallback: (qrCode: { token: Buffer; expires: number }) => void,
passwordCallback?: (hint?: string) => Promise<string>,
onError?: (err: Error) => Promise<boolean> | void
): Promise<unknown> {
if (!this.client) {
throw new Error('MTProto client not initialized. Call initialize() first.');
}
try {
const user = await this.client.signInUserWithQrCode(
{
apiId: this.apiId,
apiHash: this.apiHash,
},
{
qrCode: async (qrCode) => {
qrCodeCallback(qrCode);
},
password: passwordCallback,
onError: onError || ((err: Error) => {
console.error('QR code auth error:', err);
return false;
}),
}
);
// Save session after successful login
const newSessionString = this.client.session.save();
if (newSessionString && newSessionString !== this.sessionString) {
this.sessionString = newSessionString;
this.saveSession(newSessionString);
console.log('QR code authentication successful, session saved');
}
return user;
} catch (error) {
console.error('QR code authentication failed:', error);
throw error;
}
}
/**
* Get the Telegram client instance
* @throws Error if client is not initialized
+12
View File
@@ -1 +1,13 @@
/// <reference types="vite/client" />
// Node.js polyfills for browser
declare global {
interface Window {
Buffer: typeof Buffer;
process: typeof process;
util: typeof import("util");
}
var Buffer: typeof import("buffer").Buffer;
var process: typeof import("process");
var util: typeof import("util");
}
+13 -2
View File
@@ -1,12 +1,23 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// @ts-expect-error process is a nodejs global
import { nodePolyfills } from "vite-plugin-node-polyfills";
const host = process.env.TAURI_DEV_HOST;
// https://vite.dev/config/
export default defineConfig(async () => ({
plugins: [react()],
plugins: [
nodePolyfills({
include: ["buffer", "process", "util", "crypto", "stream"],
globals: {
Buffer: true,
process: true,
global: true,
},
}),
react(),
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
//
+1007 -6
View File
File diff suppressed because it is too large Load Diff