Skip to content

Commit 07fe1aa

Browse files
jahoomaclaude
andcommitted
Restore referral landing pages and wire freebuff creator attribution
Brings back the codebuff referral landing routes (/[sponsee], /referrals/[code], /api/referrals/[code]) without free-credit mentions, and ensures freebuff persists ?referrer= through OAuth by saving to localStorage in all sign-in entrypoints and mounting ReferrerTracker in the root layout. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a71a7b9 commit 07fe1aa

7 files changed

Lines changed: 229 additions & 2 deletions

File tree

freebuff/web/src/app/layout.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import '@/styles/globals.css'
33
import type { Metadata } from 'next'
44

55
import { Footer } from '@/components/footer'
6+
import { ReferrerTracker } from '@/components/referrer-tracker'
67
import { ThemeProvider } from '@/components/theme-provider'
78
import { siteConfig } from '@/lib/constant'
89
import { fonts } from '@/lib/fonts'
@@ -55,6 +56,7 @@ export default function RootLayout({
5556
<ThemeProvider attribute="class">
5657
<SessionProvider>
5758
<PostHogProvider>
59+
<ReferrerTracker />
5860
<div className="flex-grow">{children}</div>
5961
<Footer />
6062
</PostHogProvider>

freebuff/web/src/app/onboard/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
import { isAuthCodeExpired, parseAuthCode, validateAuthCode } from './_helpers'
1414
import { authOptions } from '../api/auth/[...nextauth]/auth-options'
1515

16-
import { ReferrerTracker } from '@/components/referrer-tracker'
1716
import {
1817
Card,
1918
CardHeader,
@@ -47,7 +46,6 @@ function StatusCard({
4746
}) {
4847
return (
4948
<main className="container mx-auto flex flex-col items-center py-20">
50-
<ReferrerTracker />
5149
<div className="w-full sm:w-1/2 md:w-2/3">
5250
<Card>
5351
<CardHeader>

freebuff/web/src/components/login/login-card.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,16 @@ export function LoginCard({ authCode }: { authCode?: string | null }) {
1919
const { data: session } = useSession()
2020
const searchParams = useSearchParams() ?? new URLSearchParams()
2121

22+
const persistReferrer = () => {
23+
const referrer = searchParams.get('referrer')
24+
if (referrer) {
25+
localStorage.setItem('freebuff_referrer', referrer)
26+
}
27+
}
28+
2229
const handleContinueAsUser = () => {
30+
persistReferrer()
31+
2332
let callbackUrl = '/'
2433

2534
if (authCode) {
@@ -30,6 +39,8 @@ export function LoginCard({ authCode }: { authCode?: string | null }) {
3039
}
3140

3241
const handleUseAnotherAccount = () => {
42+
persistReferrer()
43+
3344
const searchParamsString = searchParams.toString()
3445

3546
let callbackUrl = '/login'

freebuff/web/src/components/sign-in/sign-in-button.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export function SignInButton({
2626
let callbackUrl =
2727
pathname + (searchParamsString ? `?${searchParamsString}` : '')
2828

29+
const referrer = searchParams.get('referrer')
30+
if (referrer) {
31+
localStorage.setItem('freebuff_referrer', referrer)
32+
}
33+
2934
if (pathname === '/login') {
3035
const authCode = searchParams.get('auth_code')
3136

web/src/app/[sponsee]/page.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
'use server'
2+
3+
import { env } from '@codebuff/common/env'
4+
import db from '@codebuff/internal/db'
5+
import * as schema from '@codebuff/internal/db/schema'
6+
import { eq } from 'drizzle-orm'
7+
import Link from 'next/link'
8+
import { redirect } from 'next/navigation'
9+
10+
import type { Metadata } from 'next'
11+
12+
import CardWithBeams from '@/components/card-with-beams'
13+
14+
export const generateMetadata = async ({
15+
params,
16+
}: {
17+
params: Promise<{ sponsee: string }>
18+
}): Promise<Metadata> => {
19+
const { sponsee } = await params
20+
return {
21+
title: `${sponsee}'s Referral | Codebuff`,
22+
}
23+
}
24+
25+
export default async function SponseePage({
26+
params,
27+
searchParams,
28+
}: {
29+
params: Promise<{ sponsee: string }>
30+
searchParams: Promise<Record<string, string | string[] | undefined>>
31+
}) {
32+
const { sponsee } = await params
33+
const resolvedSearchParams = await searchParams
34+
const sponseeName = sponsee.toLowerCase()
35+
36+
const referralCode = await db
37+
.select({
38+
referralCode: schema.user.referral_code,
39+
})
40+
.from(schema.user)
41+
.where(eq(schema.user.handle, sponseeName))
42+
.limit(1)
43+
.then((result) => result[0]?.referralCode ?? null)
44+
45+
if (!referralCode) {
46+
return (
47+
<CardWithBeams
48+
title="Hmm, that link doesn't look right."
49+
description={`We don't have a referral code for "${sponsee}".`}
50+
content={
51+
<>
52+
<p className="text-center">
53+
Please double-check the link you used or try contacting the person
54+
who shared it.
55+
</p>
56+
<p className="text-center text-sm text-muted-foreground">
57+
You can also reach out to our support team at{' '}
58+
<Link
59+
href={`mailto:${env.NEXT_PUBLIC_SUPPORT_EMAIL}`}
60+
className="underline"
61+
>
62+
{env.NEXT_PUBLIC_SUPPORT_EMAIL}
63+
</Link>
64+
.
65+
</p>
66+
</>
67+
}
68+
/>
69+
)
70+
}
71+
72+
const queryParams = new URLSearchParams()
73+
for (const [key, value] of Object.entries(resolvedSearchParams)) {
74+
if (value !== undefined) {
75+
if (Array.isArray(value)) {
76+
for (const v of value) {
77+
queryParams.append(key, v)
78+
}
79+
} else {
80+
queryParams.set(key, value)
81+
}
82+
}
83+
}
84+
queryParams.set('referrer', sponseeName)
85+
86+
redirect(`/referrals/${referralCode}?${queryParams.toString()}`)
87+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import db from '@codebuff/internal/db'
2+
import * as schema from '@codebuff/internal/db/schema'
3+
import { eq } from 'drizzle-orm'
4+
import { NextResponse } from 'next/server'
5+
6+
export type ReferralCodeResponse = {
7+
referrerName: string | null
8+
}
9+
10+
export async function GET(
11+
_req: Request,
12+
{ params }: { params: Promise<{ code: string }> },
13+
): Promise<NextResponse<ReferralCodeResponse | { error: string }>> {
14+
const { code } = await params
15+
16+
try {
17+
const user = await db.query.user.findFirst({
18+
where: eq(schema.user.referral_code, code),
19+
columns: { name: true },
20+
})
21+
22+
if (!user) {
23+
return NextResponse.json(
24+
{ error: 'Invalid referral code' },
25+
{ status: 400 },
26+
)
27+
}
28+
29+
return NextResponse.json({ referrerName: user.name })
30+
} catch (error) {
31+
console.error(error)
32+
return NextResponse.json(
33+
{ error: 'Internal Server Error' },
34+
{ status: 500 },
35+
)
36+
}
37+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { env } from '@codebuff/common/env'
2+
import { headers } from 'next/headers'
3+
import Link from 'next/link'
4+
5+
import type { ReferralCodeResponse } from '../../api/referrals/[code]/route'
6+
import type { Metadata } from 'next'
7+
8+
import CardWithBeams from '@/components/card-with-beams'
9+
import { Button } from '@/components/ui/button'
10+
import { InstallInstructions } from '@/components/ui/install-instructions'
11+
12+
export const generateMetadata = async ({
13+
searchParams,
14+
}: {
15+
params: Promise<{ code: string }>
16+
searchParams: Promise<{ referrer?: string }>
17+
}): Promise<Metadata> => {
18+
const resolvedSearchParams = await searchParams
19+
const referrerName = resolvedSearchParams.referrer
20+
const title = referrerName
21+
? `${referrerName} invited you to Codebuff!`
22+
: 'You were invited to Codebuff!'
23+
24+
return {
25+
title,
26+
description: 'Install Codebuff and start building with AI in your terminal.',
27+
}
28+
}
29+
30+
export default async function ReferralPage({
31+
params,
32+
searchParams,
33+
}: {
34+
params: Promise<{ code: string }>
35+
searchParams: Promise<{ referrer?: string }>
36+
}) {
37+
const { code } = await params
38+
const resolvedSearchParams = await searchParams
39+
const referrerParam = resolvedSearchParams.referrer
40+
41+
let referrerName: string | null = null
42+
try {
43+
const baseUrl = env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'http://localhost:3000'
44+
const headerList = await headers()
45+
const cookie = headerList.get('Cookie') ?? ''
46+
const response = await fetch(`${baseUrl}/api/referrals/${code}`, {
47+
headers: { Cookie: cookie },
48+
})
49+
50+
if (!response.ok) {
51+
throw new Error('Failed to fetch referral data')
52+
}
53+
54+
const referralData: ReferralCodeResponse = await response.json()
55+
referrerName = referralData.referrerName
56+
} catch {
57+
return (
58+
<CardWithBeams
59+
title="Invalid Referral Link"
60+
description="This referral link is not valid or has expired."
61+
content={
62+
<>
63+
<p className="text-center text-muted-foreground">
64+
Please double-check the link you used or contact the person who
65+
shared it.
66+
</p>
67+
<div className="flex justify-center mt-4">
68+
<Button asChild>
69+
<Link href="/">Go to Homepage</Link>
70+
</Button>
71+
</div>
72+
</>
73+
}
74+
/>
75+
)
76+
}
77+
78+
const displayName = referrerName || referrerParam || 'Someone'
79+
80+
return (
81+
<CardWithBeams
82+
title={`${displayName} invited you to Codebuff!`}
83+
description="Install Codebuff and start building with AI in your terminal."
84+
content={<InstallInstructions />}
85+
/>
86+
)
87+
}

0 commit comments

Comments
 (0)