diff --git a/app/playwright.config.ts b/app/playwright.config.ts index ec51054..452b9de 100644 --- a/app/playwright.config.ts +++ b/app/playwright.config.ts @@ -1,10 +1,10 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * @see https://playwright.dev/docs/test-configuration */ export default defineConfig({ - testDir: './e2e', + testDir: "./e2e", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -14,31 +14,31 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://127.0.0.1:3000', + baseURL: "http://127.0.0.1:3000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: "on-first-retry", }, /* Configure projects for major browsers */ projects: [ { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + name: "chromium", + use: { ...devices["Desktop Chrome"] }, }, { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + name: "firefox", + use: { ...devices["Desktop Firefox"] }, }, { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: "webkit", + use: { ...devices["Desktop Safari"] }, }, /* Test against mobile viewports. */ @@ -64,8 +64,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'yarn dev', - url: 'http://127.0.0.1:3000', + command: "yarn dev", + url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, }, -}); \ No newline at end of file +}); diff --git a/app/src/App.test.tsx b/app/src/App.test.tsx index 6bb6052..4a3649e 100644 --- a/app/src/App.test.tsx +++ b/app/src/App.test.tsx @@ -1,21 +1,22 @@ -import { render } from '@testing-library/react' -import { describe, it, expect } from 'vitest' -import { BrowserRouter } from 'react-router-dom' -import App from './App' -import { Provider } from './provider' +import { render } from "@testing-library/react"; +import { describe, it, expect } from "vitest"; +import { BrowserRouter } from "react-router-dom"; -describe('App', () => { - it('renders without crashing', () => { +import App from "./App"; +import { Provider } from "./provider"; + +describe("App", () => { + it("renders without crashing", () => { render( - - ) + , + ); // Just verify the app renders without throwing errors - expect(document.querySelector('body')).toBeInTheDocument() + expect(document.querySelector("body")).toBeInTheDocument(); // Debug what's actually rendered // console.log(screen.debug()) - }) -}) \ No newline at end of file + }); +}); diff --git a/app/src/App.tsx b/app/src/App.tsx index 47b5b8c..d9278b7 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { Route, Routes } from "react-router-dom"; -import { ErrorBoundary } from "@/components/error-boundary"; +import { ErrorBoundary } from "@/components/error-boundary"; import IndexPage from "@/pages/index"; import DocsPage from "@/pages/docs"; import PricingPage from "@/pages/pricing"; @@ -16,11 +16,13 @@ function App() { useEffect(() => { // Initialize device on app startup - initializeDevice().then((device) => { - setCurrentDevice(device); - }).catch((error) => { - console.error('Failed to initialize device:', error); - }); + initializeDevice() + .then((device) => { + setCurrentDevice(device); + }) + .catch((error) => { + console.error("Failed to initialize device:", error); + }); }, [setCurrentDevice]); return ( diff --git a/app/src/components/error-boundary.tsx b/app/src/components/error-boundary.tsx index a2ecd06..68795fe 100644 --- a/app/src/components/error-boundary.tsx +++ b/app/src/components/error-boundary.tsx @@ -1,6 +1,6 @@ -import React from 'react'; -import { Button } from '@heroui/button'; -import { Card, CardBody, CardHeader } from '@heroui/card'; +import React from "react"; +import { Button } from "@heroui/button"; +import { Card, CardBody, CardHeader } from "@heroui/card"; interface ErrorBoundaryState { hasError: boolean; @@ -13,7 +13,10 @@ interface ErrorBoundaryProps { fallback?: React.ComponentType<{ error?: Error; resetError: () => void }>; } -export class ErrorBoundary extends React.Component { +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; @@ -24,7 +27,7 @@ export class ErrorBoundary extends React.Component; + + return ( + + ); } - return ; + return ( + + ); } return this.props.children; @@ -57,7 +71,9 @@ function DefaultErrorFallback({ error, resetError }: ErrorFallbackProps) {
-

Something went wrong

+

+ Something went wrong +

An unexpected error occurred in the application

@@ -72,19 +88,19 @@ function DefaultErrorFallback({ error, resetError }: ErrorFallbackProps) {
)}
- - @@ -104,4 +120,4 @@ export function useErrorHandler() { // This will trigger the nearest error boundary throw error; }; -} \ No newline at end of file +} diff --git a/app/src/components/pairing/qr-display.tsx b/app/src/components/pairing/qr-display.tsx index 7bdffcd..6293d02 100644 --- a/app/src/components/pairing/qr-display.tsx +++ b/app/src/components/pairing/qr-display.tsx @@ -1,10 +1,15 @@ -import { useEffect, useState } from 'react'; -import { Card, CardBody } from '@heroui/card'; -import { Button } from '@heroui/button'; -import { Spinner } from '@heroui/spinner'; -import { generateQRCodeDataURL, generatePairingData } from '../../crypto/qr'; -import { generateDeviceFingerprint, formatSafetyWords } from '../../crypto/fingerprint'; -import type { Device } from '../../state/types'; +import type { Device } from "../../state/types"; + +import { useEffect, useState } from "react"; +import { Card, CardBody } from "@heroui/card"; +import { Button } from "@heroui/button"; +import { Spinner } from "@heroui/spinner"; + +import { generateQRCodeDataURL, generatePairingData } from "../../crypto/qr"; +import { + generateDeviceFingerprint, + formatSafetyWords, +} from "../../crypto/fingerprint"; interface QRDisplayProps { device: Device; @@ -13,10 +18,10 @@ interface QRDisplayProps { } export function QRDisplay({ device, onClose, className }: QRDisplayProps) { - const [qrCodeUrl, setQrCodeUrl] = useState(''); - const [safetyWords, setSafetyWords] = useState(''); + const [qrCodeUrl, setQrCodeUrl] = useState(""); + const [safetyWords, setSafetyWords] = useState(""); const [loading, setLoading] = useState(true); - const [error, setError] = useState(''); + const [error, setError] = useState(""); useEffect(() => { generateQRData(); @@ -25,22 +30,29 @@ export function QRDisplay({ device, onClose, className }: QRDisplayProps) { const generateQRData = async () => { try { setLoading(true); - setError(''); + setError(""); // Generate pairing data (ICE servers now use defaults, not embedded in QR) - const signalingURL = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/signaling`; - + const signalingURL = `${window.location.protocol === "https:" ? "wss:" : "ws:"}//${window.location.host}/ws/signaling`; + const pairingData = generatePairingData(device, signalingURL); - + // Generate QR code const qrUrl = await generateQRCodeDataURL(pairingData); + setQrCodeUrl(qrUrl); // Generate safety words for verification - const fingerprint = await generateDeviceFingerprint(device.id, device.pubKeyJwk); + const fingerprint = await generateDeviceFingerprint( + device.id, + device.pubKeyJwk, + ); + setSafetyWords(formatSafetyWords(fingerprint.safetyWords)); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to generate QR code'); + setError( + err instanceof Error ? err.message : "Failed to generate QR code", + ); } finally { setLoading(false); } @@ -94,11 +106,7 @@ export function QRDisplay({ device, onClose, className }: QRDisplayProps) { {/* QR Code */}
- Pairing QR Code + Pairing QR Code
{/* Device Info */} @@ -124,18 +132,10 @@ export function QRDisplay({ device, onClose, className }: QRDisplayProps) { {/* Actions */}
- -
@@ -149,4 +149,4 @@ export function QRDisplay({ device, onClose, className }: QRDisplayProps) { ); -} \ No newline at end of file +} diff --git a/app/src/components/pairing/qr-scanner.tsx b/app/src/components/pairing/qr-scanner.tsx index cf627de..33fb5e1 100644 --- a/app/src/components/pairing/qr-scanner.tsx +++ b/app/src/components/pairing/qr-scanner.tsx @@ -1,11 +1,20 @@ -import { useRef, useEffect, useState } from 'react'; -import { Card, CardBody } from '@heroui/card'; -import { Button } from '@heroui/button'; -import { Spinner } from '@heroui/spinner'; -import { QRScanner } from '../../crypto/scanner'; -import { parsePairingData, validatePairingTimestamp, expandPublicKey } from '../../crypto/qr'; -import { generateDeviceFingerprint, formatSafetyWords } from '../../crypto/fingerprint'; -import type { PairingQRData } from '../../crypto/qr'; +import type { PairingQRData } from "../../crypto/qr"; + +import { useRef, useEffect, useState } from "react"; +import { Card, CardBody } from "@heroui/card"; +import { Button } from "@heroui/button"; +import { Spinner } from "@heroui/spinner"; + +import { QRScanner } from "../../crypto/scanner"; +import { + parsePairingData, + validatePairingTimestamp, + expandPublicKey, +} from "../../crypto/qr"; +import { + generateDeviceFingerprint, + formatSafetyWords, +} from "../../crypto/fingerprint"; interface QRScannerProps { onScanSuccess: (data: PairingQRData) => void; @@ -13,18 +22,23 @@ interface QRScannerProps { className?: string; } -export function QRScannerComponent({ onScanSuccess, onCancel, className }: QRScannerProps) { +export function QRScannerComponent({ + onScanSuccess, + onCancel, + className, +}: QRScannerProps) { const videoRef = useRef(null); const scannerRef = useRef(null); - + const [isScanning, setIsScanning] = useState(false); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const [scanResult, setScanResult] = useState(null); - const [safetyWords, setSafetyWords] = useState(''); + const [safetyWords, setSafetyWords] = useState(""); const [showVerification, setShowVerification] = useState(false); useEffect(() => { startScanning(); + return () => { stopScanning(); }; @@ -35,20 +49,16 @@ export function QRScannerComponent({ onScanSuccess, onCancel, className }: QRSca try { setIsScanning(true); - setError(''); + setError(""); scannerRef.current = new QRScanner(videoRef.current); - - await scannerRef.current.start( - handleScanResult, - handleScanError, - { - preferredCamera: 'back', - maxScanTime: 60000, // 1 minute timeout - } - ); + + await scannerRef.current.start(handleScanResult, handleScanError, { + preferredCamera: "back", + maxScanTime: 60000, // 1 minute timeout + }); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to start camera'); + setError(err instanceof Error ? err.message : "Failed to start camera"); setIsScanning(false); } }; @@ -65,22 +75,26 @@ export function QRScannerComponent({ onScanSuccess, onCancel, className }: QRSca try { // Parse QR code data const pairingData = parsePairingData(result.data); - + // Validate timestamp if (!validatePairingTimestamp(pairingData)) { - throw new Error('QR code has expired. Please generate a new one.'); + throw new Error("QR code has expired. Please generate a new one."); } // Generate safety words for verification (expand compact key format) const fullPublicKey = expandPublicKey(pairingData.key); - const fingerprint = await generateDeviceFingerprint(pairingData.id, fullPublicKey); + const fingerprint = await generateDeviceFingerprint( + pairingData.id, + fullPublicKey, + ); + setSafetyWords(formatSafetyWords(fingerprint.safetyWords)); - + setScanResult(pairingData); setShowVerification(true); stopScanning(); } catch (err) { - setError(err instanceof Error ? err.message : 'Invalid QR code'); + setError(err instanceof Error ? err.message : "Invalid QR code"); // Continue scanning } }; @@ -99,12 +113,12 @@ export function QRScannerComponent({ onScanSuccess, onCancel, className }: QRSca const handleVerificationCancel = () => { setShowVerification(false); setScanResult(null); - setSafetyWords(''); + setSafetyWords(""); startScanning(); // Resume scanning }; const handleRetry = () => { - setError(''); + setError(""); startScanning(); }; @@ -144,17 +158,17 @@ export function QRScannerComponent({ onScanSuccess, onCancel, className }: QRSca {/* Actions */}
- - @@ -184,11 +198,11 @@ export function QRScannerComponent({ onScanSuccess, onCancel, className }: QRSca