Purchase flow: name -> placeholder -> configure via the shipped PlanConfigurator -> Paddle overlay with custom_data -> paste install UUID to link and issue. lineItemsFor mirrors the Go catalogue.LineItems/billable exactly (base included in exactly one place). ManageBillingButton opens the hosted portal. Paddle token and env are baked into the build, never fetched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
36 lines
1.2 KiB
TypeScript
36 lines
1.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { ApiError, api } from "@/lib/api";
|
|
import { Button } from "@/components/Button";
|
|
|
|
/* Opens Paddle's hosted customer portal in a new tab. The account learns its
|
|
* paddle_customer_id from its first paid subscription's webhook, so this reports
|
|
* a plain message rather than erroring when there is no billing account yet. */
|
|
export function ManageBillingButton() {
|
|
const [busy, setBusy] = useState(false);
|
|
const [note, setNote] = useState<string | null>(null);
|
|
|
|
async function open() {
|
|
setBusy(true);
|
|
setNote(null);
|
|
try {
|
|
const { url } = await api.billingPortal();
|
|
window.open(url, "_blank", "noopener");
|
|
} catch (e) {
|
|
setNote(e instanceof ApiError ? e.message : "Could not open billing.");
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<span className="inline-flex items-center gap-2">
|
|
<Button type="button" variant="line" onClick={open} disabled={busy}>
|
|
{busy ? "Opening…" : "Manage billing"}
|
|
</Button>
|
|
{note && <span className="text-[0.78rem] text-ink-3">{note}</span>}
|
|
</span>
|
|
);
|
|
}
|