Files
pieced-portal/src/app/[locale]/settings/billing/page.tsx
admin ad4f614130
All checks were successful
Build and Push / build (push) Successful in 1m45s
Phase8: Auto bill credit card
2026-05-27 20:45:25 +02:00

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>
);
}