diff --git a/src/components/CopyTextBox.tsx b/src/components/CopyTextBox.tsx index e516bf0d6..5f12a7f39 100644 --- a/src/components/CopyTextBox.tsx +++ b/src/components/CopyTextBox.tsx @@ -8,29 +8,40 @@ import { useTranslations } from "next-intl"; type CopyTextBoxProps = { text?: string; displayText?: string; + getCopyText?: () => Promise; wrapText?: boolean; outline?: boolean; + centered?: boolean; }; export default function CopyTextBox({ text = "", displayText, + getCopyText, wrapText = false, - outline = true + outline = true, + centered = false }: CopyTextBoxProps) { const [isCopied, setIsCopied] = useState(false); + const [isCopying, setIsCopying] = useState(false); const textRef = useRef(null); const t = useTranslations(); const copyToClipboard = async () => { - if (textRef.current) { - try { - await navigator.clipboard.writeText(text); - setIsCopied(true); - setTimeout(() => setIsCopied(false), 2000); - } catch (err) { - console.error(t("copyTextFailed"), err); - } + if (!textRef.current || isCopying) { + return; + } + + setIsCopying(true); + try { + const value = getCopyText ? await getCopyText() : text; + await navigator.clipboard.writeText(value); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (err) { + console.error(t("copyTextFailed"), err); + } finally { + setIsCopying(false); } }; @@ -40,7 +51,9 @@ export default function CopyTextBox({ >
                 {isCopied ? (
diff --git a/src/components/CopyToClipboard.tsx b/src/components/CopyToClipboard.tsx
index 147972e78..344a2996a 100644
--- a/src/components/CopyToClipboard.tsx
+++ b/src/components/CopyToClipboard.tsx
@@ -1,5 +1,5 @@
 import { cn } from "@app/lib/cn";
-import { Check, Copy } from "lucide-react";
+import { Check, Copy, Loader2 } from "lucide-react";
 import Link from "next/link";
 import { useState } from "react";
 import { useTranslations } from "next-intl";
@@ -7,6 +7,7 @@ import { useTranslations } from "next-intl";
 type CopyToClipboardProps = {
     text: string;
     displayText?: string;
+    getCopyText?: () => Promise;
     isLink?: boolean;
     className?: string;
 };
@@ -14,18 +15,31 @@ type CopyToClipboardProps = {
 const CopyToClipboard = ({
     text,
     displayText,
+    getCopyText,
     isLink,
     className
 }: CopyToClipboardProps) => {
     const [copied, setCopied] = useState(false);
+    const [copying, setCopying] = useState(false);
 
-    const handleCopy = () => {
-        navigator.clipboard.writeText(text);
-        setCopied(true);
+    const handleCopy = async () => {
+        if (copying) {
+            return;
+        }
 
-        setTimeout(() => {
-            setCopied(false);
-        }, 2000);
+        setCopying(true);
+        try {
+            const value = getCopyText ? await getCopyText() : text;
+            await navigator.clipboard.writeText(value);
+            setCopied(true);
+            setTimeout(() => {
+                setCopied(false);
+            }, 2000);
+        } catch {
+            // Fetch errors are toasted by the caller; clipboard failures stay silent.
+        } finally {
+            setCopying(false);
+        }
     };
 
     const displayValue = displayText ?? text;
@@ -38,11 +52,14 @@ const CopyToClipboard = ({
                 type="button"
                 className="h-4 w-4 p-0 flex items-center justify-center cursor-pointer flex-shrink-0"
                 onClick={handleCopy}
+                disabled={copying}
             >
-                {!copied ? (
-                    
-                ) : (
+                {copying ? (
+                    
+                ) : copied ? (
                     
+                ) : (
+                    
                 )}
                 {t("copyText")}
             
diff --git a/src/components/UserVirtualApiKeys.tsx b/src/components/UserVirtualApiKeys.tsx
index 578f007b7..1ab2ae92d 100644
--- a/src/components/UserVirtualApiKeys.tsx
+++ b/src/components/UserVirtualApiKeys.tsx
@@ -1,8 +1,6 @@
 "use client";
 
-import { useState } from "react";
 import { useTranslations } from "next-intl";
-import { AxiosResponse } from "axios";
 import moment from "moment";
 import { Button } from "@app/components/ui/button";
 import CopyTextBox from "@app/components/CopyTextBox";
@@ -17,73 +15,18 @@ import {
     SettingsSectionHeader,
     SettingsSectionTitle as SectionTitle
 } from "@app/components/Settings";
-import { createApiClient, formatAxiosError } from "@app/lib/api";
-import { useEnvContext } from "@app/hooks/useEnvContext";
-import { toast } from "@app/hooks/useToast";
+import { useMyVirtualApiKeySecret } from "@app/hooks/useMyVirtualApiKeySecret";
 import type {
-    GetMyVirtualApiKeyResponse,
     ListMyVirtualApiKeysResponse,
     VirtualApiKeyWithResources
 } from "@server/routers/virtualApiKey/types";
-import {
-    formatVirtualApiKeyCredential,
-    formatVirtualApiKeyPreview
-} from "@app/lib/virtualApiKeyFormat";
+import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat";
 
 type UserVirtualApiKeysProps = {
     orgId: string;
     initialData: ListMyVirtualApiKeysResponse;
 };
 
-function useRevealSecret(orgId: string, virtualApiKeyId: string) {
-    const t = useTranslations();
-    const api = createApiClient(useEnvContext());
-    const [credential, setCredential] = useState(null);
-    const [loading, setLoading] = useState(false);
-
-    const revealSecret = () => {
-        if (credential || loading) {
-            return;
-        }
-
-        setLoading(true);
-        api.get>(
-            `/org/${orgId}/my-virtual-api-keys/${virtualApiKeyId}`
-        )
-            .then((res) => {
-                const secret = res.data.data.virtualApiKey.secret;
-                if (secret) {
-                    setCredential(
-                        formatVirtualApiKeyCredential(virtualApiKeyId, secret)
-                    );
-                } else {
-                    toast({
-                        variant: "destructive",
-                        title: t("virtualApiKeysErrorFetchSecret"),
-                        description: t(
-                            "virtualApiKeysErrorFetchSecretDescription"
-                        )
-                    });
-                }
-            })
-            .catch((e) => {
-                toast({
-                    variant: "destructive",
-                    title: t("virtualApiKeysErrorFetchSecret"),
-                    description: formatAxiosError(
-                        e,
-                        t("virtualApiKeysErrorFetchSecretDescription")
-                    )
-                });
-            })
-            .finally(() => {
-                setLoading(false);
-            });
-    };
-
-    return { credential, loading, revealSecret };
-}
-
 function OwnedKeySecret({
     orgId,
     virtualApiKeyId,
@@ -95,11 +38,9 @@ function OwnedKeySecret({
 }) {
     const t = useTranslations();
     const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
-    const { credential, loading, revealSecret } = useRevealSecret(
-        orgId,
-        virtualApiKeyId
-    );
-    const displayValue = credential ?? preview;
+    const { credential, revealed, loading, getCopyText, revealSecret } =
+        useMyVirtualApiKeySecret(orgId, virtualApiKeyId);
+    const displayValue = revealed && credential ? credential : preview;
 
     return (
         
@@ -107,9 +48,10 @@ function OwnedKeySecret({
- {!credential ? ( + {!revealed ? (