65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useTranslations } from "next-intl";
|
|
|
|
interface Props {
|
|
invoiceNumber: string;
|
|
}
|
|
|
|
/**
|
|
* Pay-with-card button. Posts to /api/billing/invoices/[n]/pay,
|
|
* which returns a Stripe Checkout Session URL; we redirect the
|
|
* browser there.
|
|
*
|
|
* The button is rendered only by the parent for status='open' or
|
|
* 'overdue' invoices — the API enforces this too, but pre-filtering
|
|
* UI-side keeps the dead state out of the customer's face.
|
|
*/
|
|
export function PayInvoiceButton({ invoiceNumber }: Props) {
|
|
const t = useTranslations("customerBilling");
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const onClick = async () => {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(
|
|
`/api/billing/invoices/${encodeURIComponent(invoiceNumber)}/pay`,
|
|
{ method: "POST" }
|
|
);
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
}
|
|
if (!data.url) {
|
|
throw new Error("Payment session URL missing from response.");
|
|
}
|
|
// Hard navigation, not Next.js router — Stripe Checkout is a
|
|
// separate origin and the browser needs to fully leave our app.
|
|
window.location.href = data.url;
|
|
} catch (e: any) {
|
|
setError(e?.message ?? String(e));
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col items-end gap-1">
|
|
<button
|
|
onClick={onClick}
|
|
disabled={busy}
|
|
className="px-4 py-2 rounded-md bg-accent text-surface-0 text-sm font-medium hover:bg-accent-dim transition-colors disabled:opacity-50 cursor-pointer"
|
|
>
|
|
{busy ? t("redirectingToStripe") : t("payWithCard")}
|
|
</button>
|
|
{error && (
|
|
<span className="text-xs text-error max-w-[260px] text-right">
|
|
{error}
|
|
</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|