From 4d7a6ada490e0f460ce64587071482c5b7a26191 Mon Sep 17 00:00:00 2001 From: Kilwizzy Date: Sun, 29 Mar 2026 08:00:14 +0100 Subject: [PATCH 1/2] ui: add clipboard copy for payment intent IDs in dashboard table --- frontend/src/components/RecentPayments.tsx | 38 +++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/RecentPayments.tsx b/frontend/src/components/RecentPayments.tsx index 6c9b812..43b4a34 100644 --- a/frontend/src/components/RecentPayments.tsx +++ b/frontend/src/components/RecentPayments.tsx @@ -5,6 +5,7 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useLocale, useTranslations } from "next-intl"; import Skeleton from "react-loading-skeleton"; import "react-loading-skeleton/dist/skeleton.css"; +import toast from "react-hot-toast"; import PaymentDetailModal from "@/components/PaymentDetailModal"; import ExportCsvButton from "@/components/ExportCsvButton"; import { localeToLanguageTag } from "@/i18n/config"; @@ -383,6 +384,21 @@ export default function RecentPayments({ setSelectedPayment(null); }; + const handleCopyId = async (id: string, event: React.MouseEvent) => { + event.stopPropagation(); + try { + await navigator.clipboard.writeText(id); + } catch { + const el = document.createElement("textarea"); + el.value = id; + document.body.appendChild(el); + el.select(); + document.execCommand("copy"); + document.body.removeChild(el); + } + toast.success("Intent ID copied to clipboard"); + }; + if (showSkeleton || loading) { return (
@@ -1003,7 +1019,27 @@ export default function RecentPayments({ - {payment.amount} {payment.asset} + + {payment.amount} {payment.asset} + + From c4178a7ad5d8448fcf550f150d9ccf6862eb7e1f Mon Sep 17 00:00:00 2001 From: Kilwizzy Date: Sun, 29 Mar 2026 09:14:23 +0100 Subject: [PATCH 2/2] feat: add dynamic inline code snippets for API integration --- frontend/messages/en.json | 4 +- frontend/messages/es.json | 4 +- frontend/messages/pt.json | 4 +- frontend/package.json | 3 + frontend/src/components/CreatePaymentForm.tsx | 64 +++++- .../components/IntegrationCodeSnippets.tsx | 182 ++++++++++++++++++ 6 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/IntegrationCodeSnippets.tsx diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 5f5f122..acb86ab 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -110,7 +110,9 @@ "generating": "Generating...", "generate": "Generate Payment Link", "rateLimitError": "You're creating links too quickly. Try again in {seconds} seconds.", - "retryWait": "Wait {seconds}s…" + "retryWait": "Wait {seconds}s…", + "integrationCode": "Integration Code", + "snippetsHelper": "Code snippets update automatically with your form values." }, "paymentMetrics": { "downloadImage": "Download Image", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 3766c58..ca76aff 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -110,7 +110,9 @@ "generating": "Generando...", "generate": "Generar enlace de pago", "rateLimitError": "Estás creando enlaces demasiado rápido. Inténtalo de nuevo en {seconds} segundos.", - "retryWait": "Espera {seconds}s…" + "retryWait": "Espera {seconds}s…", + "integrationCode": "Código de Integración", + "snippetsHelper": "Los fragmentos de código se actualizan automáticamente con los valores del formulario." }, "paymentMetrics": { "downloadImage": "Descargar imagen", diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index 5fd9f2f..c92a753 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -110,7 +110,9 @@ "generating": "Gerando...", "generate": "Gerar link de pagamento", "rateLimitError": "Você está criando links rápido demais. Tente novamente em {seconds} segundos.", - "retryWait": "Aguarde {seconds}s…" + "retryWait": "Aguarde {seconds}s…", + "integrationCode": "Código de Integração", + "snippetsHelper": "Os trechos de código são atualizados automaticamente com os valores do formulário." }, "paymentMetrics": { "downloadImage": "Baixar imagem", diff --git a/frontend/package.json b/frontend/package.json index bc33047..1c2178f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.16", "@sentry/nextjs": "^10.46.0", "@stellar/freighter-api": "^1.7.1", + "@types/prismjs": "^1.26.6", "@walletconnect/sign-client": "^2.23.9", "@walletconnect/types": "^2.23.8", "boring-avatars": "^2.0.4", @@ -30,6 +31,7 @@ "next-intl": "^4.8.3", "next-mdx-remote": "^5.0.0", "next-themes": "^0.4.6", + "prismjs": "^1.30.0", "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-confetti": "^6.4.0", @@ -49,6 +51,7 @@ "@types/canvas-confetti": "^1.9.0", "@types/event-source-polyfill": "^1.0.5", "@types/node": "25.5.0", + "@types/prismjs": "^1.26.5", "@types/react": "18.3.28", "@types/react-dom": "^18.3.7", "autoprefixer": "^10.4.19", diff --git a/frontend/src/components/CreatePaymentForm.tsx b/frontend/src/components/CreatePaymentForm.tsx index 8764e42..d40d29d 100644 --- a/frontend/src/components/CreatePaymentForm.tsx +++ b/frontend/src/components/CreatePaymentForm.tsx @@ -5,6 +5,7 @@ import { useTranslations } from "next-intl"; import { motion, AnimatePresence, type Variants } from "framer-motion"; import confetti from "canvas-confetti"; import CopyButton from "./CopyButton"; +import IntegrationCodeSnippets from "./IntegrationCodeSnippets"; import toast from "react-hot-toast"; import Link from "next/link"; import { @@ -338,6 +339,7 @@ function SuccessCard({ created, onReset, t }: SuccessCardProps) { export default function CreatePaymentForm() { const t = useTranslations("createPaymentForm"); + const [view, setView] = useState<"form" | "code">("form"); const [amount, setAmount] = useLocalStorage("payment_amount", ""); const [asset, setAsset] = useLocalStorage<"XLM" | "USDC">( "payment_asset", @@ -545,15 +547,67 @@ export default function CreatePaymentForm() { t={t} /> ) : ( - + {/* Tab bar */} +
+ + +
+ + {view === "code" ? ( + + ) : ( + {error && ( - +
+ )} + )} ); diff --git a/frontend/src/components/IntegrationCodeSnippets.tsx b/frontend/src/components/IntegrationCodeSnippets.tsx new file mode 100644 index 0000000..3bb1426 --- /dev/null +++ b/frontend/src/components/IntegrationCodeSnippets.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useState, useEffect, useRef, useMemo } from "react"; +import { useTranslations } from "next-intl"; +import { motion, AnimatePresence } from "framer-motion"; +import CopyButton from "./CopyButton"; +import Prism from "prismjs"; +import "prismjs/components/prism-bash"; +import "prismjs/components/prism-javascript"; +import "prismjs/components/prism-python"; + +const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000"; + +interface IntegrationCodeSnippetsProps { + apiKey: string; + amount: string; + asset: "XLM" | "USDC"; + recipient: string; + description: string; + usdcIssuer: string; +} + +type Language = "curl" | "node" | "python"; + +const LANGUAGES: { id: Language; label: string; grammar: string }[] = [ + { id: "curl", label: "cURL", grammar: "bash" }, + { id: "node", label: "Node.js", grammar: "javascript" }, + { id: "python", label: "Python", grammar: "python" }, +]; + +function generateSnippet( + lang: Language, + apiKey: string, + amount: string, + asset: "XLM" | "USDC", + recipient: string, + description: string, + usdcIssuer: string, +): string { + const numAmount = parseFloat(amount) || 0; + const safeRecipient = + recipient || "GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; + const safeDescription = description || "Payment for services"; + + const body: Record = { + amount: numAmount, + asset, + recipient: safeRecipient, + }; + if (asset === "USDC") body.asset_issuer = usdcIssuer; + if (description) body.description = safeDescription; + + const jsonBody = JSON.stringify(body, null, 2); + + switch (lang) { + case "curl": + return `curl -X POST "${API_URL}/api/create-payment" \\ + -H "Content-Type: application/json" \\ + -H "x-api-key: ${apiKey}" \\ + -d '${jsonBody}'`; + + case "node": + return `const response = await fetch("${API_URL}/api/create-payment", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-api-key": "${apiKey}", + }, + body: JSON.stringify(${jsonBody}), +}); + +const data = await response.json(); +console.log(data.payment_link);`; + + case "python": + return `import requests + +response = requests.post( + "${API_URL}/api/create-payment", + headers={ + "Content-Type": "application/json", + "x-api-key": "${apiKey}", + }, + json=${jsonBody}, +) + +data = response.json() +print(data["payment_link"])`; + } +} + +export default function IntegrationCodeSnippets({ + apiKey, + amount, + asset, + recipient, + description, + usdcIssuer, +}: IntegrationCodeSnippetsProps) { + const t = useTranslations("createPaymentForm"); + const [activeTab, setActiveTab] = useState("curl"); + const codeRef = useRef(null); + + const snippet = useMemo( + () => generateSnippet(activeTab, apiKey, amount, asset, recipient, description, usdcIssuer), + [activeTab, apiKey, amount, asset, recipient, description, usdcIssuer], + ); + + useEffect(() => { + if (codeRef.current) { + Prism.highlightElement(codeRef.current); + } + }, [snippet, activeTab]); + + const grammarMap: Record = { + curl: "bash", + node: "javascript", + python: "python", + }; + + return ( +
+ {/* Tab bar */} +
+ {LANGUAGES.map((lang) => ( + + ))} +
+ + {/* Code block */} +
+ {/* Copy button */} +
+ +
+ + + +
+              
+                {snippet}
+              
+            
+
+
+
+ + {/* Helper text */} +

+ {t("snippetsHelper")} +

+
+ ); +}