Files
hanzo.ai/components/referrals/ReferralLink.tsx
zeekay 3a8a35d13c beta: the portal is the app's Betas pane, after a plan step
/beta's portal is one component now. A stranger has it in the site's frame;
a member has the app in its Betas mode (@hanzo/app 0.2.27: no sidebar there)
with the same portal as its pane: the form if there is no application, a plan
step once after filing (Pro with the beta code, or stay on Free), then the
dashboard.

Every share link, on the dashboard, the referral card, /referral and the page
share widget, goes out through tagged(): the sharer's ref and the channel as
utm_source, so a visit, a lead and a signup each say who shared and where. The
page share widget's tracking was a no-op; it records waitlist_shared now.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-25 18:38:26 -07:00

479 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useEffect, useState } from 'react'
import {
Check,
Copy,
Edit3,
Gift,
Mail,
MessageCircle,
Share2,
Video,
Zap,
} from 'lucide-react'
import {
Box,
Button,
Input,
Label,
Text,
View,
XStack,
YStack,
toast,
} from '@hanzo/ui'
import { useAnalytics } from '@hanzo/event/react'
import { EVENTS } from '@hanzo/event'
import { VisuallyHidden } from '@hanzo/gui'
import { Line } from '@/components/type'
import { PANEL } from '@/components/referrals/panel'
import { updateCode, tagged } from '@/lib/hanzo/referrals'
import { scope } from '@hanzo/app/lib/session'
interface ReferralLinkProps {
referralLink: string
referralCode: string
}
const ReferralLink = ({ referralLink: initialLink, referralCode: initialCode }: ReferralLinkProps) => {
const [activeCode, setActiveCode] = useState(initialCode)
const [activeLink, setActiveLink] = useState(initialLink)
const [isCustomizing, setIsCustomizing] = useState(false)
const [customSlugInput, setCustomSlugInput] = useState(initialCode)
const [savingSlug, setSavingSlug] = useState(false)
const [emailInput, setEmailInput] = useState('')
const [copied, setCopied] = useState(false)
const analytics = useAnalytics()
useEffect(() => {
setActiveCode(initialCode)
setActiveLink(initialLink)
setCustomSlugInput(initialCode)
}, [initialCode, initialLink])
if (!scope().Authorization || !activeCode || !activeLink) {
return null
}
// The link as shared on one channel (lib/hanzo/referrals `tagged`).
const via = (channel: string) => tagged(activeLink, channel)
const shareMessage =
'Building with frontier AI models on @hanzoai: autonomous Dev coding agent, multimodal Studio, and Zen frontier reasoning. Join the beta with my link:'
const handleShare = () => {
navigator.clipboard.writeText(via('copy'))
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'link', refCode: activeCode })
setCopied(true)
toast.success('Referral link copied to clipboard!')
window.setTimeout(() => setCopied(false), 2400)
}
const handleNativeShare = async () => {
if (typeof navigator.share === 'function') {
try {
await navigator.share({
title: 'Hanzo AI',
text: shareMessage,
url: via('native'),
})
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'native_share', refCode: activeCode })
} catch {
// cancelled
}
} else {
handleShare()
}
}
const handleSaveCustomSlug = async (e: React.FormEvent) => {
e.preventDefault()
const clean = customSlugInput.trim().replace(/[^a-zA-Z0-9_-]/g, '')
if (clean.length < 2 || clean.length > 32) {
toast.error('Custom link must be between 2 and 32 characters (letters, numbers, hyphens)')
return
}
setSavingSlug(true)
try {
const updated = await updateCode(clean)
const savedCode = updated.code || clean.toUpperCase()
const newUrl = updated.link || `https://hanzo.ai/?ref=${savedCode}`
setActiveCode(savedCode)
setActiveLink(newUrl)
setCustomSlugInput(savedCode)
setIsCustomizing(false)
toast.success(`Custom link active: hanzo.ai/?ref=${savedCode}`)
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Could not save custom link. Please try again.')
} finally {
setSavingSlug(false)
}
}
const handleResetToDefault = () => {
setIsCustomizing(false)
setCustomSlugInput(activeCode)
}
const handleSendInvites = (e: React.FormEvent) => {
e.preventDefault()
const emails = emailInput
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (emails.length === 0) {
toast.error('Please enter at least one email address')
return
}
const subject = 'Join me on Hanzo'
const body = `Hey,\n\nI am using Hanzo AI for autonomous coding (Hanzo Dev), multimodal generation, and frontier reasoning models. Sign up with my referral link to get early access and free tier credits:\n\n${via('email')}\n\nCheers!`
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'email', count: emails.length, refCode: activeCode })
window.location.href = `mailto:${emails.map(encodeURIComponent).join(',')}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`
}
const handleTikTokShare = async () => {
const tiktokSnippet = `Building on @hanzoinc: autonomous AI coding & frontier reasoning. Free access link in bio: ${via('tiktok')} #techtok #coding #ai #HanzoAI #developer`
try {
await navigator.clipboard.writeText(tiktokSnippet)
toast.success('Copied TikTok bio/caption snippet! Paste into your TikTok bio or video caption.')
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'tiktok', refCode: activeCode })
window.open('https://www.tiktok.com/@hanzoinc', '_blank')
} catch {
handleShare()
}
}
const handleYouTubeShare = async () => {
const ytSnippet = `Try Hanzo AI (@HanzoAI) for autonomous coding and frontier reasoning models: ${via('youtube')}\n\n#AI #Coding #SoftwareEngineering #Hanzo`
try {
await navigator.clipboard.writeText(ytSnippet)
toast.success('Copied YouTube description snippet with your link!')
analytics.capture(EVENTS.WAITLIST_SHARED, { method: 'youtube', refCode: activeCode })
window.open('https://youtube.com/@HanzoAI', '_blank')
} catch {
handleShare()
}
}
const socialChannels = [
{
name: '𝕏 @hanzoai',
channel: 'x',
icon: <Text fontWeight="700">𝕏</Text>,
href: `https://x.com/intent/post?text=${encodeURIComponent(shareMessage)}&url=${encodeURIComponent(via('x'))}`,
action: null,
},
{
name: 'LinkedIn',
channel: 'linkedin',
icon: <Text fontWeight="700">in</Text>,
href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(via('linkedin'))}`,
action: null,
},
{
name: 'Facebook',
channel: 'facebook',
icon: <Text fontWeight="700">f</Text>,
href: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(via('facebook'))}`,
action: null,
},
{
name: 'Bluesky',
channel: 'bluesky',
icon: <Text fontWeight="700">🦋</Text>,
href: `https://bsky.app/intent/compose?text=${encodeURIComponent(`${shareMessage} @hanzoai.bsky.social ${via('bluesky')}`)}`,
action: null,
},
{
name: 'Reddit r/HanzoAI',
channel: 'reddit',
icon: <Text fontWeight="700">r/</Text>,
href: `https://www.reddit.com/r/HanzoAI/submit?url=${encodeURIComponent(via('reddit'))}&title=${encodeURIComponent('Testing Hanzo AI frontier models and autonomous Dev coding agent')}`,
action: null,
},
{
name: 'TikTok @hanzoinc',
channel: 'tiktok',
icon: <Video size={16} aria-hidden />,
href: 'https://www.tiktok.com/@hanzoinc',
action: handleTikTokShare,
},
{
name: 'YouTube @HanzoAI',
channel: 'youtube',
icon: <Text fontWeight="700">▶</Text>,
href: 'https://youtube.com/@HanzoAI',
action: handleYouTubeShare,
},
{
name: 'Instagram @hanzoinc',
channel: 'instagram',
icon: <Text fontWeight="700">IG</Text>,
href: 'https://www.instagram.com/hanzoinc',
action: null,
},
{
name: 'Discord',
channel: 'discord',
icon: <MessageCircle size={15} aria-hidden />,
href: 'https://discord.gg/XthHQQj',
action: null,
},
{
name: 'Telegram',
channel: 'telegram',
icon: <MessageCircle size={15} aria-hidden />,
href: `https://t.me/share/url?url=${encodeURIComponent(via('telegram'))}&text=${encodeURIComponent(shareMessage)}`,
action: null,
},
{
name: 'WhatsApp',
channel: 'whatsapp',
icon: <Share2 size={15} aria-hidden />,
href: `https://api.whatsapp.com/send?text=${encodeURIComponent(`${shareMessage} ${via('whatsapp')}`)}`,
action: null,
},
]
return (
<YStack {...PANEL} gap={24}>
{/* Real-time Kickback Badges */}
<View
display="grid"
gap={12}
gridTemplateColumns="1fr"
$md={{ gridTemplateColumns: 'repeat(2, minmax(0, 1fr))' }}
>
<XStack items="center" gap={12} p={14} rounded={12} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)">
<Zap size={20} color="#ffd700" style={{ flexShrink: 0 }} aria-hidden />
<YStack gap={2}>
<Text fontSize="$2" fontWeight="700" color="var(--foreground)">
10% Tokens Back (Lifetime Kickback)
</Text>
<Text fontSize="$1" color="var(--muted-foreground)">
Get 10% of all tokens consumed by people you refer credited back to your balance automatically as they build.
</Text>
</YStack>
</XStack>
<XStack items="center" gap={12} p={14} rounded={12} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)">
<Gift size={20} color="var(--foreground)" style={{ flexShrink: 0 }} aria-hidden />
<YStack gap={2}>
<Text fontSize="$2" fontWeight="700" color="var(--foreground)">
1 Free Week Per Paid Plan
</Text>
<Text fontSize="$1" color="var(--muted-foreground)">
Every time someone you refer upgrades to a paid plan, unlock 1 full free week of Hanzo Pro or Enterprise.
</Text>
</YStack>
</XStack>
</View>
{/* 1. Primary Referral Link & Custom Slug Switcher */}
<YStack gap={16}>
<XStack items="center" justify="space-between" flexWrap="wrap" gap={8}>
<Line render="h2" size="xl" weight="500">
Your Personal Referral Link
</Line>
<XStack items="center" gap={8}>
<Text fontFamily="$mono" fontSize="$1" color="var(--muted-foreground)">
Code: {activeCode}
</Text>
<Button
size="sm"
variant="outline"
onClick={() => {
setCustomSlugInput(activeCode)
setIsCustomizing(!isCustomizing)
}}
>
<Edit3 size={13} style={{ marginRight: 4 }} aria-hidden />
{isCustomizing ? 'Close' : 'Customize Link'}
</Button>
</XStack>
</XStack>
{isCustomizing && (
<form onSubmit={handleSaveCustomSlug}>
<YStack gap={10} p={16} rounded={12} bg="var(--neutral-900)" borderWidth={1} borderColor="var(--neutral-700)">
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
Create Your Custom Vanity Referral Link
</Text>
<Text fontSize="$1" color="var(--muted-foreground)">
Set a memorable username, handle, or brand slug (e.g. yourname, techlead, devmemes).
</Text>
<XStack items="center" gap={8} flexWrap="wrap">
<Text fontFamily="$mono" fontSize="$2" color="var(--muted-foreground)">
https://hanzo.ai/?ref=
</Text>
<Box grow={1} minW={140} maxW={280}>
<Input
placeholder="custom-handle"
value={customSlugInput}
onChange={(e) => setCustomSlugInput(e.target.value)}
bg="var(--neutral-950)"
borderColor="var(--neutral-700)"
/>
</Box>
<Button type="submit" disabled={savingSlug}>
{savingSlug ? 'Saving…' : 'Save Custom Link'}
</Button>
{activeCode !== initialCode && (
<Button type="button" variant="outline" onClick={handleResetToDefault}>
Reset
</Button>
)}
</XStack>
</YStack>
</form>
)}
<YStack gap={12} $md={{ flexDirection: 'row' }}>
<Box position="relative" grow={1} shrink={1} flexBasis="0%">
<Input
value={activeLink}
readOnly
pr={48}
bg="var(--neutral-900)"
borderColor="var(--neutral-700)"
/>
</Box>
<Button gap={8} onClick={handleShare}>
{copied ? <Check width={16} height={16} /> : <Copy width={16} height={16} />}
{copied ? 'Copied Link!' : 'Copy Link'}
</Button>
<Button variant="outline" gap={8} onClick={handleNativeShare}>
<Share2 width={16} height={16} />
Share
</Button>
</YStack>
</YStack>
{/* 2. 1-Click Multi-Platform Social Sharing Buttons */}
<YStack gap={12} pt={20} borderTopWidth={1} borderColor="var(--neutral-800)">
<YStack gap={4}>
<Line render="h3" size="lg" weight="500">
Share Across All Platforms & Socials
</Line>
<Line size="sm" color="var(--muted-foreground)">
Post your custom link on TikTok, 𝕏, Reddit, YouTube, or LinkedIn to earn 10% token kickbacks and cash bounties.
</Line>
</YStack>
<View
display="grid"
gap={10}
gridTemplateColumns="repeat(2, minmax(0, 1fr))"
$sm={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
$md={{ gridTemplateColumns: 'repeat(4, minmax(0, 1fr))' }}
>
{socialChannels.map((item) => {
if (item.action) {
return (
<XStack
key={item.channel}
render={<button type="button" onClick={item.action} />}
items="center"
justify="center"
gap={8}
height={44}
rounded={12}
borderWidth={1}
borderColor="var(--neutral-700)"
bg="var(--neutral-900)"
cursor="pointer"
hoverStyle={{ borderColor: 'var(--foreground)', bg: 'var(--neutral-800)' }}
$platform-web={{ border: 'none', transition: 'all 140ms' }}
>
{item.icon}
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
{item.name}
</Text>
</XStack>
)
}
return (
<XStack
key={item.channel}
render={
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
onClick={() =>
analytics.capture(EVENTS.WAITLIST_SHARED, {
method: item.channel,
refCode: activeCode,
})
}
/>
}
items="center"
justify="center"
gap={8}
height={44}
rounded={12}
borderWidth={1}
borderColor="var(--neutral-700)"
bg="var(--neutral-900)"
cursor="pointer"
hoverStyle={{ borderColor: 'var(--foreground)', bg: 'var(--neutral-800)' }}
$platform-web={{ textDecoration: 'none', color: 'var(--foreground)', transition: 'all 140ms' }}
>
{item.icon}
<Text fontSize="$2" fontWeight="500" color="var(--foreground)">
{item.name}
</Text>
</XStack>
)
})}
</View>
</YStack>
{/* 3. Direct Email Invite */}
<Box pt={20} borderTopWidth={1} borderColor="var(--neutral-800)">
<YStack gap={16}>
<YStack gap={4}>
<Line render="h3" size="lg" weight="500">
Invite by Email
</Line>
<Line size="sm" color="var(--muted-foreground)">
Opens your email client with your referral link pre-written.
</Line>
</YStack>
<form onSubmit={handleSendInvites}>
<YStack gap={16} $md={{ flexDirection: 'row' }}>
<Box grow={1} shrink={1} flexBasis="0%">
<VisuallyHidden>
<Label htmlFor="emails">Email Addresses</Label>
</VisuallyHidden>
<Input
id="emails"
placeholder="Enter email addresses, separated by commas"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
bg="var(--neutral-900)"
borderColor="var(--neutral-700)"
/>
</Box>
<Button type="submit" gap={8}>
<Mail width={16} height={16} />
Write Invite
</Button>
</YStack>
</form>
</YStack>
</Box>
</YStack>
)
}
export default ReferralLink