import { useQueryClient } from "@tanstack/react-query"; import { KeyRound, Smartphone, Timer } from "lucide-react"; import { type FC, useEffect, useRef } from "react"; import type { NativePairStatus } from "@/api/gen/model/nativePairStatus"; import { getGetNativePairingQueryKey, getListNativeClientsQueryKey, useArmNativePairing, useDisarmNativePairing, useGetNativePairing, } from "@/api/gen/native/native"; import { QueryState } from "@/components/query-state"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import type { Loadable } from "@/lib/query"; import { m } from "@/paraglide/messages"; /** Seconds → `m:ss`. */ function fmtTime(secs: number): string { const s = Math.max(0, Math.floor(secs)); return `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, "0")}`; } /** * Container: native (punktfunk/1) pairing — arm a window, poll fast while armed * for the live countdown, slow otherwise. */ export const NativePairingSection: FC = () => { const qc = useQueryClient(); const native = useGetNativePairing({ query: { refetchInterval: (q) => (q.state.data?.armed ? 1_000 : 4_000) }, }); const arm = useArmNativePairing(); const disarm = useDisarmNativePairing(); // A device pairs via the QUIC PIN ceremony, NOT through approve/deny, so nothing else // invalidates the paired-devices list on the happy path — it would stay stale until remount. // The status poll's `paired_clients` count is the pairing signal: when it rises, refresh the // list so the newly paired device appears immediately. const pairedCount = native.data?.paired_clients; const prevPairedCount = useRef(pairedCount); useEffect(() => { if ( prevPairedCount.current !== undefined && pairedCount !== undefined && pairedCount !== prevPairedCount.current ) { qc.invalidateQueries({ queryKey: getListNativeClientsQueryKey() }); } prevPairedCount.current = pairedCount; }, [pairedCount, qc]); const refresh = () => qc.invalidateQueries({ queryKey: getGetNativePairingQueryKey() }); const onArm = () => arm.mutate({ data: { ttl_secs: 120 } }, { onSuccess: refresh }); const onDisarm = () => disarm.mutate(undefined, { onSuccess: refresh }); return ( ); }; /** Native (punktfunk/1) pairing: arm a window → DISPLAY the PIN the user enters on their device. */ export const NativePairingCard: FC<{ status: Loadable; onArm: () => void; onDisarm: () => void; isArming: boolean; isDisarming: boolean; }> = ({ status, onArm, onDisarm, isArming, isDisarming }) => { const d = status.data; return ( {m.pairing_native_title()} {!d?.enabled ? ( {m.pairing_native_disabled()} ) : d.armed && d.pin ? ( {m.pairing_native_enter()} {d.pin} {d.expires_in_secs != null && ( {m.pairing_native_expires()} {fmtTime(d.expires_in_secs)} )} {m.pairing_native_cancel()} ) : ( <> {m.pairing_native_desc()} {m.pairing_native_arm()} > )} ); };
{m.pairing_native_disabled()}
{m.pairing_native_enter()}
{m.pairing_native_expires()} {fmtTime(d.expires_in_secs)}
{m.pairing_native_desc()}