diff --git a/apps/desktop/src/renderer/src/components/qr-code.tsx b/apps/desktop/src/renderer/src/components/qr-code.tsx index 6ea9a019..413f37d6 100644 --- a/apps/desktop/src/renderer/src/components/qr-code.tsx +++ b/apps/desktop/src/renderer/src/components/qr-code.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from 'react' +import { useState, useCallback, useMemo } from 'react' import { QRCodeSVG } from 'qrcode.react' import { Button } from '@multica/ui/components/ui/button' import { Copy, Check } from 'lucide-react' +import { useQRToken, useCountdown } from './qr-hooks' // ============ Types ============ @@ -22,65 +23,7 @@ export interface ConnectionQRCodeProps { size?: number } -// ============ Hooks ============ - -/** Generate a secure random token */ -function generateToken(): string { - return crypto.randomUUID() -} - -/** - * Hook to manage QR token lifecycle - * - Generates token on mount - * - Auto-refreshes when expired - * - Registers token with Hub - */ -export function useQRToken(agentId: string, expirySeconds: number) { - const [token, setToken] = useState(generateToken) - const [expiresAt, setExpiresAt] = useState(() => Date.now() + expirySeconds * 1000) - - const refresh = useCallback(() => { - const newToken = generateToken() - const newExpiry = Date.now() + expirySeconds * 1000 - setToken(newToken) - setExpiresAt(newExpiry) - window.electronAPI?.hub.registerToken(newToken, agentId, newExpiry) - }, [agentId, expirySeconds]) - - // Register initial token - useEffect(() => { - window.electronAPI?.hub.registerToken(token, agentId, expiresAt) - }, []) // eslint-disable-line react-hooks/exhaustive-deps - - return { token, expiresAt, refresh } -} - -/** - * Hook for countdown timer - * Returns remaining seconds, auto-updates every second - */ -export function useCountdown(expiresAt: number, onExpire: () => void) { - const [remaining, setRemaining] = useState(() => - Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)) - ) - const onExpireRef = useRef(onExpire) - onExpireRef.current = onExpire - - useEffect(() => { - // Reset when expiresAt changes - setRemaining(Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000))) - - const id = setInterval(() => { - const next = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)) - setRemaining(next) - if (next === 0) onExpireRef.current() - }, 1000) - - return () => clearInterval(id) - }, [expiresAt]) - - return remaining -} +// Hooks are in ./qr-hooks.ts (separate file for react-refresh compatibility) /** * Hook for clipboard copy with feedback diff --git a/apps/desktop/src/renderer/src/components/qr-hooks.ts b/apps/desktop/src/renderer/src/components/qr-hooks.ts new file mode 100644 index 00000000..6e250335 --- /dev/null +++ b/apps/desktop/src/renderer/src/components/qr-hooks.ts @@ -0,0 +1,59 @@ +import { useState, useEffect, useCallback, useRef } from 'react' + +/** Generate a secure random token */ +function generateToken(): string { + return crypto.randomUUID() +} + +/** + * Hook to manage QR token lifecycle + * - Generates token on mount + * - Auto-refreshes when expired + * - Registers token with Hub + */ +export function useQRToken(agentId: string, expirySeconds: number) { + const [token, setToken] = useState(generateToken) + const [expiresAt, setExpiresAt] = useState(() => Date.now() + expirySeconds * 1000) + + const refresh = useCallback(() => { + const newToken = generateToken() + const newExpiry = Date.now() + expirySeconds * 1000 + setToken(newToken) + setExpiresAt(newExpiry) + window.electronAPI?.hub.registerToken(newToken, agentId, newExpiry) + }, [agentId, expirySeconds]) + + // Register initial token + useEffect(() => { + window.electronAPI?.hub.registerToken(token, agentId, expiresAt) + }, []) // eslint-disable-line react-hooks/exhaustive-deps + + return { token, expiresAt, refresh } +} + +/** + * Hook for countdown timer + * Returns remaining seconds, auto-updates every second + */ +export function useCountdown(expiresAt: number, onExpire: () => void) { + const [remaining, setRemaining] = useState(() => + Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)) + ) + const onExpireRef = useRef(onExpire) + onExpireRef.current = onExpire + + useEffect(() => { + // Reset when expiresAt changes + setRemaining(Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000))) + + const id = setInterval(() => { + const next = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000)) + setRemaining(next) + if (next === 0) onExpireRef.current() + }, 1000) + + return () => clearInterval(id) + }, [expiresAt]) + + return remaining +} diff --git a/apps/desktop/src/renderer/src/components/telegram-qr.tsx b/apps/desktop/src/renderer/src/components/telegram-qr.tsx index be957a3e..6157f6cb 100644 --- a/apps/desktop/src/renderer/src/components/telegram-qr.tsx +++ b/apps/desktop/src/renderer/src/components/telegram-qr.tsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' import { QRCodeSVG } from 'qrcode.react' import { Loader2 } from 'lucide-react' -import { useQRToken, useCountdown, QRCodeFrame, ExpiryTimer } from './qr-code' +import { useQRToken, useCountdown } from './qr-hooks' +import { QRCodeFrame, ExpiryTimer } from './qr-code' export interface TelegramConnectQRProps { gateway: string diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx index e83e489e..5ad004ed 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/connect-step.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback, useRef } from 'react' +import { useEffect, useState, useCallback } from 'react' import { Button } from '@multica/ui/components/ui/button' import { Separator } from '@multica/ui/components/ui/separator' import { ChevronLeft, Info, Check, Smartphone } from 'lucide-react'