feat: device identity for OpenClaw 2026.2.14+ pairing (#6)
- Generate Ed25519 keypair via Web Crypto API - Persist keypair in IndexedDB (survives page reloads) - Sign connect payload with device private key - Include device object in connect params (id, publicKey, signature, signedAt) - Handle NOT_PAIRED error with 'pairing' connection status - Show pairing-pending banner with instructions to run openclaw devices approve - Extract nonce from connect.challenge for v2 payload signing - Add i18n translations for pairing banner in all 8 languages Closes #6
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Wifi, Loader2 } from 'lucide-react';
|
||||
import { Wifi, Loader2, ShieldAlert } from 'lucide-react';
|
||||
import type { ConnectionStatus } from '../types';
|
||||
import { useT } from '../hooks/useLocale';
|
||||
|
||||
@@ -7,7 +7,7 @@ interface Props {
|
||||
status: ConnectionStatus;
|
||||
}
|
||||
|
||||
type BannerState = 'hidden' | 'reconnecting' | 'reconnected';
|
||||
type BannerState = 'hidden' | 'reconnecting' | 'reconnected' | 'pairing';
|
||||
|
||||
export function ConnectionBanner({ status }: Props) {
|
||||
const t = useT();
|
||||
@@ -21,8 +21,10 @@ export function ConnectionBanner({ status }: Props) {
|
||||
dismissTimer.current = null;
|
||||
}
|
||||
|
||||
if (current === 'disconnected' || current === 'connecting') {
|
||||
if (prev === 'connected') {
|
||||
if (current === 'pairing') {
|
||||
setBanner('pairing');
|
||||
} else if (current === 'disconnected' || current === 'connecting') {
|
||||
if (prev === 'connected' || prev === 'pairing') {
|
||||
setBanner('reconnecting');
|
||||
}
|
||||
} else if (current === 'connected' && prev !== null && prev !== 'connected') {
|
||||
@@ -44,18 +46,26 @@ export function ConnectionBanner({ status }: Props) {
|
||||
if (banner === 'hidden') return null;
|
||||
|
||||
const isReconnecting = banner === 'reconnecting';
|
||||
const isPairing = banner === 'pairing';
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
className={`flex items-center justify-center gap-2 px-4 py-2 text-xs font-medium transition-all duration-500 animate-in slide-in-from-top ${
|
||||
isReconnecting
|
||||
? 'bg-amber-500/10 text-amber-300 border-b border-amber-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-300 border-b border-emerald-500/20'
|
||||
isPairing
|
||||
? 'bg-blue-500/10 text-blue-300 border-b border-blue-500/20'
|
||||
: isReconnecting
|
||||
? 'bg-amber-500/10 text-amber-300 border-b border-amber-500/20'
|
||||
: 'bg-emerald-500/10 text-emerald-300 border-b border-emerald-500/20'
|
||||
}`}
|
||||
>
|
||||
{isReconnecting ? (
|
||||
{isPairing ? (
|
||||
<>
|
||||
<ShieldAlert size={14} />
|
||||
<span>{t('connection.pairing')}</span>
|
||||
</>
|
||||
) : isReconnecting ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
<span>{t('connection.reconnecting')}</span>
|
||||
|
||||
Reference in New Issue
Block a user