"use client";
import { useState, useCallback, useRef, useEffect } from "react";
import { Button } from "@multica/ui/components/ui/button";
import { Textarea } from "@multica/ui/components/ui/textarea";
import { Loading } from "@multica/ui/components/ui/loading";
import { useIsMobile } from "@multica/ui/hooks/use-mobile";
import { HugeiconsIcon } from "@hugeicons/react";
import {
Camera01Icon,
TextIcon,
CheckmarkCircle02Icon,
Alert02Icon,
} from "@hugeicons/core-free-icons";
import { QrScannerView } from "@multica/ui/components/qr-scanner-view";
import { parseConnectionCode } from "@multica/store";
import type { ConnectionIdentity } from "@/hooks/use-gateway-connection";
export interface DevicePairingProps {
connectionState: string;
lastError: string | null;
onConnect: (identity: ConnectionIdentity, token: string) => void;
onCancel: () => void;
}
type Mode = "scan" | "paste";
type PasteState = "idle" | "success" | "error";
/** Shown while connecting to Gateway or waiting for Owner approval */
function ConnectionStatus({
connectionState,
fullscreen,
onCancel,
}: {
connectionState: string;
fullscreen?: boolean;
onCancel: () => void;
}) {
const isVerifying = connectionState === "verifying";
const wrapper = fullscreen
? "fixed inset-0 z-50 bg-background flex flex-col items-center justify-center gap-5 px-6"
: "flex flex-col items-center justify-center h-full gap-5 px-4";
return (
{isVerifying ? "Waiting for approval" : "Connecting..."}
{isVerifying
? "The device owner needs to approve this connection on their computer"
: "Establishing connection to the agent"}
);
}
/** Shown when Owner rejects the connection, auto-dismisses after 2s */
function RejectedStatus({
fullscreen,
onDismiss,
}: {
fullscreen?: boolean;
onDismiss: () => void;
}) {
useEffect(() => {
const timer = setTimeout(onDismiss, 2000);
return () => clearTimeout(timer);
}, [onDismiss]);
const wrapper = fullscreen
? "fixed inset-0 z-50 bg-background flex flex-col items-center justify-center gap-5 px-6"
: "flex flex-col items-center justify-center h-full gap-5 px-4";
return (
Connection rejected
The device owner declined this connection
);
}
export function DevicePairing({
connectionState,
lastError,
onConnect,
onCancel,
}: DevicePairingProps) {
const [mode, setMode] = useState("scan");
const [codeInput, setCodeInput] = useState("");
const [pasteState, setPasteState] = useState("idle");
const [pasteError, setPasteError] = useState(null);
const [showRejected, setShowRejected] = useState(false);
const isMobile = useIsMobile();
const validatingRef = useRef(false);
// Detect verify rejection
useEffect(() => {
if (lastError && connectionState === "disconnected") {
setShowRejected(true);
}
}, [lastError, connectionState]);
const handleDismissRejected = useCallback(() => {
setShowRejected(false);
}, []);
const tryConnect = useCallback(
(raw: string) => {
const trimmed = raw.trim();
if (!trimmed || validatingRef.current) return;
validatingRef.current = true;
try {
const info = parseConnectionCode(trimmed);
setPasteState("success");
navigator.vibrate?.(50);
setTimeout(() => {
onConnect(
{ gateway: info.gateway, hubId: info.hubId, agentId: info.agentId },
info.token,
);
}, 600);
} catch (e) {
setPasteState("error");
setPasteError((e as Error).message || "Invalid code");
navigator.vibrate?.([30, 50, 30]);
setTimeout(() => {
setPasteState("idle");
setPasteError(null);
setCodeInput("");
}, 2000);
} finally {
validatingRef.current = false;
}
},
[onConnect],
);
const handlePaste = useCallback(
(e: React.ClipboardEvent) => {
const text = e.clipboardData.getData("text");
if (!text.trim()) return;
setTimeout(() => tryConnect(text), 50);
},
[tryConnect],
);
const handleScanResult = useCallback(
async (data: string) => {
const info = parseConnectionCode(data);
onConnect(
{ gateway: info.gateway, hubId: info.hubId, agentId: info.agentId },
info.token,
);
},
[onConnect],
);
const isInProgress =
connectionState === "connecting" ||
connectionState === "connected" ||
connectionState === "verifying";
if (showRejected) {
return (
);
}
if (isInProgress) {
return (
);
}
// Mobile: scanner only
if (isMobile) {
return (
Scan to connect
Scan a Multica QR code to connect to your agent
);
}
// Desktop: tab toggle (scan / paste)
return (
{mode === "scan" ? "Scan to connect" : "Paste to connect"}
{mode === "scan"
? "Scan a Multica QR code to connect to your agent"
: "Paste a Multica connection code to connect to your agent"}
{/* Mode toggle */}
{/* Content */}
{mode === "scan" ? (
) : (
{pasteState === "idle" && (
)}
);
}