71 lines
2.7 KiB
TypeScript
71 lines
2.7 KiB
TypeScript
import { redirect, notFound } from "next/navigation";
|
|
import { getTranslations } from "next-intl/server";
|
|
import { getSessionUser } from "@/lib/session";
|
|
import { getOrgBilling, getOrgBillingConfig } from "@/lib/db";
|
|
import { BillingSettingsForm } from "@/components/settings/billing-form";
|
|
import { SavedCardSection } from "@/components/settings/saved-card-section";
|
|
|
|
/**
|
|
* /settings/billing — customer-side billing details management.
|
|
*
|
|
* Owner-only by visibility: non-owner members get a 404 (same
|
|
* response as if the page didn't exist). The link to this page
|
|
* is also hidden from non-owners on /billing and elsewhere, but
|
|
* the page itself enforces too — a non-owner who learns the URL
|
|
* still gets 404, not 403, so the page's existence doesn't leak.
|
|
*
|
|
* First-time visitors see an empty form. Subsequent visits see
|
|
* the current values, editable. Save creates or updates via the
|
|
* shared upsert path; the row's existence drives whether the
|
|
* monthly issuance cron will pick this org up.
|
|
*
|
|
* Phase 9: also renders the saved-card section (Set up auto-pay /
|
|
* Visa dot-dot-dot 4242, expires MM/YY / Update card / Disable
|
|
* auto-pay / Remove card) when billing info is on file, plus a
|
|
* footer note explaining that bank transfer is available on request.
|
|
*/
|
|
export default async function BillingSettingsPage() {
|
|
const user = await getSessionUser();
|
|
if (!user) redirect("/login");
|
|
// Non-owners get a 404 — see comment above.
|
|
if (!user.roles.includes("owner")) notFound();
|
|
|
|
const t = await getTranslations("settingsBilling");
|
|
const [existing, config] = await Promise.all([
|
|
getOrgBilling(user.orgId),
|
|
getOrgBillingConfig(user.orgId),
|
|
]);
|
|
|
|
return (
|
|
<main className="max-w-3xl mx-auto px-6 py-8">
|
|
<div className="mb-8 animate-in">
|
|
<h1 className="font-display text-2xl font-semibold accent-rule">
|
|
{t("title")}
|
|
</h1>
|
|
<p className="text-sm text-text-secondary mt-3">
|
|
{user.isPersonal ? t("subtitlePersonal") : t("subtitle")}
|
|
</p>
|
|
</div>
|
|
<div className="animate-in animate-in-delay-1">
|
|
<BillingSettingsForm
|
|
initial={existing}
|
|
isPersonal={user.isPersonal}
|
|
/>
|
|
</div>
|
|
{/* Phase 9: saved-card section. Only shown once billing info
|
|
exists — without an address Stripe can't create the
|
|
customer object, so the "Set up auto-pay" button would
|
|
fail anyway. We give a clear hint up there if the form
|
|
is empty (no need to surface the card UI). */}
|
|
{existing && (
|
|
<div className="animate-in animate-in-delay-2 mt-8">
|
|
<SavedCardSection
|
|
config={config}
|
|
isPayByInvoice={!!config?.payByInvoice}
|
|
/>
|
|
</div>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|