Files
openclaw-market/hooks/use-device-token.ts

172 lines
4.3 KiB
TypeScript

"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<boolean>;
}
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<string | null>(null);
const [clawId, setClawId] = useState<string | null>(null);
const [name, setName] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(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<boolean> => {
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 };
}