-
-
-
- {
- void runBillingAction(
- "open-portal",
- () =>
- customerQuery.openCustomerPortal({
- returnUrl: window.location.href,
- }),
- "We could not open the billing portal. Please try again.",
- );
- }}
- onStartPlan={() => {
- void runBillingAction(
- "start-plan",
- () =>
- customerQuery.attach({
- planId: AUTUMN_PAID_PLAN_ID,
- redirectMode: "always",
- successUrl: window.location.href,
- }),
- "We could not open the hosted billing flow. Please try again.",
- );
- }}
- />
-
- {
- void runBillingAction(
- "top-up",
- () =>
- customerQuery.attach({
- planId: AUTUMN_SEO_DATA_TOP_UP_PLAN_ID,
- redirectMode: "always",
- successUrl: window.location.href,
- featureQuantities: [
- {
- featureId: AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
- quantity: Math.round(
- parsedTopUpAmount * AUTUMN_SEO_DATA_CREDITS_PER_USD,
- ),
- },
- ],
- }),
- "We could not open the credit purchase flow. Please try again.",
- );
- }}
- />
-
-
-
-
-
-
- Hosted billing is powered by Autumn.
-
-
- );
-}
-
-function SubscriptionSection(args: {
- basePlanName: string;
- basePlanPrice: string;
- hasManagedServiceAccess: boolean;
- includedCreditsLabel: string;
- isActionPending: boolean;
- isPortalPending: boolean;
- isStartPlanPending: boolean;
- onOpenPortal: () => void;
- onStartPlan: () => void;
-}) {
- return (
-
-
+
OpenSEO
-
+
{projectId
? projectNavItems.map((item) => {
const { icon: Icon, matchSegment, ...linkProps } = item;
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index ba3acbb..17ef7af 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -12,10 +12,12 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as VerifyEmailRouteImport } from './routes/verify-email'
import { Route as ResetPasswordRouteImport } from './routes/reset-password'
import { Route as ForgotPasswordRouteImport } from './routes/forgot-password'
+import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
import { Route as AuthRouteImport } from './routes/_auth'
import { Route as ProjectRouteRouteImport } from './routes/_project/route'
import { Route as AppRouteRouteImport } from './routes/_app/route'
import { Route as AppIndexRouteImport } from './routes/_app/index'
+import { Route as AuthenticatedSubscribeRouteImport } from './routes/_authenticated.subscribe'
import { Route as AuthSignUpRouteImport } from './routes/_auth.sign-up'
import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in'
import { Route as AppBillingRouteImport } from './routes/_app/billing'
@@ -48,6 +50,10 @@ const ForgotPasswordRoute = ForgotPasswordRouteImport.update({
path: '/forgot-password',
getParentRoute: () => rootRouteImport,
} as any)
+const AuthenticatedRoute = AuthenticatedRouteImport.update({
+ id: '/_authenticated',
+ getParentRoute: () => rootRouteImport,
+} as any)
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
@@ -65,6 +71,11 @@ const AppIndexRoute = AppIndexRouteImport.update({
path: '/',
getParentRoute: () => AppRouteRoute,
} as any)
+const AuthenticatedSubscribeRoute = AuthenticatedSubscribeRouteImport.update({
+ id: '/subscribe',
+ path: '/subscribe',
+ getParentRoute: () => AuthenticatedRoute,
+} as any)
const AuthSignUpRoute = AuthSignUpRouteImport.update({
id: '/sign-up',
path: '/sign-up',
@@ -158,6 +169,7 @@ export interface FileRoutesByFullPath {
'/billing': typeof AppBillingRoute
'/sign-in': typeof AuthSignInRoute
'/sign-up': typeof AuthSignUpRoute
+ '/subscribe': typeof AuthenticatedSubscribeRoute
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute
@@ -180,6 +192,7 @@ export interface FileRoutesByTo {
'/billing': typeof AppBillingRoute
'/sign-in': typeof AuthSignInRoute
'/sign-up': typeof AuthSignUpRoute
+ '/subscribe': typeof AuthenticatedSubscribeRoute
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/api/autumn/$': typeof ApiAutumnSplatRoute
@@ -197,12 +210,14 @@ export interface FileRoutesById {
'/_app': typeof AppRouteRouteWithChildren
'/_project': typeof ProjectRouteRouteWithChildren
'/_auth': typeof AuthRouteWithChildren
+ '/_authenticated': typeof AuthenticatedRouteWithChildren
'/forgot-password': typeof ForgotPasswordRoute
'/reset-password': typeof ResetPasswordRoute
'/verify-email': typeof VerifyEmailRoute
'/_app/billing': typeof AppBillingRoute
'/_auth/sign-in': typeof AuthSignInRoute
'/_auth/sign-up': typeof AuthSignUpRoute
+ '/_authenticated/subscribe': typeof AuthenticatedSubscribeRoute
'/_app/': typeof AppIndexRoute
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
@@ -228,6 +243,7 @@ export interface FileRouteTypes {
| '/billing'
| '/sign-in'
| '/sign-up'
+ | '/subscribe'
| '/p/$projectId'
| '/help/dataforseo-api-key'
| '/api/auth/$'
@@ -250,6 +266,7 @@ export interface FileRouteTypes {
| '/billing'
| '/sign-in'
| '/sign-up'
+ | '/subscribe'
| '/help/dataforseo-api-key'
| '/api/auth/$'
| '/api/autumn/$'
@@ -266,12 +283,14 @@ export interface FileRouteTypes {
| '/_app'
| '/_project'
| '/_auth'
+ | '/_authenticated'
| '/forgot-password'
| '/reset-password'
| '/verify-email'
| '/_app/billing'
| '/_auth/sign-in'
| '/_auth/sign-up'
+ | '/_authenticated/subscribe'
| '/_app/'
| '/_project/p/$projectId'
| '/_app/help/dataforseo-api-key'
@@ -292,6 +311,7 @@ export interface RootRouteChildren {
AppRouteRoute: typeof AppRouteRouteWithChildren
ProjectRouteRoute: typeof ProjectRouteRouteWithChildren
AuthRoute: typeof AuthRouteWithChildren
+ AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
ForgotPasswordRoute: typeof ForgotPasswordRoute
ResetPasswordRoute: typeof ResetPasswordRoute
VerifyEmailRoute: typeof VerifyEmailRoute
@@ -322,6 +342,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ForgotPasswordRouteImport
parentRoute: typeof rootRouteImport
}
+ '/_authenticated': {
+ id: '/_authenticated'
+ path: ''
+ fullPath: '/'
+ preLoaderRoute: typeof AuthenticatedRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/_auth': {
id: '/_auth'
path: ''
@@ -350,6 +377,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AppIndexRouteImport
parentRoute: typeof AppRouteRoute
}
+ '/_authenticated/subscribe': {
+ id: '/_authenticated/subscribe'
+ path: '/subscribe'
+ fullPath: '/subscribe'
+ preLoaderRoute: typeof AuthenticatedSubscribeRouteImport
+ parentRoute: typeof AuthenticatedRoute
+ }
'/_auth/sign-up': {
id: '/_auth/sign-up'
path: '/sign-up'
@@ -548,10 +582,23 @@ const AuthRouteChildren: AuthRouteChildren = {
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
+interface AuthenticatedRouteChildren {
+ AuthenticatedSubscribeRoute: typeof AuthenticatedSubscribeRoute
+}
+
+const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
+ AuthenticatedSubscribeRoute: AuthenticatedSubscribeRoute,
+}
+
+const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
+ AuthenticatedRouteChildren,
+)
+
const rootRouteChildren: RootRouteChildren = {
AppRouteRoute: AppRouteRouteWithChildren,
ProjectRouteRoute: ProjectRouteRouteWithChildren,
AuthRoute: AuthRouteWithChildren,
+ AuthenticatedRoute: AuthenticatedRouteWithChildren,
ForgotPasswordRoute: ForgotPasswordRoute,
ResetPasswordRoute: ResetPasswordRoute,
VerifyEmailRoute: VerifyEmailRoute,
diff --git a/src/routes/_app/billing.tsx b/src/routes/_app/billing.tsx
index 60b0ed0..ff05f8f 100644
--- a/src/routes/_app/billing.tsx
+++ b/src/routes/_app/billing.tsx
@@ -1,10 +1,21 @@
-import { createFileRoute, notFound } from "@tanstack/react-router";
-import { AutumnProvider, useCustomer, useListPlans } from "autumn-js/react";
-import { LoaderCircle } from "lucide-react";
-import { CenteredCard } from "@/client/features/billing/BillingRouteParts";
-import { HostedBillingContent } from "@/client/features/billing/HostedBillingContent";
+import { createFileRoute, notFound, useNavigate } from "@tanstack/react-router";
+import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useEffect, useState } from "react";
import { useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
+import { getStandardErrorMessage } from "@/client/lib/error-messages";
+import { BillingUsageChart } from "@/client/features/billing/BillingUsageChart";
+import { parseTopUpAmount } from "@/client/features/billing/HostedBillingContentUtils";
+import { getBillingRouteState } from "@/client/features/billing/route-state";
+import {
+ AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID,
+ AUTUMN_SEO_DATA_BALANCE_FEATURE_ID,
+ AUTUMN_SEO_DATA_CREDITS_PER_USD,
+ AUTUMN_SEO_DATA_TOP_UP_PLAN_ID,
+ AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
+ SUBSCRIBE_ROUTE,
+ autumnSeoDataCreditsToUsd,
+} from "@/shared/billing";
export const Route = createFileRoute("/_app/billing")({
beforeLoad: () => {
@@ -24,7 +35,11 @@ function BillingPage() {
}
function BillingPageContent() {
+ const navigate = useNavigate();
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: {
@@ -32,50 +47,206 @@ function BillingPageContent() {
},
});
- const plansQuery = useListPlans({
- queryOptions: {
- enabled: Boolean(session?.user?.id),
- },
+ const hasManagedServiceAccess = Boolean(
+ customerQuery.data?.flags?.[AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID],
+ );
+ const billingRouteState = getBillingRouteState({
+ hasSession: Boolean(session?.user?.id),
+ isSessionPending,
+ isCustomerLoading: customerQuery.isLoading,
+ isCustomerError: customerQuery.isError,
+ hasManagedServiceAccess,
});
+ 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);
+
+ useEffect(() => {
+ if (billingRouteState !== "redirectToSubscribe") {
+ return;
+ }
+
+ void navigate({ href: SUBSCRIBE_ROUTE, replace: true });
+ }, [billingRouteState, navigate]);
+
if (
- isSessionPending ||
- (session?.user?.id && (customerQuery.isLoading || plansQuery.isLoading))
+ billingRouteState === "loading" ||
+ billingRouteState === "redirectToSubscribe"
) {
+ return null;
+ }
+
+ if (billingRouteState === "error") {
return (
-
-
+
+
Billing unavailable
+
+ {getStandardErrorMessage(
+ customerQuery.error,
+ "We couldn't load your billing details right now. Please try again.",
+ )}
+
+
{
+ void customerQuery.refetch();
+ }}
+ >
+ Try again
+
);
}
- if (!session?.user?.id) {
- return (
-
- Go to sign in
-
- }
- />
- );
+ 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 (customerQuery.isError || plansQuery.isError) {
+ if (isPending) {
return (
-
+
+
Redirecting to Stripe...
+
);
}
return (
-
+
+
Billing
+
+
+ {/* Subscription card */}
+
+
+
+ ${totalRemaining.toFixed(2)}{" "}
+
+ remaining
+
+
+
+
+ Monthly ${monthlyRemaining.toFixed(2)}
+
+ ·
+
+ Top-ups ${topUpRemaining.toFixed(2)}
+
+
+
+
+
+ Subscription {" "}
+
+ {hasManagedServiceAccess ? "Active" : "Inactive"}
+
+
+
+
+ void runAction(
+ () =>
+ customerQuery.openCustomerPortal({
+ returnUrl: window.location.href,
+ }),
+ "We couldn't open the billing portal. Please try again.",
+ )
+ }
+ >
+ {isPending ? "Redirecting..." : "Manage subscription"}
+
+
+
+ {/* Buy credits card */}
+
+
+
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}
+
+
+
+ void runAction(
+ () =>
+ customerQuery.attach({
+ planId: AUTUMN_SEO_DATA_TOP_UP_PLAN_ID,
+ redirectMode: "always",
+ successUrl: window.location.href,
+ featureQuantities: [
+ {
+ featureId: AUTUMN_SEO_DATA_TOPUP_BALANCE_FEATURE_ID,
+ quantity: Math.round(
+ parsedTopUpAmount * AUTUMN_SEO_DATA_CREDITS_PER_USD,
+ ),
+ },
+ ],
+ }),
+ "We couldn't start the checkout. Please try again.",
+ )
+ }
+ >
+ Buy credits
+
+
+
+
+ {/* Usage chart */}
+ {hasManagedServiceAccess ?
: null}
+
+ {error ?
{error}
: null}
+
+
+ Billing is powered by Stripe.
+
+
);
}
diff --git a/src/routes/_app/index.tsx b/src/routes/_app/index.tsx
index 3abe662..81b2424 100644
--- a/src/routes/_app/index.tsx
+++ b/src/routes/_app/index.tsx
@@ -8,7 +8,7 @@ import {
} from "@/client/lib/error-messages";
import { AuthConfigErrorCard } from "@/client/components/AuthConfigErrorCard";
import { UnauthenticatedErrorCard } from "@/client/components/UnauthenticatedErrorCard";
-import { BILLING_ROUTE } from "@/shared/billing";
+import { SUBSCRIBE_ROUTE } from "@/shared/billing";
export const Route = createFileRoute("/_app/")({
component: IndexRedirect,
@@ -36,7 +36,7 @@ function IndexRedirect() {
return;
}
- void navigate({ href: BILLING_ROUTE });
+ void navigate({ href: SUBSCRIBE_ROUTE });
}, [error, navigate]);
if (isError) {
diff --git a/src/routes/_app/route.tsx b/src/routes/_app/route.tsx
index 625dd34..2cf05c9 100644
--- a/src/routes/_app/route.tsx
+++ b/src/routes/_app/route.tsx
@@ -1,11 +1,40 @@
-import { Outlet, createFileRoute } from "@tanstack/react-router";
+import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
import { AuthenticatedAppLayout } from "@/client/layout/AppShell";
+import { useSession } from "@/lib/auth-client";
+import { isHostedClientAuthMode } from "@/lib/auth-mode";
+import {
+ getCurrentAuthRedirectFromHref,
+ getSignInSearch,
+} from "@/lib/auth-redirect";
export const Route = createFileRoute("/_app")({
component: AppRouteLayout,
});
function AppRouteLayout() {
+ const navigate = useNavigate();
+ const { data: session, isPending } = useSession();
+ const isHostedMode = isHostedClientAuthMode();
+
+ useEffect(() => {
+ if (isPending || !isHostedMode || session?.user?.id) {
+ return;
+ }
+
+ void navigate({
+ to: "/sign-in",
+ search: getSignInSearch(
+ getCurrentAuthRedirectFromHref(window.location.href),
+ ),
+ replace: true,
+ });
+ }, [isPending, isHostedMode, session?.user?.id, navigate]);
+
+ if (isHostedMode && (isPending || !session?.user?.id)) {
+ return null;
+ }
+
return (
diff --git a/src/routes/_authenticated.subscribe.tsx b/src/routes/_authenticated.subscribe.tsx
new file mode 100644
index 0000000..3057ca9
--- /dev/null
+++ b/src/routes/_authenticated.subscribe.tsx
@@ -0,0 +1,162 @@
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { AutumnProvider, useCustomer } from "autumn-js/react";
+import { useEffect, useState } from "react";
+import { useSession } from "@/lib/auth-client";
+import { getStandardErrorMessage } from "@/client/lib/error-messages";
+import { getSubscribeRouteState } from "@/client/features/billing/route-state";
+import {
+ AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID,
+ AUTUMN_PAID_PLAN_ID,
+} from "@/shared/billing";
+
+export const Route = createFileRoute("/_authenticated/subscribe")({
+ component: SubscribePage,
+});
+
+function SubscribePage() {
+ return (
+
+
+
+ );
+}
+
+function SubscribePageContent() {
+ const navigate = useNavigate();
+ const { data: session } = useSession();
+ const [isAttaching, setIsAttaching] = useState(false);
+ const [error, setError] = useState(null);
+
+ const customerQuery = useCustomer({
+ queryOptions: {
+ enabled: Boolean(session?.user?.id),
+ },
+ });
+
+ const hasManagedServiceAccess = Boolean(
+ customerQuery.data?.flags?.[AUTUMN_MANAGED_SERVICE_ACCESS_FEATURE_ID],
+ );
+ const subscribeRouteState = getSubscribeRouteState({
+ hasSession: Boolean(session?.user?.id),
+ isCustomerLoading: customerQuery.isLoading,
+ isCustomerError: customerQuery.isError,
+ hasManagedServiceAccess,
+ });
+
+ useEffect(() => {
+ if (subscribeRouteState === "redirectToApp") {
+ void navigate({ to: "/", replace: true });
+ }
+ }, [navigate, subscribeRouteState]);
+
+ if (
+ subscribeRouteState === "loading" ||
+ subscribeRouteState === "redirectToApp"
+ ) {
+ return null;
+ }
+
+ if (subscribeRouteState === "error") {
+ return (
+
+
+
+
Billing unavailable
+
+
+
+ {getStandardErrorMessage(
+ customerQuery.error,
+ "We couldn't verify your billing status right now. Please try again.",
+ )}
+
+
+
{
+ void customerQuery.refetch();
+ }}
+ >
+ Try again
+
+
+ );
+ }
+
+ async function handleSubscribe() {
+ setError(null);
+ setIsAttaching(true);
+
+ try {
+ await customerQuery.attach({
+ planId: AUTUMN_PAID_PLAN_ID,
+ redirectMode: "always",
+ successUrl: window.location.origin,
+ });
+ } catch (err) {
+ setError(
+ getStandardErrorMessage(
+ err,
+ "We couldn't start the checkout. Please try again.",
+ ),
+ );
+ setIsAttaching(false);
+ }
+ }
+
+ return (
+
+
+
+
Get started
+
+
+
+
+ Base Plan
+ $10/month
+
+
+ {[
+ "Access to OpenSEO's managed service",
+ "Includes $10.00 of Usage Credits each month",
+ "Credits are consumed as you go for SEO data and AI features",
+ ].map((item) => (
+
+
+ —
+
+ {item}
+
+ ))}
+
+
+
+ {error ?
{error}
: null}
+
+
void handleSubscribe()}
+ >
+ {isAttaching ? "Redirecting..." : "Subscribe"}
+
+
+
+ Cancel anytime — no commitment. Powered by Stripe.
+
+
+ );
+}
diff --git a/src/routes/_authenticated.tsx b/src/routes/_authenticated.tsx
new file mode 100644
index 0000000..caf92b4
--- /dev/null
+++ b/src/routes/_authenticated.tsx
@@ -0,0 +1,35 @@
+import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useEffect } from "react";
+import { AuthPageShell } from "@/client/features/auth/AuthPage";
+import { useSession } from "@/lib/auth-client";
+import { isHostedClientAuthMode } from "@/lib/auth-mode";
+
+export const Route = createFileRoute("/_authenticated")({
+ component: AuthenticatedShellLayout,
+});
+
+function AuthenticatedShellLayout() {
+ const navigate = useNavigate();
+ const { data: session, isPending } = useSession();
+ const isHostedMode = isHostedClientAuthMode();
+
+ useEffect(() => {
+ if (isPending || !isHostedMode) return;
+ if (!session?.user?.id) {
+ void navigate({
+ to: "/sign-in",
+ search: { redirect: window.location.pathname },
+ });
+ }
+ }, [isPending, isHostedMode, session?.user?.id, navigate]);
+
+ if (!isHostedMode || isPending || !session?.user?.id) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/src/routes/verify-email.tsx b/src/routes/verify-email.tsx
index 6a20857..fa0814e 100644
--- a/src/routes/verify-email.tsx
+++ b/src/routes/verify-email.tsx
@@ -1,4 +1,4 @@
-import { Link, createFileRoute, useNavigate } from "@tanstack/react-router";
+import { Link, createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import {
@@ -94,7 +94,6 @@ function getVerifyEmailPageCopy({
function VerifyEmailPage() {
const search = Route.useSearch();
- const navigate = useNavigate();
const redirectTo = normalizeAuthRedirect(search.redirect);
const isHostedMode = isHostedClientAuthMode();
const { data: session, isPending } = useSession();
@@ -117,8 +116,13 @@ function VerifyEmailPage() {
return;
}
- void navigate({ href: redirectTo, replace: true });
- }, [isVerified, navigate, redirectTo]);
+ // Full page reload instead of client-side navigation: the auth→app
+ // transition needs a clean server-side load so that all server function
+ // handlers are freshly registered (client-side nav during Vite HMR can
+ // hit the server before updated handlers are ready, causing
+ // "action is not a function" errors).
+ window.location.replace(redirectTo);
+ }, [isVerified, redirectTo]);
async function handleResend() {
if (!email) return;
diff --git a/src/shared/billing.ts b/src/shared/billing.ts
index f857ab9..5f9ee05 100644
--- a/src/shared/billing.ts
+++ b/src/shared/billing.ts
@@ -1,4 +1,5 @@
export const BILLING_ROUTE = "/billing";
+export const SUBSCRIBE_ROUTE = "/subscribe";
export const AUTUMN_PAID_PLAN_ID = "base-plan";
export const AUTUMN_SEO_DATA_TOP_UP_PLAN_ID = "credit-top-up";