import { useEffect } from "react"; import { Dialog } from "@cloudflare/kumo/components/dialog"; import { useQuery } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { getUpsellInfo, startProCheckout, startTopupCheckout } from "~/fns/billing"; import { useAction } from "~/hooks/use-action"; import { trackEvent } from "~/lib/analytics"; import { formatQuantity } from "~/lib/meters"; import type { MeterDef } from "~/lib/meters"; import { usd } from "~/lib/pricing-display"; import { dismissUpsell, useUpsell } from "~/lib/upsell"; import type { PaymentDetail } from "~/lib/upsell"; import { Button, buttonClass } from "~/ui/Button"; import { useViewer } from "~/hooks/use-viewer"; import { ErrorBanner } from "~/ui/Callout"; import { VerifyEmailHint } from "~/components/VerifyEmailHint"; import { canWriteAsViewer } from "~/lib/email-verification"; function allowance(unit: MeterDef["unit"] | undefined, value: number | null): string { if (value === null) return "unlimited"; if (value === 0) return "none"; return formatQuantity(unit ?? "count", value); } const HEADLINES: Record = { plan_limit: "You've reached this month's limit", spending_limit: "You've reached your spending limit", paid_feature: "This needs a paid plan", }; function OwnerMustUpgrade({ detail }: { detail: PaymentDetail }) { return (
{HEADLINES[detail.reason]}

{detail.message}

This limit belongs to the account that owns this work. Ask its owner to upgrade, add credit or raise the spending limit.

See pricing
); } function UpsellBody({ detail }: { detail: PaymentDetail }) { const viewer = useViewer(); const org = detail.account?.kind === "org" ? detail.account.name : null; // getUpsellInfo resolves the viewer's own account or one of their orgs; a site admin looking at another // user's account only gets the link. const fetchable = org !== null || detail.account?.name === viewer?.name; const info = useQuery({ queryKey: ["upsell", org, detail.meter], queryFn: () => getUpsellInfo({ data: { org, meter: detail.meter } }), staleTime: 60_000, enabled: fetchable, }); const checkout = useAction(startProCheckout, { invalidate: false }); const topup = useAction(startTopupCheckout, { invalidate: false }); const go = (result: { url: string } | undefined, kind: string) => { if (!result?.url) return; trackEvent("upsell_checkout_started", { kind, meter: detail.meter, reason: detail.reason }); window.location.assign(result.url); }; const data = info.data; const billingPath = detail.billingPath ?? (org ? `/orgs/${org}/billing` : "/settings/billing"); const unit = data?.meter?.unit as MeterDef["unit"] | undefined; const canUpgrade = Boolean( data && data.billingEnabled && data.pro.available && !data.plan.billsOverage && detail.reason !== "spending_limit", ); const verified = canWriteAsViewer(viewer); const canEdit = (data?.account.canEdit ?? false) && verified; return (
{HEADLINES[detail.reason]}

{detail.message}

{detail.account && (

Account: {detail.account.name} {data ? ` ยท ${data.plan.label} plan` : ""}

)}
{data?.meter && ( {[ { label: "Used this month", value: data.used === null ? null : allowance(unit, data.used) }, { label: "Free includes", value: allowance(unit, data.included.free) }, { label: `${data.pro.label} includes (${usd(data.pro.priceCents / 100)}/month)`, value: allowance(unit, data.included.pro), }, { label: "Beyond that, or with credit", value: data.rate }, ] .filter((row) => row.value) .map((row) => ( ))}
{row.label} {row.value}
)} {data && !data.billingEnabled ? (

Billing isn't set up on this instance. Ask a site administrator to change this account's plan.

) : (
{!verified && } {canUpgrade && ( )} {data && data.packs.length > 0 && (

Or buy prepaid credit, which never expires:

{data.packs.map((pack) => ( ))}
)} {detail.reason === "spending_limit" && (

Raise or remove the monthly spending limit on the billing page.

)} {data && !canEdit && (

Only the account owner can upgrade or buy credit.

)}
)}
Billing and pricing
); } /** One app-wide dialog that opens whenever an action comes back 402 Payment Required. */ export function UpsellDialog() { const detail = useUpsell(); useEffect(() => { if (detail) trackEvent("upsell_shown", { meter: detail.meter, reason: detail.reason }); }, [detail]); return ( !open && dismissUpsell()}> {detail && (detail.manageable ? ( ) : ( ))} ); }