"use client"; import { useState, useEffect, useCallback } from "react"; interface DeviceTokenData { clawId: string; apiKey: string; name: string; deviceId: string; } interface UseDeviceTokenReturn { token: string | null; clawId: string | null; name: string | null; isLoading: boolean; error: string | null; updateName: (newName: string) => Promise; } const STORAGE_KEY = "openclaw_device"; function generateUUID(): string { if (typeof crypto !== "undefined" && crypto.randomUUID) { return crypto.randomUUID(); } return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === "x" ? r : (r & 0x3) | 0x8; return v.toString(16); }); } function getDeviceInfo(): { platform: string; browser: string; screen: string; language: string; } { if (typeof navigator === "undefined") { return { platform: "unknown", browser: "unknown", screen: "unknown", language: "en" }; } const ua = navigator.userAgent; let browser = "Unknown"; if (ua.includes("Chrome") && !ua.includes("Edg")) browser = "Chrome"; else if (ua.includes("Firefox")) browser = "Firefox"; else if (ua.includes("Safari") && !ua.includes("Chrome")) browser = "Safari"; else if (ua.includes("Edg")) browser = "Edge"; return { platform: navigator.platform || "unknown", browser, screen: typeof screen !== "undefined" ? `${screen.width}x${screen.height}` : "unknown", language: navigator.language || "en", }; } function getCachedData(): DeviceTokenData | null { if (typeof localStorage === "undefined") return null; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; return JSON.parse(raw) as DeviceTokenData; } catch { return null; } } function setCachedData(data: DeviceTokenData): void { if (typeof localStorage === "undefined") return; localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } function updateCachedName(newName: string): void { const data = getCachedData(); if (data) { setCachedData({ ...data, name: newName }); } } export function useDeviceToken(): UseDeviceTokenReturn { const [token, setToken] = useState(null); const [clawId, setClawId] = useState(null); const [name, setName] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const init = async () => { // Check cache first const cached = getCachedData(); if (cached) { setToken(cached.apiKey); setClawId(cached.clawId); setName(cached.name); setIsLoading(false); return; } // Register new device try { const deviceId = generateUUID(); const deviceInfo = getDeviceInfo(); const res = await fetch("/api/v1/device/register", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ deviceId, ...deviceInfo, }), }); if (!res.ok) { throw new Error(`Registration failed: ${res.status}`); } const data = await res.json(); const tokenData: DeviceTokenData = { clawId: data.clawId, apiKey: data.apiKey, name: data.name, deviceId, }; setCachedData(tokenData); setToken(data.apiKey); setClawId(data.clawId); setName(data.name); } catch (err) { setError(err instanceof Error ? err.message : "Registration failed"); } finally { setIsLoading(false); } }; init(); }, []); const updateName = useCallback( async (newName: string): Promise => { if (!token) return false; try { const res = await fetch("/api/v1/device/name", { method: "PUT", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name: newName }), }); if (!res.ok) return false; setName(newName); updateCachedName(newName); return true; } catch { return false; } }, [token] ); return { token, clawId, name, isLoading, error, updateName }; }