import { createFileRoute, notFound } from "@tanstack/react-router"; import { useCustomer } from "autumn-js/react"; import { useState } from "react"; import { useSession } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { useCanManageBilling } from "@/client/features/team/organizationQueries"; import { captureClientEvent } from "@/client/lib/posthog"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { buildCheckoutSuccessUrl } from "@/client/features/billing/checkout-url"; import { BillingUsageChart } from "@/client/features/billing/BillingUsageChart"; import { BillingFeatureBreakdown } from "@/client/features/billing/BillingFeatureBreakdown"; import { parseTopUpAmount } from "@/client/features/billing/HostedBillingContentUtils"; import { getBillingRouteState } from "@/client/features/billing/route-state"; import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection"; import { AUTUMN_CHECKOUT_SESSION_PARAMS, AUTUMN_PAID_PLAN_ID, BILLING_ROUTE, AUTUMN_SEO_DATA_BALANCE_FEATURE_ID, LOW_CREDITS_THRESHOLD_USD, AUTUMN_SEO_DATA_CREDITS_PER_USD, AUTUMN_SEO_DATA_TOP_UP_PLAN_ID, AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID, autumnSeoDataCreditsToUsd, } from "@/shared/billing"; export const Route = createFileRoute("/_app/billing")({ beforeLoad: () => { if (!isHostedClientAuthMode()) { throw notFound(); } }, component: BillingPage, }); function BillingPage() { const { data: session, isPending: isSessionPending } = useSession(); const [topUpAmount, setTopUpAmount] = useState("20"); const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); const customerQuery = useCustomer({ queryOptions: { enabled: Boolean(session?.user?.id), }, }); // Subscription changes are owner-only; other members see balances but are // pointed at the owner instead of checkout (the server enforces this too). const canManageBilling = useCanManageBilling(); const planStatus = getCustomerPlanStatus(customerQuery.data); const isFreePlan = planStatus === "free"; const billingRouteState = getBillingRouteState({ hasSession: Boolean(session?.user?.id), isSessionPending, isCustomerLoading: customerQuery.isLoading, isCustomerError: customerQuery.isError, }); const monthlyRemaining = autumnSeoDataCreditsToUsd( customerQuery.data?.balances?.[AUTUMN_SEO_DATA_BALANCE_FEATURE_ID] ?.remaining ?? 0, ); const topUpRemaining = autumnSeoDataCreditsToUsd( customerQuery.data?.balances?.[AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID] ?.remaining ?? 0, ); const totalRemaining = monthlyRemaining + topUpRemaining; const { isValid: isValidTopUp, parsed: parsedTopUpAmount } = parseTopUpAmount(topUpAmount); if (billingRouteState === "loading") { return null; } if (billingRouteState === "error") { return (

Billing unavailable

{getStandardErrorMessage( customerQuery.error, "We couldn't load your billing details right now. Please try again.", )}

); } function startUpgradeCheckout() { captureClientEvent("billing:checkout_start"); return customerQuery.attach({ planId: AUTUMN_PAID_PLAN_ID, redirectMode: "always", successUrl: buildCheckoutSuccessUrl(BILLING_ROUTE), checkoutSessionParams: AUTUMN_CHECKOUT_SESSION_PARAMS, }); } async function runAction( callback: () => Promise, fallbackMessage: string, ) { setError(null); setIsPending(true); try { await callback(); await customerQuery.refetch(); } catch (err) { setError(getStandardErrorMessage(err, fallbackMessage)); } finally { setIsPending(false); } } if (isPending) { return (

Redirecting to Stripe...

); } return (

Billing

{/* Subscription card */}
${totalRemaining.toFixed(2)}{" "} remaining
{!isFreePlan ? (
Monthly ${monthlyRemaining.toFixed(2)} · Top-ups ${topUpRemaining.toFixed(2)}
) : null} {totalRemaining <= 0 ? (

You’ve used all your credits.{" "} {isFreePlan ? "Upgrade your plan to continue." : "Buy more credits below to continue."}

) : totalRemaining < LOW_CREDITS_THRESHOLD_USD ? (

You’re running low on credits.{" "} {isFreePlan ? "Upgrade to get $10/month." : "Buy more credits below."}

) : null}
Plan{" "} {isFreePlan ? "Free Plan" : "Base Plan"}
{!canManageBilling ? (

Only the organization owner can change the plan or buy credits. Ask them if you need more.

) : isFreePlan ? (
Base Plan $10/month
    {[ "Access to all CrawlerX features", "Includes $10.00 of Usage Credits each month", ].map((item) => (
  • {item}
  • ))}
) : ( )}
{/* Buy credits card — paid plan only, owner-only */} {!isFreePlan && canManageBilling ? (
Buy credits

Top-up credits never expire and are used after your monthly credits.

$ setTopUpAmount(e.target.value)} />
{topUpAmount.trim() !== "" && !isValidTopUp ? (

Enter between $10–$99.

) : null}
) : null}
{/* Usage chart */} {/* Per-feature usage breakdown */} {error ?

{error}

: null}

Billing is powered by Stripe.

); }