Move payment settings to Brand tab in Settings

- Integrated PaymentSettingsForm into SettingsClient Brand tab
- Added paymentSettings prop to SettingsClient and Settings page
- Redirected /admin/settings/payments to /admin/settings#brand
- Updated PaymentSettingsForm to use admin CSS variables for styling
- Fixed colors to match admin design system (no dark backgrounds)
This commit is contained in:
2026-06-02 04:14:27 +00:00
parent d44506dd01
commit 7783604141
11 changed files with 535 additions and 264 deletions
@@ -66,7 +66,7 @@ export default function BillingClientPage({
return (
<div className="space-y-4">
{/* ── 1. Header summary bar ───────────────────────────────────────────── */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 px-6 py-5">
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] px-6 py-5">
<div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-4">
<div>
@@ -76,26 +76,26 @@ export default function BillingClientPage({
</span>
{subscriptionStatus && (
<span className={`rounded-full px-2.5 py-0.5 text-xs font-bold uppercase ${
subscriptionStatus === "active" ? "bg-green-900/40 text-green-400" :
subscriptionStatus === "active" ? "bg-[var(--admin-success-light)] text-[var(--admin-success-accent)]" :
subscriptionStatus === "past_due" ? "bg-amber-100 text-amber-700" :
subscriptionStatus === "canceled" ? "bg-red-900/40 text-red-400" :
subscriptionStatus === "trialing" ? "bg-blue-900/40 text-blue-700" :
"bg-zinc-950 text-zinc-400"
subscriptionStatus === "canceled" ? "bg-red-100 text-red-600" :
subscriptionStatus === "trialing" ? "bg-blue-100 text-blue-600" :
"bg-[var(--admin-bg)] text-[var(--admin-text-muted)]"
}`}>
{subscriptionStatus.replace("_", " ")}
</span>
)}
<span className="text-sm text-zinc-500">
<span className="text-sm text-[var(--admin-text-muted)]">
{currentPeriodEnd ? (
<>Next billing: <span className="font-medium text-zinc-300">{new Date(currentPeriodEnd).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</span></>
<>Next billing: <span className="font-medium text-[var(--admin-text-primary)]">{new Date(currentPeriodEnd).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}</span></>
) : (
"No active subscription"
)}
</span>
</div>
<p className="text-2xl font-bold text-zinc-100">
${totalMonthly}<span className="text-sm font-normal text-slate-400">/mo</span>
<span className="ml-2 text-sm font-medium text-green-600">
<p className="text-2xl font-bold text-[var(--admin-text-primary)]">
${totalMonthly}<span className="text-sm font-normal text-[var(--admin-text-muted)]">/mo</span>
<span className="ml-2 text-sm font-medium text-[var(--admin-success-accent)]">
{billingCycle === "annual" ? "Annual (saves 25%)" : ""}
</span>
</p>
@@ -108,35 +108,35 @@ export default function BillingClientPage({
{/* ── 2. Current plan + Add-ons (two-column) ───────────────────────────── */}
<div className="grid gap-4 lg:grid-cols-5">
{/* Current plan card */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5 lg:col-span-2">
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5 lg:col-span-2">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-zinc-100">Your Plan</h3>
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Your Plan</h3>
<span className={`rounded-full px-2.5 py-0.5 text-xs font-bold uppercase ${currentPlan.color}`}>
{currentPlan.label}
</span>
</div>
<p className="text-lg font-bold text-zinc-100 mb-1">
<p className="text-lg font-bold text-[var(--admin-text-primary)] mb-1">
{billingCycle === "annual" ? `$${currentPlan.annualPrice}/yr` : `$${currentPlan.monthlyPrice}/mo`}
</p>
<p className="text-xs text-zinc-500 mb-4">
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
{billingCycle === "annual" && currentPlan.annualPrice
? `$${Math.round(currentPlan.annualPrice / 12)}/mo equivalent`
: "billed monthly"}
</p>
<ul className="space-y-1 mb-4">
{(currentPlan.features as readonly string[]).slice(0, 5).map((f, i) => (
<li key={i} className="flex items-center gap-2 text-xs text-zinc-400">
<span className="text-green-500 shrink-0"></span> {f}
<li key={i} className="flex items-center gap-2 text-xs text-[var(--admin-text-secondary)]">
<span className="text-[var(--admin-success)] shrink-0"></span> {f}
</li>
))}
{(currentPlan.features as readonly string[]).length > 5 && (
<li className="text-xs text-slate-400 pl-5">+ {(currentPlan.features as readonly string[]).length - 5} more</li>
<li className="text-xs text-[var(--admin-text-muted)] pl-5">+ {(currentPlan.features as readonly string[]).length - 5} more</li>
)}
</ul>
{isPlatformAdmin && (
<button
onClick={() => setCompareOpen(!compareOpen)}
className="text-xs text-violet-600 hover:underline font-medium"
className="text-xs text-[var(--admin-accent)] hover:underline font-medium"
>
{compareOpen ? "Hide plan comparison" : "Compare plans →"}
</button>
@@ -144,10 +144,10 @@ export default function BillingClientPage({
</div>
{/* Add-ons */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5 lg:col-span-3">
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5 lg:col-span-3">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-zinc-100">Add-ons</h3>
<span className="text-xs text-slate-400">+{addonsMonthlyTotal}/mo</span>
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Add-ons</h3>
<span className="text-xs text-[var(--admin-text-muted)]">+{addonsMonthlyTotal}/mo</span>
</div>
<div className="space-y-2.5">
{allAddonKeys.map((key) => {
@@ -157,19 +157,19 @@ export default function BillingClientPage({
? Math.round(addon.annualPrice / 12)
: addon.monthlyPrice;
return (
<div key={key} className="flex items-center justify-between rounded-lg border bg-slate-50 px-3 py-2.5">
<div key={key} className="flex items-center justify-between rounded-lg border border-[var(--admin-border)] bg-[var(--admin-bg)] px-3 py-2.5">
<div className="flex items-center gap-2.5">
<span className="text-base leading-none">{addon.icon}</span>
<div>
<p className="text-sm font-medium text-slate-800">{addon.label}</p>
<p className="text-xs text-slate-400">{addon.description}</p>
<p className="text-sm font-medium text-[var(--admin-text-primary)]">{addon.label}</p>
<p className="text-xs text-[var(--admin-text-muted)]">{addon.description}</p>
</div>
</div>
<div className="flex items-center gap-3 shrink-0">
<span className="text-sm font-semibold text-zinc-300">+${displayPrice}/mo</span>
<span className="text-sm font-semibold text-[var(--admin-text-secondary)]">+${displayPrice}/mo</span>
{isEnabled ? (
<div className="flex items-center gap-1.5">
<span className="rounded-full bg-green-900/40 text-green-400 text-xs px-2 py-0.5 font-medium">Active</span>
<span className="rounded-full bg-[var(--admin-success-light)] text-[var(--admin-success-accent)] text-xs px-2 py-0.5 font-medium">Active</span>
{isPlatformAdmin && (
<RemoveAddonButton brandId={brandId} addonKey={key} onRemoved={() => window.location.reload()} />
)}
@@ -187,18 +187,18 @@ export default function BillingClientPage({
{/* ── 3. Compare plans (collapsible) ───────────────────────────────────── */}
{compareOpen && (
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 overflow-hidden">
<div className="border-b border-slate-100 bg-slate-50 px-5 py-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-zinc-100">Plan Comparison</h3>
<button onClick={() => setCompareOpen(false)} className="text-xs text-slate-400 hover:text-zinc-400">
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] overflow-hidden">
<div className="border-b border-[var(--admin-border)] bg-[var(--admin-bg)] px-5 py-3 flex items-center justify-between">
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)]">Plan Comparison</h3>
<button onClick={() => setCompareOpen(false)} className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)]">
Close
</button>
</div>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-100">
<th className="pb-3 pr-4 text-left font-semibold text-zinc-500 w-2/5" />
<tr className="border-b border-[var(--admin-border)]">
<th className="pb-3 pr-4 text-left font-semibold text-[var(--admin-text-muted)] w-2/5" />
{(["starter", "farm", "enterprise"] as const).map((tier) => {
const plan = PLAN_TIERS[tier];
const price = billingCycle === "annual" ? plan.annualPrice : plan.monthlyPrice;
@@ -208,13 +208,13 @@ export default function BillingClientPage({
{plan.label}
</span>
<div className="mt-1">
<span className="text-lg font-bold text-zinc-100">
<span className="text-lg font-bold text-[var(--admin-text-primary)]">
{price !== null ? `$${price}` : "$399"}
</span>
<span className="text-slate-400 text-xs">/{billingCycle === "annual" ? "yr" : "mo"}</span>
<span className="text-[var(--admin-text-muted)] text-xs">/{billingCycle === "annual" ? "yr" : "mo"}</span>
</div>
{tier === "enterprise" && billingCycle === "annual" && (
<p className="text-xs text-slate-400 mt-0.5">or $399/mo</p>
<p className="text-xs text-[var(--admin-text-muted)] mt-0.5">or $399/mo</p>
)}
</th>
);
@@ -241,28 +241,28 @@ export default function BillingClientPage({
].map(([feature, tiers]) => {
const t = tiers as Record<string, boolean>;
return (
<tr key={feature as string} className="border-t border-slate-100">
<td className="py-2 pr-4 text-zinc-400">{feature as string}</td>
<tr key={feature as string} className="border-t border-[var(--admin-border)]">
<td className="py-2 pr-4 text-[var(--admin-text-secondary)]">{feature as string}</td>
{(["starter", "farm", "enterprise"] as const).map((tier) => (
<td key={tier} className="py-2 px-3 text-center">
{t[tier]
? <span className="text-green-500"></span>
: <span className="text-slate-300"></span>}
? <span className="text-[var(--admin-success)]"></span>
: <span className="text-[var(--admin-text-muted)]"></span>}
</td>
))}
</tr>
);
})}
<tr className="border-t border-zinc-800">
<tr className="border-t border-[var(--admin-border)]">
<td className="pt-3 pr-4" />
{(["starter", "farm", "enterprise"] as const).map((tier) => {
const isCurrent = tier === planTier;
return (
<td key={tier} className="pt-3 px-3 text-center">
{isCurrent ? (
<span className="inline-block rounded-lg bg-green-900/40 px-2 py-1 text-xs font-medium text-green-400">Current</span>
<span className="inline-block rounded-lg bg-[var(--admin-success-light)] px-2 py-1 text-xs font-medium text-[var(--admin-success-accent)]">Current</span>
) : tier === "enterprise" ? (
<a href="mailto:team@cielohermosa.com?subject=Enterprise+Plan" className="inline-block rounded-lg border border-violet-200 bg-violet-50 px-2 py-1 text-xs font-medium text-violet-700 hover:bg-violet-100">
<a href="mailto:team@cielohermosa.com?subject=Enterprise+Plan" className="inline-block rounded-lg border border-[var(--admin-accent)] bg-[var(--admin-accent-light)] px-2 py-1 text-xs font-medium text-[var(--admin-accent-text)] hover:opacity-80">
Contact
</a>
) : (
@@ -281,38 +281,38 @@ export default function BillingClientPage({
{/* ── 4. Payment + Invoices (two-column) ──────────────────────────────── */}
<div className="grid gap-4 lg:grid-cols-2">
{/* Payment method */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5">
<h3 className="text-sm font-semibold text-zinc-100 mb-4">Payment Method</h3>
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5">
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)] mb-4">Payment Method</h3>
{hasStripeCustomer ? (
<div className="space-y-3">
<div className="flex items-center gap-3 rounded-xl border border-zinc-800 p-3.5">
<div className="flex items-center gap-3 rounded-xl border border-[var(--admin-border)] p-3.5">
<div className="flex items-center justify-center h-9 w-12 rounded-lg bg-gradient-to-br from635bff to-purple-600 shrink-0">
<svg className="h-4 w-7 text-white" viewBox="0 0 32 20" fill="currentColor">
<path d="M13.193 7.448c-.114-.272-.379-.358-.65-.358-.27 0-.535.09-.65.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .534-.09.648-.357.13-.28.115-.561-.115-.837-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.27 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.536-.357.806-.357.271 0 .536.09.65.357l.001-.001c.115-.272.38-.358.65-.358.271 0 .536.09.65.357l.001.001c.122.28.122.56-.008.838zm8.697-.001c-.122-.276-.379-.357-.65-.357-.27 0-.535.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.65-.357.13-.28.114-.561-.115-.837-.238-.286-.536-.357-.807-.357-.27 0-.535.09-.65.357l-.001-.001-.001.001c-.114-.267-.379-.357-.65-.357-.27 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.649-.357.13-.277.114-.558-.115-.838-.236-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.114-.276-.379-.357-.65-.357-.271 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.536.357.806.357.271 0 .536-.09.65-.357.13-.277.114-.558-.115-.838-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.535-.357.806-.357.27 0 .535.09.65.357l.001.001c.122.28.122.56-.008.838z"/>
<path d="M13.193 7.448c-.114-.272-.379-.358-.65-.358-.27 0-.535.09-.65.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .534-.09.648-.357.13-.28.115-.561-.115-.837-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.27 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.536-.357.806-.357.271 0 .536.09.65.357l.001-.001c.115-.272.38-.358.65-.358.271 0 .536.09.65.357l.001.001c.122.28.122.56-.008.838zm8.697-.001c-.122-.276-.379-.357-.65-.357-.27 0-.535.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.65-.357.13-.28.114-.561-.115-.837-.238-.286-.536-.357-.807-.357-.27 0-.535.09-.65.357l-.001-.001-.001.001c-.114-.267-.379-.357-.65-.357-.27 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.535.357.806.357.27 0 .535-.09.649-.357.13-.277.114-.558-.115-.838-.236-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001.001-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.114-.276-.379-.357-.65-.357-.271 0-.536.09-.649.357-.13.277-.114.558.115.838.236.286.536.357.806.357.271 0 .536-.09.65-.357.13-.277.114-.558-.115-.838-.237-.286-.536-.357-.806-.357-.271 0-.536.09-.65.357l-.001-.001c-.114.267-.379.357-.65.357-.271 0-.536-.09-.65-.357-.13-.277-.114-.558.115-.838.236-.286.535.357.806-.357.27 0 .535.09.65.357l.001.001c.122.28.122.56-.008.838z"/>
</svg>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-slate-800">Visa ending in 4242</p>
<p className="text-xs text-slate-400">Expires 12/26</p>
<p className="text-sm font-medium text-[var(--admin-text-primary)]">Visa ending in 4242</p>
<p className="text-xs text-[var(--admin-text-muted)]">Expires 12/26</p>
</div>
<span className="rounded-full bg-green-900/40 text-green-400 text-xs px-2 py-0.5 font-medium shrink-0">Active</span>
<span className="rounded-full bg-[var(--admin-success-light)] text-[var(--admin-success-accent)] text-xs px-2 py-0.5 font-medium shrink-0">Active</span>
</div>
<StripePortalButton brandId={brandId} variant="secondary" label="Manage in Stripe Portal →" />
<p className="text-xs text-slate-400 text-center">
<p className="text-xs text-[var(--admin-text-muted)] text-center">
Payment powered by Stripe · Invoiced by Cielo Hermosa, LLC
</p>
</div>
) : (
<div className="space-y-3">
<div className="rounded-xl border border-amber-200 bg-amber-900/30 p-3.5">
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3.5">
<p className="text-sm text-amber-700">
<strong>No payment method on file.</strong> Add a card to activate your subscription.
</p>
</div>
<AddPaymentMethodButton brandId={brandId} />
<p className="text-xs text-slate-400 text-center">
<p className="text-xs text-[var(--admin-text-muted)] text-center">
Set up in{" "}
<a href="/admin/settings/payments" className="underline hover:text-zinc-400">Payments settings</a>
<a href="/admin/settings/payments" className="underline hover:text-[var(--admin-text-primary)]">Payments settings</a>
{" "}to enable billing.
</p>
</div>
@@ -320,41 +320,41 @@ export default function BillingClientPage({
</div>
{/* Invoice history */}
<div className="rounded-2xl bg-zinc-900 shadow-black/20 ring-1 ring-zinc-700 p-5">
<h3 className="text-sm font-semibold text-zinc-100 mb-4">Invoice History</h3>
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-5">
<h3 className="text-sm font-semibold text-[var(--admin-text-primary)] mb-4">Invoice History</h3>
<div className="overflow-x-auto">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-slate-100">
<th className="pb-2 text-left font-semibold text-zinc-500">Invoice</th>
<th className="pb-2 text-left font-semibold text-zinc-500">Date</th>
<th className="pb-2 text-right font-semibold text-zinc-500">Amount</th>
<th className="pb-2 text-right font-semibold text-zinc-500">Status</th>
<tr className="border-b border-[var(--admin-border)]">
<th className="pb-2 text-left font-semibold text-[var(--admin-text-muted)]">Invoice</th>
<th className="pb-2 text-left font-semibold text-[var(--admin-text-muted)]">Date</th>
<th className="pb-2 text-right font-semibold text-[var(--admin-text-muted)]">Amount</th>
<th className="pb-2 text-right font-semibold text-[var(--admin-text-muted)]">Status</th>
<th className="pb-2"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-50">
<tbody className="divide-y divide-[var(--admin-border)]">
{[
{ id: "INV-2026-004", date: "May 1, 2026", amount: totalMonthly, status: "paid" },
{ id: "INV-2026-003", date: "Apr 1, 2026", amount: totalMonthly, status: "paid" },
{ id: "INV-2026-002", date: "Mar 1, 2026", amount: totalMonthly, status: "paid" },
].map((inv) => (
<tr key={inv.id}>
<td className="py-2 font-medium text-slate-800">{inv.id}</td>
<td className="py-2 text-zinc-500">{inv.date}</td>
<td className="py-2 text-right font-semibold text-slate-800">${inv.amount}</td>
<td className="py-2 font-medium text-[var(--admin-text-primary)]">{inv.id}</td>
<td className="py-2 text-[var(--admin-text-muted)]">{inv.date}</td>
<td className="py-2 text-right font-semibold text-[var(--admin-text-primary)]">${inv.amount}</td>
<td className="py-2 text-right">
<span className="rounded-full bg-green-900/40 text-green-400 px-1.5 py-0.5 text-xs font-medium capitalize">{inv.status}</span>
<span className="rounded-full bg-[var(--admin-success-light)] text-[var(--admin-success-accent)] px-1.5 py-0.5 text-xs font-medium capitalize">{inv.status}</span>
</td>
<td className="py-2 text-right">
<button className="text-xs text-slate-400 hover:text-zinc-400">PDF</button>
<button className="text-xs text-[var(--admin-text-muted)] hover:text-[var(--admin-accent)]">PDF</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-3 text-xs text-slate-400 text-center">
<p className="mt-3 text-xs text-[var(--admin-text-muted)] text-center">
Invoiced by Cielo Hermosa, LLC · <a href="mailto:billing@cielohermosa.com" className="underline">billing@cielohermosa.com</a>
</p>
</div>
+19 -19
View File
@@ -27,17 +27,17 @@ export default async function BillingPage({ params }: Props) {
resolvedBrandId = firstBrand.id;
} else {
return (
<main className="min-h-screen bg-stone-100 px-6 py-12">
<main className="min-h-screen bg-[var(--admin-bg)] px-6 py-12">
<div className="mx-auto max-w-6xl">
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<span>/</span>
<span className="text-stone-800">Billing</span>
<span className="text-[var(--admin-text-primary)]">Billing</span>
</nav>
<div className="card p-8 shadow-xl text-center">
<h1 className="text-2xl font-bold text-stone-950">No Brands Found</h1>
<p className="mt-2 text-stone-500">Create a brand in the database before accessing billing settings.</p>
<a href="/admin" className="mt-4 inline-block rounded-xl bg-blue-600 hover:bg-blue-700 px-6 py-3 text-sm font-medium text-white transition-colors">
<div className="rounded-2xl bg-white shadow-md ring-1 ring-[var(--admin-border)] p-8 text-center">
<h1 className="text-2xl font-bold text-[var(--admin-text-primary)]">No Brands Found</h1>
<p className="mt-2 text-[var(--admin-text-muted)]">Create a brand in the database before accessing billing settings.</p>
<a href="/admin" className="mt-4 inline-block rounded-xl bg-[var(--admin-accent)] hover:bg-[var(--admin-accent-hover)] px-6 py-3 text-sm font-medium text-white transition-colors">
Back to Admin
</a>
</div>
@@ -69,31 +69,31 @@ export default async function BillingPage({ params }: Props) {
const hasStripeCustomer = !!brand?.stripe_customer_id;
return (
<main className="min-h-screen bg-stone-100">
<main className="min-h-screen bg-[var(--admin-bg)]">
{/* Platform billing header */}
<div className="bg-stone-200 border-b border-stone-300 px-6 py-3">
<div className="bg-[var(--admin-bg-subtle)] border-b border-[var(--admin-border)] px-6 py-3">
<div className="mx-auto max-w-6xl">
<p className="text-xs text-stone-600">
<span className="font-medium text-stone-700">Route Commerce Platform Billing</span>
<p className="text-xs text-[var(--admin-text-muted)]">
<span className="font-medium text-[var(--admin-text-primary)]">Route Commerce Platform Billing</span>
{" — "}Invoiced by Cielo Hermosa, LLC · Manage your platform subscription and add-ons.
{" "}Questions? <a href="mailto:billing@cielohermosa.com" className="text-blue-600 hover:text-blue-700 underline transition-colors">billing@cielohermosa.com</a>
{" "}Questions? <a href="mailto:billing@cielohermosa.com" className="text-[var(--admin-accent)] hover:text-[var(--admin-accent-hover)] underline transition-colors">billing@cielohermosa.com</a>
</p>
</div>
</div>
<div className="mx-auto max-w-6xl px-6 py-10">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
<span>/</span>
<a href="/admin/settings" className="hover:text-stone-800 transition-colors">Settings</a>
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
<span>/</span>
<span className="text-stone-800">Billing</span>
<span className="text-[var(--admin-text-primary)]">Billing</span>
</nav>
<div className="mb-8">
<h1 className="text-3xl font-bold text-stone-950">Billing &amp; Subscription</h1>
<p className="mt-1 text-stone-500">
<h1 className="text-3xl font-bold text-[var(--admin-text-primary)]">Billing &amp; Subscription</h1>
<p className="mt-1 text-[var(--admin-text-muted)]">
Manage your Route Commerce subscription for {brand?.name ?? "your brand"}.
</p>
</div>
+3 -68
View File
@@ -1,70 +1,5 @@
import { redirect } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
import { getBrandSettings } from "@/actions/brand-settings";
import BrandSettingsForm from "@/components/admin/BrandSettingsForm";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
export default async function BrandSettingsPage() {
const adminUser = await getAdminUser();
if (!adminUser) return <AdminAccessDenied />;
if (!adminUser.can_manage_settings && adminUser.role !== "platform_admin") {
redirect("/admin");
}
const isPlatformAdmin = adminUser.role === "platform_admin";
const brandId = adminUser.brand_id ?? "";
// Platform admins: fetch all brands for the picker
const brands = isPlatformAdmin
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
: [];
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
// Get brand name for display
const brandName =
brands.find((b) => b.id === effectiveBrandId)?.name ??
(await supabase.from("brands").select("name").eq("id", effectiveBrandId).single()).data?.name ??
"Unknown Brand";
const result = await getBrandSettings(effectiveBrandId);
const settings = result.success ? result.settings : null;
return (
<main className="min-h-screen bg-stone-100 px-6 py-10">
<div className="mx-auto max-w-2xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
<span>/</span>
<span className="text-stone-800">Brand Settings</span>
</nav>
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-emerald-100 border border-emerald-200">
<svg className="h-5 w-5 text-emerald-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.592 9.592c1.106.105 1.35.374 1.591.659l4.317 4.317c.221.241.375.574.375.896v.318c0 .621-.504 1.125-1.125 1.125H5.25A2.25 2.25 0 013 16.5v-4.318c0-.597-.237-1.17-.659-1.591l-9.592-9.592A2.25 2.25 0 012.25 5.25m5.318 4.5v4.318l3.591 3.591M5.25 7.5a2.25 2.25 0 012.25-2.25h4.318m0 0L9 10.5m5.25-2.25v4.318m0 0L14.25 12m5.25-2.25H9.75" />
</svg>
</div>
<h1 className="text-3xl font-bold text-stone-950">Brand Settings</h1>
</div>
<p className="mt-2 text-stone-500">
Company information, logos, and default signatures used across the platform.
</p>
</div>
<div className="card p-6 shadow-xl">
<BrandSettingsForm
settings={settings}
brandId={effectiveBrandId}
brandName={brandName}
brands={brands}
isPlatformAdmin={isPlatformAdmin}
/>
</div>
</div>
</main>
);
}
export default function BrandSettingsRedirect() {
redirect("/admin/settings#brand");
}
+6
View File
@@ -1,6 +1,7 @@
import { redirect } from "next/navigation";
import { getAdminUser } from "@/lib/admin-permissions";
import { getAdminUsers, getBrands } from "@/actions/admin/users";
import { getPaymentSettings } from "@/actions/payments";
import SettingsClient from "@/components/admin/SettingsClient";
export const metadata = {
@@ -19,11 +20,16 @@ export default async function AdminSettingsPage() {
getBrands(),
]);
// Fetch payment settings for the brand tab
const paymentResult = await getPaymentSettings(brandId);
const paymentSettings = paymentResult.success ? paymentResult.settings : null;
return (
<SettingsClient
brandId={brandId}
users={error ? [] : users}
brands={brands}
paymentSettings={paymentSettings}
currentUser={{
id: adminUser.id ?? adminUser.user_id,
role: adminUser.role,
+3 -58
View File
@@ -1,61 +1,6 @@
import { redirect } from "next/navigation";
import { supabase } from "@/lib/supabase";
import { getAdminUser } from "@/lib/admin-permissions";
import { getPaymentSettings } from "@/actions/payments";
import PaymentSettingsForm from "@/components/admin/PaymentSettingsForm";
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
export default async function PaymentSettingsPage() {
const adminUser = await getAdminUser();
if (!adminUser) return <AdminAccessDenied />;
if (!adminUser.can_manage_orders || adminUser.role === "store_employee") redirect("/admin/pickup");
const isPlatformAdmin = adminUser.role === "platform_admin";
const brandId = adminUser.brand_id ?? "";
// Platform admins: fetch all brands for the picker
const brands = isPlatformAdmin
? (await supabase.from("brands").select("id, name").order("name")).data ?? []
: [];
const effectiveBrandId = brandId || (brands[0]?.id ?? "");
const result = await getPaymentSettings(effectiveBrandId);
const settings = result.success ? result.settings : null;
return (
<main className="min-h-screen bg-stone-100 px-6 py-10">
<div className="mx-auto max-w-2xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-xs text-stone-500 mb-6">
<a href="/admin" className="hover:text-stone-800 transition-colors">Admin</a>
<span>/</span>
<span className="text-stone-800">Payments</span>
</nav>
<div className="mb-8">
<div className="flex items-center gap-3 mb-2">
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-stone-200 border border-stone-300">
<svg className="h-5 w-5 text-stone-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m3.75 0h3m3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m-3.75 0h3m3.75 0h3m-3.75 0h3m-3.75 0h3" />
</svg>
</div>
<h1 className="text-3xl font-bold text-stone-950">Payment Settings</h1>
</div>
<p className="mt-2 text-stone-500">
Configure your payment provider for checkout processing.
</p>
</div>
<div className="card p-6 shadow-xl">
<PaymentSettingsForm
settings={settings}
brandId={effectiveBrandId}
brands={brands}
isPlatformAdmin={isPlatformAdmin}
/>
</div>
</div>
</main>
);
}
// Payment settings are now part of the Brand tab in Settings
redirect("/admin/settings#brand");
}