diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx
index 700e93a07..7d2d4d979 100644
--- a/apps/web/app/(app)/layout.tsx
+++ b/apps/web/app/(app)/layout.tsx
@@ -3,10 +3,12 @@
import { EnsureWorkspace } from "@/components/ensure-workspace"
import { PWAInstallPrompt } from "@/components/pwa-install-prompt"
import { SettingsModalProvider } from "@/components/settings/settings-modal"
+import { PromoCodeHost } from "@/hooks/use-promo-code"
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
+
{children}
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index 5de4eb43f..2ba044fde 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -11,6 +11,7 @@ import { Suspense } from "react"
import { Toaster } from "@ui/components/sonner"
import { NuqsAdapter } from "nuqs/adapters/next/app"
import { ThemeProvider } from "@/lib/theme-provider"
+import { PromoCodeCapture } from "@/hooks/use-promo-code"
const font = Space_Grotesk({
subsets: ["latin"],
@@ -95,6 +96,7 @@ export default function RootLayout({
includeCredentials={true}
headers={{ "X-App-Source": "nova" }}
>
+
diff --git a/apps/web/components/add-document/connections.tsx b/apps/web/components/add-document/connections.tsx
index 56339a085..8b24a6bd2 100644
--- a/apps/web/components/add-document/connections.tsx
+++ b/apps/web/components/add-document/connections.tsx
@@ -42,6 +42,7 @@ import {
getConnectionSubtitle,
} from "@/components/settings/sync-utils"
import type { ImportProvider } from "@/components/settings/sync-utils"
+import { usePromoCode } from "@/hooks/use-promo-code"
type GDriveSyncScope = "scoped" | "full"
@@ -309,6 +310,7 @@ interface ConnectContentProps {
export function ConnectContent({ selectedProject }: ConnectContentProps) {
const queryClient = useQueryClient()
const autumn = useCustomer()
+ const promoCode = usePromoCode()
const { connectorAccess } = useConnectorAccess()
const [connectingProvider, setConnectingProvider] =
useState(null)
@@ -330,8 +332,10 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
try {
const result = await autumn.attach({
planId,
+ discounts: promoCode.getDiscounts(),
successUrl: window.location.href,
})
+ promoCode.clear()
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
diff --git a/apps/web/components/add-document/index.tsx b/apps/web/components/add-document/index.tsx
index 1c231f7a0..6ab77d9a4 100644
--- a/apps/web/components/add-document/index.tsx
+++ b/apps/web/components/add-document/index.tsx
@@ -21,6 +21,7 @@ import { formatUsageNumber } from "@/lib/billing-utils"
import { SpaceSelector } from "../space-selector"
import { useIsMobile } from "@hooks/use-mobile"
import { addDocumentParam } from "@/lib/search-params"
+import { usePromoCode } from "@/hooks/use-promo-code"
type TabType = "note" | "link" | "file" | "connect"
@@ -153,6 +154,7 @@ export function AddDocument({
})
const autumn = useCustomer()
+ const promoCode = usePromoCode()
const {
tokensUsed,
searchesUsed,
@@ -342,8 +344,10 @@ export function AddDocument({
try {
const result = await autumn.attach({
planId: "api_pro",
+ discounts: promoCode.getDiscounts(),
successUrl: `${window.location.origin}/settings#account`,
})
+ promoCode.clear()
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
@@ -442,8 +446,10 @@ export function AddDocument({
try {
const result = await autumn.attach({
planId: "api_pro",
+ discounts: promoCode.getDiscounts(),
successUrl: `${window.location.origin}/settings#account`,
})
+ promoCode.clear()
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
diff --git a/apps/web/components/integrations-view.tsx b/apps/web/components/integrations-view.tsx
index 12031cd9c..900f4ca6d 100644
--- a/apps/web/components/integrations-view.tsx
+++ b/apps/web/components/integrations-view.tsx
@@ -80,6 +80,7 @@ import {
import { MCPSteps } from "./mcp-modal/mcp-detail-view"
import { GranolaConnectModal } from "./granola-connect-modal"
import { detectPluginSpace, detectPluginSource } from "@/lib/plugin-space"
+import { usePromoCode } from "@/hooks/use-promo-code"
type Connection = z.infer
@@ -2555,6 +2556,7 @@ export function IntegrationsView({
const { allProjects } = useContainerTags()
const shortcutsConnect = useShortcutsConnect()
const autumn = useCustomer({ queryOptions: { enabled: !publicMode } })
+ const promoCode = usePromoCode()
// connectorAccess covers pro-tier connectors (incl. company_brain orgs); plugins
// stay on hasProProduct. See useConnectorAccess.
const { hasPro: hasProProduct, connectorAccess } = useConnectorAccess({
@@ -2825,8 +2827,10 @@ export function IntegrationsView({
try {
const result = await autumn.attach({
planId: checkoutPlanId,
+ discounts: promoCode.getDiscounts(),
successUrl: `${window.location.origin}/integrations`,
})
+ promoCode.clear()
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
@@ -2837,7 +2841,7 @@ export function IntegrationsView({
toast.error("Failed to start checkout. Please try again.")
}
},
- [autumn],
+ [autumn, promoCode],
)
const redirectToLogin = useCallback(() => {
diff --git a/apps/web/components/integrations/plugins-detail.tsx b/apps/web/components/integrations/plugins-detail.tsx
index cb50ecaf7..cfcba4816 100644
--- a/apps/web/components/integrations/plugins-detail.tsx
+++ b/apps/web/components/integrations/plugins-detail.tsx
@@ -30,6 +30,7 @@ import {
type PluginInfo,
} from "@/lib/plugin-catalog"
import { INSET, InstallSteps, PillButton } from "./install-steps"
+import { usePromoCode } from "@/hooks/use-promo-code"
interface ConnectedPlugin {
id: string
@@ -418,6 +419,7 @@ function PluginRow({
export function PluginsDetail() {
const { org } = useAuth()
const autumn = useCustomer()
+ const promoCode = usePromoCode()
const queryClient = useQueryClient()
const [connectingPlugin, setConnectingPlugin] = useState(null)
const [finishSetupPluginId, setFinishSetupPluginId] = useState(
@@ -570,8 +572,10 @@ export function PluginsDetail() {
try {
const result = await autumn.attach({
planId: "api_pro",
+ discounts: promoCode.getDiscounts(),
successUrl: `${window.location.origin}/integrations`,
})
+ promoCode.clear()
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
diff --git a/apps/web/components/onboarding-brain/step-sources.tsx b/apps/web/components/onboarding-brain/step-sources.tsx
index d37339919..861235e2e 100644
--- a/apps/web/components/onboarding-brain/step-sources.tsx
+++ b/apps/web/components/onboarding-brain/step-sources.tsx
@@ -82,6 +82,7 @@ import { useCustomer } from "autumn-js/react"
import { toast } from "sonner"
import { analytics } from "@/lib/analytics"
import type { BrainMode } from "./types"
+import { usePromoCode } from "@/hooks/use-promo-code"
type SourceId =
| "drive"
@@ -614,6 +615,7 @@ function OnboardingPlansModal({
requestedPlan: RequiredPlan
}) {
const autumn = useCustomer()
+ const promoCode = usePromoCode()
const { currentPlan, isLoading } = useTokenUsage(autumn)
const [upgradingPlan, setUpgradingPlan] = useState(
null,
@@ -632,8 +634,10 @@ function OnboardingPlansModal({
try {
const result = await autumn.attach({
planId,
+ discounts: promoCode.getDiscounts(),
successUrl: window.location.href,
})
+ promoCode.clear()
if ((result as { paymentUrl?: string })?.paymentUrl) {
window.location.href = (result as { paymentUrl: string }).paymentUrl
return
diff --git a/apps/web/components/settings/billing.tsx b/apps/web/components/settings/billing.tsx
index b8e366a40..d69474b95 100644
--- a/apps/web/components/settings/billing.tsx
+++ b/apps/web/components/settings/billing.tsx
@@ -36,6 +36,7 @@ import {
} from "lucide-react"
import { useEffect, useMemo, useRef, useState } from "react"
import { toast } from "sonner"
+import { usePromoCode } from "@/hooks/use-promo-code"
const API_BASE =
process.env.NEXT_PUBLIC_BACKEND_URL ?? "https://api.supermemory.ai"
@@ -531,6 +532,7 @@ export default function Billing() {
const queryClient = useQueryClient()
const { user, org } = useAuth()
const autumn = useCustomer()
+ const promoCode = usePromoCode()
const posthog = usePostHog()
const isCompanyBrain = useHasCompanyBrain()
const brainTrial = useMemo(
@@ -698,8 +700,10 @@ export default function Billing() {
try {
const result = await autumn.attach({
planId,
+ discounts: promoCode.getDiscounts(),
successUrl: `${window.location.origin}/settings#billing`,
})
+ promoCode.clear()
if ((result as { paymentUrl?: string })?.paymentUrl) {
window.location.href = (result as { paymentUrl: string }).paymentUrl
return
diff --git a/apps/web/components/settings/connections-mcp.tsx b/apps/web/components/settings/connections-mcp.tsx
index ccd098d68..96cd382dd 100644
--- a/apps/web/components/settings/connections-mcp.tsx
+++ b/apps/web/components/settings/connections-mcp.tsx
@@ -38,6 +38,7 @@ import {
getConnectionSubtitle,
} from "@/components/settings/sync-utils"
import type { ImportProvider } from "@/components/settings/sync-utils"
+import { usePromoCode } from "@/hooks/use-promo-code"
type Connection = z.infer
@@ -420,6 +421,7 @@ function FeatureItem({ text }: { text: string }) {
export default function ConnectionsMCP() {
const queryClient = useQueryClient()
const autumn = useCustomer()
+ const promoCode = usePromoCode()
const [addDoc, setAddDoc] = useQueryState("add", addDocumentParam)
const router = useRouter()
const [removeDialog, setRemoveDialog] = useState<{
@@ -552,8 +554,10 @@ export default function ConnectionsMCP() {
try {
const result = await autumn.attach({
planId: "api_pro",
+ discounts: promoCode.getDiscounts(),
successUrl: `${window.location.origin}/settings#connections`,
})
+ promoCode.clear()
if (result?.paymentUrl) {
window.open(result.paymentUrl, "_self")
return
diff --git a/apps/web/hooks/use-promo-code.ts b/apps/web/hooks/use-promo-code.ts
new file mode 100644
index 000000000..1bc8ca1c0
--- /dev/null
+++ b/apps/web/hooks/use-promo-code.ts
@@ -0,0 +1,82 @@
+"use client"
+
+import { useAuth } from "@lib/auth-context"
+import { useRouter } from "next/navigation"
+import { useCallback, useEffect, useMemo } from "react"
+import { toast } from "sonner"
+
+const PENDING_PROMO_CODE_KEY = "sm.promoCode.pending"
+const PROMO_TOAST_ID = "promo-code"
+
+function promoCodeKey(orgId: string): string {
+ return `sm.promoCode.org_${orgId}`
+}
+
+function readOrgPromoCode(orgId?: string): string | null {
+ if (!orgId || typeof window === "undefined") return null
+ return window.localStorage.getItem(promoCodeKey(orgId))
+}
+
+export function usePromoCode() {
+ const { org } = useAuth()
+ const orgId = org?.id
+
+ const getDiscounts = useCallback(() => {
+ const promotionCode = readOrgPromoCode(orgId)
+ return promotionCode ? [{ promotionCode }] : undefined
+ }, [orgId])
+
+ const clear = useCallback(() => {
+ if (!orgId) return
+ window.localStorage.removeItem(promoCodeKey(orgId))
+ toast.dismiss(PROMO_TOAST_ID)
+ }, [orgId])
+
+ return useMemo(() => ({ getDiscounts, clear }), [getDiscounts, clear])
+}
+
+export function PromoCodeCapture() {
+ useEffect(() => {
+ const url = new URL(window.location.href)
+ const code = url.searchParams.get("discountCode")
+ if (!code) return
+
+ window.localStorage.setItem(PENDING_PROMO_CODE_KEY, code)
+ url.searchParams.delete("discountCode")
+ window.history.replaceState({}, "", url.toString())
+ }, [])
+
+ return null
+}
+
+export function PromoCodeHost() {
+ const { org } = useAuth()
+ const router = useRouter()
+
+ useEffect(() => {
+ if (!org?.id) return
+
+ const pending = window.localStorage.getItem(PENDING_PROMO_CODE_KEY)
+ if (pending) {
+ window.localStorage.setItem(promoCodeKey(org.id), pending)
+ window.localStorage.removeItem(PENDING_PROMO_CODE_KEY)
+ }
+
+ const code = readOrgPromoCode(org.id)
+ if (!code) {
+ toast.dismiss(PROMO_TOAST_ID)
+ return
+ }
+ toast.success("Discount code active", {
+ id: PROMO_TOAST_ID,
+ description: `Code ${code} will apply at checkout.`,
+ duration: Number.POSITIVE_INFINITY,
+ action: {
+ label: "Upgrade",
+ onClick: () => router.push("/settings#billing"),
+ },
+ })
+ }, [org?.id, router])
+
+ return null
+}