diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 49987e2b..1ae3fd6f 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -94,7 +94,9 @@ "checkoutPreview": "Checkout preview", "payNow": "Pay now", "generating": "Generating...", - "generate": "Generate Payment Link" + "generate": "Generate Payment Link", + "rateLimitError": "You're creating links too quickly. Try again in {seconds} seconds.", + "retryWait": "Wait {seconds}s…" }, "paymentMetrics": { "downloadImage": "Download Image", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index ff30f540..02505c56 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -94,7 +94,9 @@ "checkoutPreview": "Vista previa del checkout", "payNow": "Pagar ahora", "generating": "Generando...", - "generate": "Generar enlace de pago" + "generate": "Generar enlace de pago", + "rateLimitError": "Estás creando enlaces demasiado rápido. Inténtalo de nuevo en {seconds} segundos.", + "retryWait": "Espera {seconds}s…" }, "paymentMetrics": { "downloadImage": "Descargar imagen", diff --git a/frontend/messages/pt.json b/frontend/messages/pt.json index e0bb6350..d6980794 100644 --- a/frontend/messages/pt.json +++ b/frontend/messages/pt.json @@ -94,7 +94,9 @@ "checkoutPreview": "Previa do checkout", "payNow": "Pagar agora", "generating": "Gerando...", - "generate": "Gerar link de pagamento" + "generate": "Gerar link de pagamento", + "rateLimitError": "Você está criando links rápido demais. Tente novamente em {seconds} segundos.", + "retryWait": "Aguarde {seconds}s…" }, "paymentMetrics": { "downloadImage": "Baixar imagem", diff --git a/frontend/src/components/CreatePaymentForm.tsx b/frontend/src/components/CreatePaymentForm.tsx index 690324d2..a95e080d 100644 --- a/frontend/src/components/CreatePaymentForm.tsx +++ b/frontend/src/components/CreatePaymentForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, type FormEvent } from "react"; +import { useState, useEffect, useRef, type FormEvent } from "react"; import { useTranslations } from "next-intl"; import CopyButton from "./CopyButton"; import toast from "react-hot-toast"; @@ -52,9 +52,40 @@ export default function CreatePaymentForm() { const apiKey = useMerchantApiKey(); const hydrated = useMerchantHydrated(); const trustedAddresses = useMerchantTrustedAddresses(); + const [useSessionBranding, setUseSessionBranding] = useLocalStorage("payment_use_branding", false); + const [branding, setBranding] = useLocalStorage("payment_branding", DEFAULT_BRANDING); + const [selectedTrustedAddress, setSelectedTrustedAddress] = useLocalStorage("payment_trusted_address", ""); useHydrateMerchantStore(); + // ── Rate-limit countdown ────────────────────────────────── + const [retryAfter, setRetryAfter] = useState(0); + const retryTimerRef = useRef | null>(null); + + useEffect(() => { + if (retryAfter <= 0) { + if (retryTimerRef.current) clearInterval(retryTimerRef.current); + return; + } + + retryTimerRef.current = setInterval(() => { + setRetryAfter((prev) => { + if (prev <= 1) { + clearInterval(retryTimerRef.current!); + retryTimerRef.current = null; + setError(null); + return 0; + } + return prev - 1; + }); + }, 1000); + + return () => { + if (retryTimerRef.current) clearInterval(retryTimerRef.current); + }; + }, [retryAfter]); + // ────────────────────────────────────────────────────────── + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); setError(null); @@ -100,6 +131,19 @@ export default function CreatePaymentForm() { }); const data = await res.json(); + + // ── 429 Rate-limit handling ───────────────────────────── + if (res.status === 429) { + const retryHeader = res.headers.get("Retry-After"); + const seconds = retryHeader ? Math.max(1, Math.ceil(Number(retryHeader))) : 60; + setRetryAfter(seconds); + const msg = t("rateLimitError", { seconds: String(seconds) }); + setError(msg); + toast.error(msg); + return; + } + // ──────────────────────────────────────────────────────── + if (!res.ok) throw new Error(data.error ?? t("failedCreate")); @@ -136,6 +180,7 @@ export default function CreatePaymentForm() { localStorage.removeItem("payment_trusted_address"); setError(null); + setRetryAfter(0); }; const handleTrustedAddressSelect = (addressId: string) => { @@ -243,7 +288,27 @@ export default function CreatePaymentForm() { return (
- {error && ( + {error && retryAfter > 0 && ( +
+ + + +
+ {t("rateLimitError", { seconds: String(retryAfter) })} +
+
+
+
+
+ )} + {error && retryAfter <= 0 && (
0} className="group relative flex h-12 items-center justify-center rounded-xl bg-mint px-6 font-bold text-black transition-all hover:bg-glow disabled:cursor-not-allowed disabled:opacity-50" > {loading ? ( @@ -463,6 +528,8 @@ export default function CreatePaymentForm() { {t("generating")} + ) : retryAfter > 0 ? ( + t("retryWait", { seconds: String(retryAfter) }) ) : ( t("generate") )}