583 lines
24 KiB
TypeScript

import { useMemo, useState, useEffect } from "react";
type Status =
| "ready"
| "saved successfully"
| "injection started"
| "validating key..."
| "invalid key"
| "injecting..."
| "RL Running"
| "RL Closed"
| string;
interface UserInfo {
userId: string | null;
discordId: string | null;
epicId: string | null;
username: string | null;
globalName: string | null;
logins?: number;
}
interface KeyValidationResponse {
status: string;
user: UserInfo | null;
}
const LS_KEYS = {
spoofed: "neonGlass.spoofedUsername",
apiKey: "neonGlass.apiKey",
minimizeToTray: "neonGlass.minimizeToTray",
platform: "neonGlass.platform",
autoInject: "neonGlass.autoInject",
} as const;
const GITHUB_URL = "https://github.com/RLidentity";
const FAQ_URL = "https://rlidentity.me/faq";
function isTauriRuntime() {
return typeof window !== "undefined" && typeof (window as any).__TAURI_INTERNALS__ !== "undefined";
}
async function tryWindowApi(action: (win: any) => Promise<void>) {
if (!isTauriRuntime()) return;
const mod: any = await import("@tauri-apps/api/window");
const win = mod.getCurrentWindow();
await action(win);
}
async function tryInvoke<T>(cmd: string, args?: Record<string, unknown>) {
if (!isTauriRuntime()) return null;
const mod: any = await import("@tauri-apps/api/core");
return (await mod.invoke(cmd, args)) as T;
}
async function openUrl(url: string) {
const fallback = () => window.open(url, "_blank", "noopener,noreferrer");
if (!isTauriRuntime()) return fallback();
try {
const mod: any = await import("@tauri-apps/plugin-opener");
if (typeof mod.openUrl === "function") {
await mod.openUrl(url);
return;
}
fallback();
} catch {
fallback();
}
}
export default function App() {
const initialApiKey = useMemo(() => localStorage.getItem(LS_KEYS.apiKey) ?? "", []);
const initialSpoofed = useMemo(() => localStorage.getItem(LS_KEYS.spoofed) ?? "", []);
const initialMinToTray = useMemo(() => localStorage.getItem(LS_KEYS.minimizeToTray) === "true", []);
const initialPlatform = useMemo(() => localStorage.getItem(LS_KEYS.platform) ?? "Epic", []);
const initialAutoInject = useMemo(() => localStorage.getItem(LS_KEYS.autoInject) === "true", []);
const [apiKey, setApiKey] = useState(initialApiKey);
const [spoofedUsername, setSpoofedUsername] = useState(initialSpoofed);
const [isAuthorized, setIsAuthorized] = useState(false);
const [isRevoked, setIsRevoked] = useState(false);
const [userData, setUserData] = useState<UserInfo | null>(null);
const [status, setStatus] = useState<Status>("ready");
const [rlStatus, setRlStatus] = useState("Checking...");
const [settingsOpen, setSettingsOpen] = useState(false);
const [logsOpen, setLogsOpen] = useState(false);
const [lastLog, setLastLog] = useState("");
const [minimizeToTray, setMinimizeToTray] = useState(initialMinToTray);
const [platform, setPlatform] = useState(initialPlatform);
const [autoInject, setAutoInject] = useState(initialAutoInject);
const [platformPickerOpen, setPlatformPickerOpen] = useState(false);
// Easter Egg State
const [debugOpen, setDebugOpen] = useState(false);
const [logoClicks, setLogoClicks] = useState(0);
const handleLogoClick = (e: React.MouseEvent) => {
e.stopPropagation();
setLogoClicks(prev => prev + 1);
};
useEffect(() => {
if (logoClicks >= 5) {
setDebugOpen(true);
setLogoClicks(0);
}
const timer = setTimeout(() => {
if (logoClicks > 0) setLogoClicks(0);
}, 1000);
return () => clearTimeout(timer);
}, [logoClicks]);
// Tutorial State
const [tutorialStep, setTutorialStep] = useState(-1);
// Startup Authorization & Update Check
useEffect(() => {
if (initialApiKey) {
authorize(initialApiKey);
}
syncAssetsAndCheckUpdates();
}, []);
async function syncAssetsAndCheckUpdates() {
if (!isTauriRuntime()) return;
// 1. Download DLL and Injector
try {
await tryInvoke("download_assets");
console.log("Assets synced successfully");
} catch (e) {
console.error("Failed to sync assets:", e);
}
// 2. Check for App updates
checkForUpdates();
}
async function checkForUpdates() {
if (!isTauriRuntime()) return;
try {
const { check } = await import("@tauri-apps/plugin-updater");
const update = await check();
if (update) {
console.log(`Update available: ${update.version}`);
const confirmed = window.confirm(`A new version (${update.version}) is available. Would you like to update?`);
if (confirmed) {
setStatus("Updating...");
await update.downloadAndInstall();
// The app will restart automatically after install on some platforms,
// or we might need to relaunch. Tauri v2 updater usually handles this.
const { relaunch } = await import("@tauri-apps/plugin-process");
await relaunch();
}
}
} catch (e) {
console.error("Failed to check for updates:", e);
}
}
// Sync revoked background to body
useEffect(() => {
if (isRevoked) {
document.body.classList.add('revoked-bg');
} else {
document.body.classList.remove('revoked-bg');
}
}, [isRevoked]);
// Poll for Rocket League status & Auto Inject
useEffect(() => {
if (!isAuthorized || isRevoked) return;
const interval = setInterval(async () => {
try {
const res = await tryInvoke<{is_running: boolean}>("check_status");
if (res) {
const wasRunning = rlStatus === "RL Running";
const isRunning = res.is_running;
setRlStatus(isRunning ? "RL Running" : "RL Closed");
// Auto Inject Logic: if it just started running and autoInject is on
if (!wasRunning && isRunning && autoInject) {
inject();
}
}
} catch (e) {
console.error(e);
}
}, 2000);
return () => clearInterval(interval);
}, [isAuthorized, isRevoked, rlStatus, autoInject]);
async function authorize(keyToTry: string) {
if (!keyToTry.trim()) {
setStatus("Please enter a key");
return;
}
setStatus("validating key...");
setIsRevoked(false);
try {
const hwid = await tryInvoke<string>("get_hwid") || "UNKNOWN";
const res = await tryInvoke<KeyValidationResponse>("validate_key", { key: keyToTry.trim(), hwid });
if (res && res.status === "valid") {
localStorage.setItem(LS_KEYS.apiKey, keyToTry.trim());
setUserData(res.user);
setIsAuthorized(true);
setIsRevoked(false);
setStatus("ready");
// Check for tutorial
if (res.user?.logins === 0) {
setTutorialStep(0);
}
} else if (res && res.status === "revoked") {
setIsRevoked(true);
setIsAuthorized(false);
setStatus("Error: Key Revoked");
} else if (res && res.status === "invalid_hwid") {
setStatus("Error: Key locked to another PC");
setIsAuthorized(false);
} else {
setStatus("Error: Invalid key");
setIsAuthorized(false);
}
} catch (e) {
setStatus("Network Error: Check connection");
}
}
async function saveConfig() {
localStorage.setItem(LS_KEYS.spoofed, spoofedUsername.trim());
localStorage.setItem(LS_KEYS.platform, platform);
try {
await tryInvoke("save_config", { name: spoofedUsername.trim(), platform });
setStatus("saved successfully");
window.setTimeout(() => setStatus("ready"), 1400);
} catch (e) {
setStatus("Save error: " + String(e));
}
}
async function inject() {
setStatus("injecting...");
try {
const res = await tryInvoke<string>("inject_dll", { discordId: userData?.discordId });
setLastLog(res || "Successfully Injected!");
setStatus("Successfully Injected!");
window.setTimeout(() => setStatus("ready"), 1400);
} catch (e) {
setStatus("Injection Failed!");
setLastLog(String(e));
setLogsOpen(true);
}
}
function toggleMinimizeToTray(next: boolean) {
setMinimizeToTray(next);
localStorage.setItem(LS_KEYS.minimizeToTray, String(next));
}
function toggleAutoInject(next: boolean) {
setAutoInject(next);
localStorage.setItem(LS_KEYS.autoInject, String(next));
}
async function handleMinimizeClick() {
if (!isTauriRuntime()) return;
if (minimizeToTray) {
await tryInvoke("minimize_to_tray");
} else {
await tryWindowApi((w) => w.minimize());
}
}
const logout = () => {
localStorage.removeItem(LS_KEYS.apiKey);
window.location.reload();
};
const tutorialSteps = [
{ title: "Welcome to RLidentity", text: "Let's show you around. First, enter your spoofed username here.", target: "input" },
{ title: "Injection", text: "Once Rocket League is running, click Inject to start. Or enable Auto-Inject in settings!", target: "btn-primary" },
{ title: "Settings", text: "Customize your experience here. Change your platform or toggle Auto-Injection.", target: "tb-action" },
{ title: "All Set!", text: "You're ready to win. Happy gaming!", target: "none" }
];
if (!isAuthorized) {
return (
<div className="app-shell">
<div className={`bg-aurora ${isRevoked ? 'revoked-aurora' : ''}`} aria-hidden="true" />
<div className="window-titlebar" data-tauri-drag-region>
<div className="window-titlebar-left">
<img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
<div className="titlebar-text" data-tauri-drag-region>
<div className="app-name neon-text-soft">RLidentity <span style={{ fontSize: '10px', opacity: 0.6, marginLeft: '4px' }}>v2.0.0</span></div>
<div className="app-slogan">{isRevoked ? 'License Revoked' : 'Authorize to continue'}</div>
</div>
</div>
<div className="titlebar-controls">
<button className="win-btn" onClick={handleMinimizeClick}></button>
<button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}></button>
</div>
</div>
<main className="panel-wrap">
<section className={`glass-card neon-ring ${isRevoked ? 'revoked' : ''}`} style={{ maxWidth: '400px', margin: 'auto' }}>
<header className="card-header">
<h1 className={`headline neon-text ${isRevoked ? 'red' : ''}`}>
{isRevoked ? 'ACCESS REVOKED' : 'RLidentity'}
</h1>
<p className="app-slogan">
{isRevoked ? 'This license is no longer active' : 'Enter your API key to continue'}
</p>
</header>
<div className="form-stack">
{!isRevoked && (
<div className="field">
<label className="label">License Key</label>
<div className="glass-input">
<input
type="password"
className="input"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="Enter your license key..."
/>
</div>
</div>
)}
<button className="btn btn-primary" onClick={() => isRevoked ? logout() : authorize(apiKey)}>
{isRevoked ? 'Change Key' : 'Login'}
</button>
{status !== "ready" && (
<p className="status-value" style={{textAlign:'center', marginTop:'10px', color: (isRevoked || status.startsWith('Error')) ? '#ff5555' : 'inherit'}}>
{status}
</p>
)}
</div>
</section>
</main>
</div>
);
}
return (
<div className="app-shell">
<div className="bg-aurora" aria-hidden="true" />
<div className="window-titlebar" data-tauri-drag-region>
<div className="window-titlebar-left">
<img src="/rlidentity.webp" className="app-logo" alt="logo" onClick={handleLogoClick} draggable="false" style={{ cursor: 'default' }} />
<div className="titlebar-app-name neon-text-soft" data-tauri-drag-region>RLidentity</div>
</div>
<div className="titlebar-controls-wrap" data-tauri-drag-region>
<div className="titlebar-controls">
<button id="step-settings" className="tb-action" onClick={() => setSettingsOpen(true)}>Settings</button>
<button className="tb-action" onClick={() => openUrl(GITHUB_URL)}>GitHub</button>
<button className="win-btn" onClick={handleMinimizeClick}></button>
<button className="win-btn" onClick={async () => await tryWindowApi(w => w.close())}></button>
</div>
</div>
</div>
<main className="panel-wrap">
<header className="welcome-section">
<h2 className="welcome-text">Welcome, <span className="neon-text-soft">{userData?.globalName || userData?.username || "User"}</span></h2>
<div className="user-id-badge">User #{userData?.userId || "0"}</div>
</header>
<section className="glass-card neon-ring">
<header className="card-header">
<h1 className="headline neon-text">Be anyone, Win everything</h1>
</header>
<div className="form-stack">
<div className="field" id="step-username">
<label className="label">spoofed username</label>
<div className="glass-input">
<input
className="input"
value={spoofedUsername}
onChange={(e) => setSpoofedUsername(e.target.value)}
/>
</div>
</div>
<button
id="step-inject"
className="btn btn-primary"
onClick={inject}
disabled={rlStatus !== "RL Running" && tutorialStep !== 1}
>
{rlStatus === "RL Running" ? "Inject" : "Start Rocket League"}
</button>
<div className="btn-row">
<button className="btn btn-secondary" onClick={() => openUrl(FAQ_URL)}>FAQ</button>
<button className="btn btn-tertiary" onClick={saveConfig}>Save Name</button>
</div>
</div>
<footer className="status-bar">
<div className="status-item">
<span className="status-label">status</span>
<span className="status-value">{status}</span>
</div>
<div className="status-item">
<span className="status-label">game</span>
<span className="status-value">{rlStatus}</span>
</div>
</footer>
</section>
</main>
{(settingsOpen || logsOpen || debugOpen) && (
<div className="modal-overlay" onClick={() => { setSettingsOpen(false); setLogsOpen(false); setDebugOpen(false); }}>
<div className="modal glass-card neon-ring" onClick={e => e.stopPropagation()} style={{ maxWidth: (logsOpen || debugOpen) ? '600px' : '400px' }}>
{debugOpen ? (
<>
<h2 className="modal-title neon-text-soft">System Credits & Debug</h2>
<div className="glass-input" style={{ padding: '15px', marginBottom: '15px' }}>
<div style={{ display: 'grid', gap: '10px', fontSize: '13px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Lead Dev & Owner:</span>
<span className="neon-text-soft">Bits</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Dev & Admin:</span>
<span style={{ color: '#fff' }}>Danni</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Co-Owner:</span>
<span style={{ color: '#fff' }}>Deniz</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Administrator:</span>
<span style={{ color: '#fff' }}>Kairo</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Helpers:</span>
<span style={{ color: '#fff' }}>Quinn, SNDR</span>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span style={{ color: '#aaa' }}>Tester:</span>
<span style={{ color: '#fff' }}>Emir</span>
</div>
<hr style={{ border: 'none', borderTop: '1px solid rgba(255,255,255,0.1)', margin: '5px 0' }} />
<div style={{ textAlign: 'center' }}>
<button
className="btn btn-secondary"
style={{ width: '100%', marginTop: '5px' }}
onClick={() => openUrl('https://rlidentity.me/discord')}
>
Join Discord (rlidentity.me/discord)
</button>
</div>
</div>
</div>
<button className="btn btn-primary" onClick={() => setDebugOpen(false)}>Close Debug</button>
</>
) : logsOpen ? (
<>
<h2 className="modal-title neon-text-soft">Injection Logs</h2>
<div className="glass-input" style={{ height: '300px', padding: '10px', overflowY: 'auto' }}>
<pre style={{ fontSize: '12px', color: '#fff', whiteSpace: 'pre-wrap' }}>
{lastLog || "No logs yet..."}
</pre>
</div>
<button className="btn btn-primary" onClick={() => setLogsOpen(false)}>Close Logs</button>
</>
) : (
<>
<h2 className="modal-title neon-text-soft">Settings</h2>
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Platform</div>
</div>
<div className="custom-dropdown-wrap">
<button
className="glass-input dropdown-trigger"
onClick={() => setPlatformPickerOpen(!platformPickerOpen)}
>
<span className="dropdown-value">{platform === "Epic" ? "Epic Games" : "Steam"}</span>
<span className="dropdown-arrow"></span>
</button>
{platformPickerOpen && (
<div className="dropdown-menu glass-card neon-ring">
<button
className={`dropdown-item ${platform === "Epic" ? "active" : ""}`}
onClick={() => { setPlatform("Epic"); setPlatformPickerOpen(false); }}
>
Epic Games
</button>
<button
className={`dropdown-item ${platform === "Steam" ? "active" : ""}`}
onClick={() => { setPlatform("Steam"); setPlatformPickerOpen(false); }}
>
Steam
</button>
</div>
)}
</div>
</div>
<div className="setting-row" id="step-autoinject">
<div className="setting-text">
<div className="setting-title">Auto Injection</div>
<div className="setting-sub">Injects automatically when RL starts</div>
</div>
<label className="switch">
<input type="checkbox" checked={autoInject} onChange={e => toggleAutoInject(e.target.checked)} />
<span className="switch-ui" />
</label>
</div>
<div className="setting-row">
<div className="setting-text">
<div className="setting-title">Minimize to tray</div>
</div>
<label className="switch">
<input type="checkbox" checked={minimizeToTray} onChange={e => toggleMinimizeToTray(e.target.checked)} />
<span className="switch-ui" />
</label>
</div>
<div className="btn-row" style={{ flexDirection: 'column', gap: '10px', marginTop: '10px' }}>
<button className="btn btn-tertiary" onClick={() => { setSettingsOpen(false); setLogsOpen(true); }}>View Last Injection Log</button>
<button className="btn btn-secondary" onClick={logout}>Logout</button>
<button className="btn btn-primary" onClick={() => setSettingsOpen(false)}>Close</button>
</div>
</>
)}
</div>
</div>
)}
{tutorialStep >= 0 && (
<div className="tutorial-overlay">
<div className={`tutorial-spotlight step-${tutorialStep}`} />
<div className={`tutorial-card glass-card neon-ring step-${tutorialStep}`}>
<h2 className="modal-title neon-text">{tutorialSteps[tutorialStep].title}</h2>
<p className="modal-p">{tutorialSteps[tutorialStep].text}</p>
<div className="btn-row">
<button
className="btn btn-secondary"
onClick={() => setTutorialStep(-1)}
>
Skip
</button>
<button
className="btn btn-primary"
onClick={() => {
if (tutorialStep === 2) {
setSettingsOpen(true);
}
if (tutorialStep < tutorialSteps.length - 1) {
setTutorialStep(tutorialStep + 1);
} else {
setSettingsOpen(false);
setTutorialStep(-1);
}
}}
>
{tutorialStep < tutorialSteps.length - 1 ? "Next" : "Finish"}
</button>
</div>
</div>
</div>
)}
</div>
);
}