import { Link, createFileRoute, useNavigate } from "@tanstack/react-router"; import { useCustomer } from "autumn-js/react"; import { useEffect, useState } from "react"; import { ArrowRight, Settings, User } from "lucide-react"; import { ThemePreferenceMenuItems } from "@/client/components/ThemePreferenceMenuItems"; import { captureClientEvent } from "@/client/lib/posthog"; import { signOutAndRedirect, useSession } from "@/lib/auth-client"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { getSubscribeRouteState } from "@/client/features/billing/route-state"; import { getCustomerPlanStatus } from "@/client/features/billing/plan-detection"; import { normalizeAuthRedirect } from "@/lib/auth-redirect"; import { useCanManageBilling } from "@/client/features/team/organizationQueries"; import { AUTUMN_CHECKOUT_SESSION_PARAMS, AUTUMN_MANAGED_ACCESS_FEATURE_ID, AUTUMN_PAID_PLAN_ID, } from "@/shared/billing"; const SUPPORT_EMAIL = "ben@openseo.so"; const PLAN_FEATURES = [ "Keyword research, backlinks, rank tracking, and site audits", "MCP server and agent skills for Claude, Cursor, and ChatGPT", "Google Search Console Integration", "Includes $10.00 of Usage Credits each month", ]; // How long the post-checkout "finalizing" screen polls Autumn before giving // up and letting the user through anyway. const FINALIZING_TIMEOUT_MS = 30_000; export const Route = createFileRoute("/_authenticated/subscribe")({ validateSearch: ( search: Record, ): { upgrade?: true; redirect?: string; checkout?: "success" } => ({ upgrade: search.upgrade === true || search.upgrade === "true" ? true : undefined, redirect: typeof search.redirect === "string" ? normalizeAuthRedirect(search.redirect) : undefined, checkout: search.checkout === "success" ? "success" : undefined, }), component: SubscribePage, }); function SubscribePage() { const navigate = useNavigate(); const { upgrade: isUpgradeFlow, redirect, checkout } = Route.useSearch(); const { data: session } = useSession(); const [isAttaching, setIsAttaching] = useState(false); const [error, setError] = useState(null); const [finalizingTimedOut, setFinalizingTimedOut] = useState(false); const checkoutCompleted = checkout === "success"; const hasSession = Boolean(session?.user?.id); const customerQuery = useCustomer({ queryOptions: { enabled: hasSession, }, }); // Checkout is owner-only; other members hitting the paywall are pointed at // their organization owner instead of a Subscribe button that would 403. const canManageBilling = useCanManageBilling(); // Read managed access from the already-loaded Autumn customer (local, no API // call) instead of a separate server round-trip. Self-hosted has no Autumn // customer, so mirror the server's "always granted" behavior there. const hasManagedAccess = isHostedClientAuthMode() ? customerQuery.check({ featureId: AUTUMN_MANAGED_ACCESS_FEATURE_ID }) .allowed : true; const planStatus = getCustomerPlanStatus(customerQuery.data); const subscribeRouteState = getSubscribeRouteState({ hasSession, isCustomerLoading: customerQuery.isLoading, isCustomerError: customerQuery.isError, hasManagedAccess, planStatus, isUpgradeFlow: isUpgradeFlow === true, checkoutCompleted, finalizingTimedOut, }); // Autumn can lag Stripe by a few seconds after checkout; poll until the // subscription shows up so the just-paid user isn't shown the paywall again. const isFinalizing = subscribeRouteState === "finalizing"; const { refetch: refetchCustomer } = customerQuery; useEffect(() => { if (!isFinalizing) return; const interval = setInterval(() => { void refetchCustomer(); }, 2000); return () => clearInterval(interval); }, [refetchCustomer, isFinalizing]); // Armed once on landing with checkout=success (not on the finalizing state, // which a transient poll error can leave and re-enter) so the deadline is a // hard bound from arrival. useEffect(() => { if (!checkoutCompleted || finalizingTimedOut) return; const timeout = setTimeout( () => setFinalizingTimedOut(true), FINALIZING_TIMEOUT_MS, ); return () => clearTimeout(timeout); }, [checkoutCompleted, finalizingTimedOut]); useEffect(() => { if (subscribeRouteState === "redirectToApp") { if (checkoutCompleted) { captureClientEvent("billing:checkout_success"); } void navigate({ href: redirect ?? "/", replace: true }); } }, [checkoutCompleted, navigate, redirect, subscribeRouteState]); useEffect(() => { if (subscribeRouteState === "showPaywall" && !isUpgradeFlow) { captureClientEvent("billing:paywall_viewed"); } }, [isUpgradeFlow, subscribeRouteState]); if ( subscribeRouteState === "loading" || subscribeRouteState === "redirectToApp" ) { return null; } if (subscribeRouteState === "finalizing") { return (
CrawlerX

Finalizing your subscription…

This usually takes a few seconds.

Taking longer?{" "} Email {SUPPORT_EMAIL} .

); } if (subscribeRouteState === "error") { return (
CrawlerX

Billing unavailable

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

); } async function handleSubscribe() { setError(null); setIsAttaching(true); try { captureClientEvent("billing:checkout_start"); const successUrl = new URL(window.location.href); successUrl.searchParams.set("checkout", "success"); await customerQuery.attach({ planId: AUTUMN_PAID_PLAN_ID, redirectMode: "always", successUrl: successUrl.toString(), checkoutSessionParams: AUTUMN_CHECKOUT_SESSION_PARAMS, }); } catch (err) { setError( getStandardErrorMessage( err, "We couldn't start the checkout. Please try again.", ), ); setIsAttaching(false); } } const firstName = session?.user?.name?.split(" ")[0] || ""; return (
CrawlerX

{isUpgradeFlow ? "Upgrade your plan" : firstName ? `Welcome to CrawlerX, ${firstName}!` : "Welcome to CrawlerX!"}

SEO on your terms. All your SEO tools in one place at a fair price.

Base Plan $10/month
{error ?

{error}

: null} {canManageBilling ? ( ) : (

Only the organization owner can subscribe. Ask them to upgrade this organization.

)}

30-day money-back guarantee . Cancel anytime. Powered by Stripe.

Questions? Email {SUPPORT_EMAIL}.

{isUpgradeFlow ? ( ) : null}
); } function SubscribePageAccountMenu({ email }: { email: string | undefined }) { if (!email) return null; const handleSignOut = () => signOutAndRedirect(); return (
  • {email}
  • Settings
); }