Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<SettingsModalProvider>
<PromoCodeHost />
<EnsureWorkspace>{children}</EnsureWorkspace>
<PWAInstallPrompt />
</SettingsModalProvider>
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down Expand Up @@ -95,6 +96,7 @@ export default function RootLayout({
includeCredentials={true}
headers={{ "X-App-Source": "nova" }}
>
<PromoCodeCapture />
<QueryProvider>
<AuthProvider>
<PostHogProvider>
Expand Down
4 changes: 4 additions & 0 deletions apps/web/components/add-document/connections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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<ConnectorProvider | null>(null)
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions apps/web/components/add-document/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -153,6 +154,7 @@ export function AddDocument({
})

const autumn = useCustomer()
const promoCode = usePromoCode()
const {
tokensUsed,
searchesUsed,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion apps/web/components/integrations-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ConnectionResponseSchema>

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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
Expand All @@ -2837,7 +2841,7 @@ export function IntegrationsView({
toast.error("Failed to start checkout. Please try again.")
}
},
[autumn],
[autumn, promoCode],
)

const redirectToLogin = useCallback(() => {
Expand Down
4 changes: 4 additions & 0 deletions apps/web/components/integrations/plugins-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string | null>(null)
const [finishSetupPluginId, setFinishSetupPluginId] = useState<string | null>(
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions apps/web/components/onboarding-brain/step-sources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -614,6 +615,7 @@ function OnboardingPlansModal({
requestedPlan: RequiredPlan
}) {
const autumn = useCustomer()
const promoCode = usePromoCode()
const { currentPlan, isLoading } = useTokenUsage(autumn)
const [upgradingPlan, setUpgradingPlan] = useState<CheckoutPlanId | null>(
null,
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions apps/web/components/settings/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions apps/web/components/settings/connections-mcp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ConnectionResponseSchema>

Expand Down Expand Up @@ -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<{
Expand Down Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions apps/web/hooks/use-promo-code.ts
Original file line number Diff line number Diff line change
@@ -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
}
Loading