import { useMemo, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { PushTotpCodeForm } from "@/components/PushTotpCodeForm"; import { useTotpCodesQuery } from "@/hooks/useTotpCodesQuery"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import type { OtpType, TotpCode } from "@/api/types"; import { Skeleton } from "@/components/ui/skeleton"; type OtpTypeFilter = "all" | OtpType; const LIMIT_OPTIONS = [25, 50, 100] as const; function formatDateTime(value: string | null): string { if (!value) { return "—"; } const date = new Date(value); if (Number.isNaN(date.getTime())) { return value; } return date.toLocaleString(); } function renderCodeContent(code: TotpCode): string { if (!code.code) { return "—"; } return code.code; } function CredentialsTotpTab() { const [identifierFilter, setIdentifierFilter] = useState(""); const [otpTypeFilter, setOtpTypeFilter] = useState("all"); const [limit, setLimit] = useState<(typeof LIMIT_OPTIONS)[number]>(50); const queryClient = useQueryClient(); const queryParams = useMemo(() => { return { totp_identifier: identifierFilter.trim() || undefined, otp_type: otpTypeFilter === "all" ? undefined : otpTypeFilter, limit, }; }, [identifierFilter, limit, otpTypeFilter]); const { data, isLoading, isFetching, isFeatureUnavailable } = useTotpCodesQuery({ params: queryParams, }); const codes = data ?? []; const hasFilters = identifierFilter.trim() !== "" || otpTypeFilter !== "all" || limit !== 50; const handleFormSuccess = () => { void queryClient.invalidateQueries({ queryKey: ["totpCodes"], }); }; return (

Push a 2FA Code

Paste the verification message you received. Skyvern extracts the code and attaches it to the relevant run.

setIdentifierFilter(event.target.value)} />
{isFeatureUnavailable && ( 2FA listing unavailable Upgrade the backend to include{" "} GET /v1/credentials/totp. Once available, this tab will automatically populate with codes. )} {!isFeatureUnavailable && (
Identifier Code Source Workflow Run Created Expires {isLoading || isFetching ? (
) : null} {!isLoading && !isFetching && codes.length === 0 ? ( No 2FA codes yet. Paste a verification message above or configure automatic forwarding. ) : null} {!isLoading && !isFetching && codes.map((code) => ( {code.totp_identifier ?? "—"} {renderCodeContent(code)} {code.otp_type ?? "unknown"} {code.source ? ( {code.source} ) : null} {code.workflow_run_id ?? "—"} {formatDateTime(code.created_at)} {formatDateTime(code.expired_at)} ))}
)}
); } export { CredentialsTotpTab };