From b46c5f8ef0da631be0bd4458ba38af89a519bda9 Mon Sep 17 00:00:00 2001 From: tizkovatereza Date: Tue, 3 Mar 2026 16:49:33 -0800 Subject: [PATCH 1/2] Add referral source question for new signups Shows a dialog asking "How did you first hear about E2B?" when a new user first lands on the dashboard. Users can type an answer and submit, or skip. Response is saved to Supabase user_metadata and posted to Slack #user-signups. The dialog never appears again once dismissed. Co-Authored-By: Claude Opus 4.6 --- src/app/api/referral-source/route.ts | 38 ++++++ src/features/dashboard/context.tsx | 16 ++- .../dashboard/referral-source-dialog.tsx | 108 ++++++++++++++++++ 3 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/app/api/referral-source/route.ts create mode 100644 src/features/dashboard/referral-source-dialog.tsx diff --git a/src/app/api/referral-source/route.ts b/src/app/api/referral-source/route.ts new file mode 100644 index 000000000..a271d49d3 --- /dev/null +++ b/src/app/api/referral-source/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from 'next/server' + +export async function POST(req: NextRequest) { + const webhookUrl = process.env.SLACK_USER_SIGNUP_WEBHOOK_URL + if (!webhookUrl) { + return NextResponse.json( + { error: 'Slack webhook not configured' }, + { status: 500 } + ) + } + + const { email, source } = await req.json() + + if (!email) { + return NextResponse.json({ error: 'Missing email' }, { status: 400 }) + } + + const message = `:mag: *Referral Source*\n*User:* ${email}\n*Source:* ${source || 'Skipped'}` + + try { + await fetch(webhookUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + channel: 'user-signups', + text: message, + }), + }) + + return NextResponse.json({ ok: true }) + } catch (error) { + console.error('Failed to send Slack message:', error) + return NextResponse.json( + { error: 'Failed to send Slack message' }, + { status: 500 } + ) + } +} diff --git a/src/features/dashboard/context.tsx b/src/features/dashboard/context.tsx index ec8ec4505..6a63853ea 100644 --- a/src/features/dashboard/context.tsx +++ b/src/features/dashboard/context.tsx @@ -1,8 +1,9 @@ 'use client' import type { User } from '@supabase/supabase-js' -import { createContext, type ReactNode, useContext, useState } from 'react' +import { createContext, type ReactNode, useContext, useEffect, useState } from 'react' import type { ClientTeam } from '@/types/dashboard.types' +import { ReferralSourceDialog } from './referral-source-dialog' interface DashboardContextValue { team: ClientTeam @@ -29,6 +30,14 @@ export function DashboardContextProvider({ }: DashboardContextProviderProps) { const [team, setTeam] = useState(initialTeam) const [user, setUser] = useState(initialUser) + const [showReferralDialog, setShowReferralDialog] = useState(false) + + useEffect(() => { + const referralAsked = user?.user_metadata?.referral_asked + if (user && !referralAsked) { + setShowReferralDialog(true) + } + }, [user]) const value = { team, @@ -41,6 +50,11 @@ export function DashboardContextProvider({ return ( {children} + ) } diff --git a/src/features/dashboard/referral-source-dialog.tsx b/src/features/dashboard/referral-source-dialog.tsx new file mode 100644 index 000000000..e9f872074 --- /dev/null +++ b/src/features/dashboard/referral-source-dialog.tsx @@ -0,0 +1,108 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/ui/primitives/button' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/ui/primitives/dialog' +import { Input } from '@/ui/primitives/input' +import { supabase } from '@/lib/clients/supabase/client' + +interface ReferralSourceDialogProps { + userEmail: string + open: boolean + onOpenChange: (open: boolean) => void +} + +export function ReferralSourceDialog({ + userEmail, + open, + onOpenChange, +}: ReferralSourceDialogProps) { + const [source, setSource] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function updateUserMetadata(referralSource: string | null) { + await supabase.auth.updateUser({ + data: { + referral_source: referralSource, + referral_asked: true, + }, + }) + } + + async function notifySlack(referralSource: string | null) { + try { + await fetch('/api/referral-source', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: userEmail, + source: referralSource, + }), + }) + } catch { + // Non-critical, don't block the user + } + } + + async function handleSubmit() { + if (!source.trim()) return + setIsSubmitting(true) + + await Promise.all([ + updateUserMetadata(source.trim()), + notifySlack(source.trim()), + ]) + + onOpenChange(false) + } + + async function handleSkip() { + await updateUserMetadata(null) + onOpenChange(false) + } + + return ( + + + + Welcome! + + How did you first hear about E2B? + + + + setSource(e.target.value)} + placeholder="e.g. X demo video, E2B hackathon, my colleague..." + autoFocus + onKeyDown={(e) => { + if (e.key === 'Enter' && source.trim()) handleSubmit() + }} + /> + + + + + + + + ) +} From a9e8057cf136842d9548000fa396142f45e6d494 Mon Sep 17 00:00:00 2001 From: tizkovatereza Date: Tue, 3 Mar 2026 16:52:51 -0800 Subject: [PATCH 2/2] Fix Biome formatting Co-Authored-By: Claude Opus 4.6 --- src/app/api/referral-source/route.ts | 2 +- src/features/dashboard/context.tsx | 8 +++++++- src/features/dashboard/referral-source-dialog.tsx | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/api/referral-source/route.ts b/src/app/api/referral-source/route.ts index a271d49d3..168585d2d 100644 --- a/src/app/api/referral-source/route.ts +++ b/src/app/api/referral-source/route.ts @@ -1,4 +1,4 @@ -import { NextRequest, NextResponse } from 'next/server' +import { type NextRequest, NextResponse } from 'next/server' export async function POST(req: NextRequest) { const webhookUrl = process.env.SLACK_USER_SIGNUP_WEBHOOK_URL diff --git a/src/features/dashboard/context.tsx b/src/features/dashboard/context.tsx index 6a63853ea..b44da17c1 100644 --- a/src/features/dashboard/context.tsx +++ b/src/features/dashboard/context.tsx @@ -1,7 +1,13 @@ 'use client' import type { User } from '@supabase/supabase-js' -import { createContext, type ReactNode, useContext, useEffect, useState } from 'react' +import { + createContext, + type ReactNode, + useContext, + useEffect, + useState, +} from 'react' import type { ClientTeam } from '@/types/dashboard.types' import { ReferralSourceDialog } from './referral-source-dialog' diff --git a/src/features/dashboard/referral-source-dialog.tsx b/src/features/dashboard/referral-source-dialog.tsx index e9f872074..ff8195cf6 100644 --- a/src/features/dashboard/referral-source-dialog.tsx +++ b/src/features/dashboard/referral-source-dialog.tsx @@ -1,6 +1,7 @@ 'use client' import { useState } from 'react' +import { supabase } from '@/lib/clients/supabase/client' import { Button } from '@/ui/primitives/button' import { Dialog, @@ -11,7 +12,6 @@ import { DialogTitle, } from '@/ui/primitives/dialog' import { Input } from '@/ui/primitives/input' -import { supabase } from '@/lib/clients/supabase/client' interface ReferralSourceDialogProps { userEmail: string