Files
zeekay cb6db9e659 design: every page and component outside the landing and beta paths is at zero lint
Inline style objects became gui props (Box for a div, View or Text rendering the
element for the rest, since Box styles only a div), raw colours and sizes became
the design's tokens, the brand swatches read @hanzo/design's own values, and the
project and creation hues moved to palette.json, which is data. The newsletter's
subscribe button is the standard filled Action. connect-steps.tsx, which nothing
imports, is deleted with the brand constants nobody read.

Pages compared before and after at 1440x900 and 390x844: blockchain, brand,
directory, projects, settings, unsubscribe identical; founder 0.03% of pixels;
buying specialist's title snaps from 28 to the ramp's 26px and the newsletter's
button is now filled.

Claude-Session: https://claude.ai/code/session_017vrVus6PPBgP5YRzKV1iqW
2026-10-26 03:49:42 -07:00

449 lines
19 KiB
TypeScript

'use client'
/**
* /unsubscribe — Email Marketing Opt-Out & Preference Center.
*
* Compliant with CAN-SPAM Act, EU GDPR (Art. 7(3) withdrawal of consent),
* Canadian CASL, and RFC 8058 One-Click Unsubscribe.
*
* Features:
* - Pre-populates email from query string (?email=... or ?to=...).
* - Instant 1-click complete opt-out from all promotional lists.
* - Granular topic preferences for users who wish to customize their email mix.
* - Records opt-out directly to Hanzo CRM (POST https://api.hanzo.ai/v1/marketing/leads).
* - Clear disclosure of physical mailing address (995 Market St, SF CA).
* - Explains transactional email retention for operational security.
* - Full look and feel alignment with hanzo.ai design system.
*/
import React, { Suspense, useEffect, useState } from 'react'
import { useSearchParams } from 'next/navigation'
import { CheckCircle2, MailX, ShieldCheck } from 'lucide-react'
import { Input, Text, View, XStack, YStack } from '@hanzo/ui'
import { HanzoMark } from '@hanzogui/shell'
import { Action, Display, Lede } from '@hanzo/ui/marketing'
import { Dither, Opening, Win } from '@/components/home/chrome'
import { Label } from '@/components/home/label'
const CRM_API = 'https://api.hanzo.ai/v1/marketing/leads'
const PREFERENCES = [
{
id: 'product',
label: 'Product Announcements & Model Releases',
desc: 'New frontier model weights, API capabilities, and engine updates.',
},
{
id: 'developer',
label: 'Developer Dispatch & Engineering News',
desc: 'Deep dives, benchmarks, architecture breakdowns, and MCP tooling.',
},
{
id: 'founder',
label: 'Founder Program & Open Source Bounties',
desc: 'OSS issues, bounty rewards, PR review digests, and Slack invites.',
},
{
id: 'offers',
label: 'Promotional Offers & Cloud Credits',
desc: 'Discount codes, seasonal coupons, and compute credits.',
},
] as const
function UnsubscribeContent() {
const searchParams = useSearchParams()
const rawEmail =
searchParams.get('email') ||
searchParams.get('address') ||
searchParams.get('addr') ||
searchParams.get('e') ||
searchParams.get('to') ||
searchParams.get('recipient') ||
searchParams.get('u') ||
''
const initialEmail = rawEmail ? decodeURIComponent(rawEmail).trim().toLowerCase() : ''
const isOneClick = searchParams.get('oneclick') === '1' || searchParams.get('auto') === '1'
const token = (searchParams.get('token') || searchParams.get('t') || searchParams.get('sig') || '').trim()
const org = (searchParams.get('org') || 'hanzo').trim()
const channel = (searchParams.get('channel') || 'email').trim().toLowerCase()
const [email, setEmail] = useState(initialEmail)
const [unsubscribeAll, setUnsubscribeAll] = useState(true)
const [selectedPrefs, setSelectedPrefs] = useState<Record<string, boolean>>({
product: false,
developer: false,
founder: false,
offers: false,
})
const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'resubscribed' | 'error'>('idle')
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
if (initialEmail && !email) {
setEmail(initialEmail)
}
}, [initialEmail, email])
// Automatic RFC 8058 one-click handling if explicitly flagged
useEffect(() => {
if (isOneClick && initialEmail && status === 'idle') {
performUnsubscribe(initialEmail, true)
}
}, [isOneClick, initialEmail, status])
const performUnsubscribe = async (targetEmail: string, all: boolean) => {
if (!targetEmail || !targetEmail.includes('@')) {
setErrorMessage('Please enter a valid email address.')
setStatus('error')
return
}
setStatus('submitting')
setErrorMessage('')
const normalized = targetEmail.trim().toLowerCase()
try {
await fetch(CRM_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: normalized,
consent: false,
stage: 'unsubscribed',
unsubscribed: true,
unsubscribedAll: all,
preferences: all ? [] : Object.keys(selectedPrefs).filter((k) => selectedPrefs[k]),
source: 'hanzo.ai/unsubscribe',
need: all ? 'Unsubscribed from all marketing communications' : 'Updated email preferences',
}),
})
// If signed token is present, also invoke the RFC 8058 HMAC endpoint
if (token) {
try {
await fetch(
`https://api.hanzo.ai/v1/marketing/unsubscribe?org=${encodeURIComponent(org)}&channel=${encodeURIComponent(
channel
)}&address=${encodeURIComponent(normalized)}&token=${encodeURIComponent(token)}`
)
} catch {
// Unsubscription record is already captured
}
}
setStatus('success')
} catch {
// For privacy & CAN-SPAM compliance, never block or trap opt-outs on network errors
setStatus('success')
}
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await performUnsubscribe(email, unsubscribeAll)
}
const handleResubscribe = async () => {
setStatus('submitting')
try {
await fetch(CRM_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: email.trim().toLowerCase(),
consent: true,
stage: 'subscriber',
unsubscribed: false,
source: 'hanzo.ai/unsubscribe#resubscribe',
need: 'Re-subscribed via preferences page',
}),
})
setStatus('resubscribed')
} catch {
setStatus('resubscribed')
}
}
return (
<View width="100%" minH="100vh" position="relative" overflow="hidden" $platform-web={{ overflowX: 'clip', minHeight: '100vh' }}>
<Opening />
<Dither />
<View
render="main"
position="relative"
width="100%"
maxW={860}
mx="auto"
px="var(--page-gutter)"
pt="max(48px, min(8vh, 88px))"
pb="max(64px, min(10vh, 96px))"
>
<YStack gap="$5" items="center">
<Label center href="/">
HANZO.PREFERENCES
</Label>
<Display text="center" mt={4}>
<span>Email Preferences &amp;</span>{' '}
<Text render="span" color="var(--muted-foreground)">
Opt-Out.
</Text>
</Display>
<Lede text="center" maxW={640} mb={6} $platform-web={{ textWrap: 'balance' }}>
We respect your inbox. Manage your email subscription preferences or unsubscribe from all marketing communications with a single click.
</Lede>
{/* Draggable Telemetry Window for Privacy Transparency */}
<View width="100%" maxW={680} mx="auto" my={4}>
<Win title="Privacy.ConsentCenter" icon={<HanzoMark size={14} />} slot="consent" open>
<YStack p={24} gap={20}>
{status === 'success' ? (
<YStack items="center" justify="center" p={24} gap={16}>
<View p={12} rounded={9999} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)">
<CheckCircle2 size={40} color="var(--state-online)" />
</View>
<Text fontSize="$5" fontWeight="700" color="var(--foreground)" text="center">
You Have Been Unsubscribed
</Text>
<Text fontSize="$2" color="var(--muted-foreground)" maxW={480} lineHeight={22} text="center">
Marketing emails to <Text render="strong" color="var(--foreground)">{email}</Text> have been stopped.
{unsubscribeAll
? ' You have been removed from all promotional and newsletter distribution lists.'
: ' Your updated category preferences have been saved.'}
</Text>
<View p={16} rounded={14} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)" width="100%" maxW={520} mt={4}>
<XStack items="flex-start" gap={10}>
<View mt={2} shrink={0}>
<ShieldCheck size={18} color="var(--muted-foreground)" />
</View>
<YStack gap={2}>
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
Transactional &amp; Account Security Notice
</Text>
<Text fontSize="$1" color="var(--muted-foreground)" lineHeight={18}>
Critical transactional emails (password resets, cloud usage invoices, two-factor authentication codes, and security alerts) are required for service operation and cannot be unsubscribed.
</Text>
</YStack>
</XStack>
</View>
<XStack items="center" gap={16} mt={8} flexWrap="wrap" justify="center">
<Action render="a" href="/" $platform-web={{ textDecoration: 'none' }}>
Return to Homepage →
</Action>
<Action render="button" type="button" onClick={handleResubscribe}>
Unsubscribed by mistake? Re-subscribe
</Action>
</XStack>
</YStack>
) : status === 'resubscribed' ? (
<YStack items="center" justify="center" p={24} gap={16}>
<CheckCircle2 size={40} color="var(--state-online)" />
<Text fontSize="$5" fontWeight="700" color="var(--foreground)" text="center">
Welcome Back!
</Text>
<Text fontSize="$2" color="var(--muted-foreground)" maxW={460} lineHeight={22} text="center">
Your subscription for <strong>{email}</strong> has been restored. You will receive our regular product announcements and engineering dispatches.
</Text>
<Action render="a" href="/" mt={8} $platform-web={{ textDecoration: 'none' }}>
Return to Homepage →
</Action>
</YStack>
) : (
<View
render={<form onSubmit={handleSubmit} />}
display="flex"
flexDirection="column"
gap={20}
>
{/* Email Input */}
<YStack gap={6}>
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
Email Address
</Text>
<Input
value={email}
onChangeText={setEmail}
placeholder="[email protected]"
keyboardType="email-address"
required
rounded={10}
bg="var(--secondary)"
borderColor="var(--border)"
$platform-web={{ borderRadius: '10px' }}
/>
</YStack>
{/* Unsubscribe All Radio */}
<YStack gap={12} pt={4}>
<XStack
items="flex-start"
gap={12}
p={14}
rounded={12}
bg={unsubscribeAll ? 'var(--secondary)' : 'transparent'}
borderWidth={1}
borderColor={unsubscribeAll ? 'var(--border-strong)' : 'var(--border)'}
cursor="pointer"
onClick={() => setUnsubscribeAll(true)}
$platform-web={{ borderRadius: '12px', transition: 'all 0.15s ease' }}
>
<View
render={<input type="radio" name="optout-choice" id="optout-all" checked={unsubscribeAll} onChange={() => setUnsubscribeAll(true)} />}
mt={3}
$platform-web={{ accentColor: "var(--foreground)", display: "inline-block" }}
/>
<YStack gap={2} flex={1}>
<Text
render={<label htmlFor="optout-all" />}
fontSize="$3"
fontWeight="600"
color="var(--foreground)"
cursor="pointer"
>
Unsubscribe from all marketing communications (Recommended)
</Text>
<Text fontSize="$1" color="var(--muted-foreground)" lineHeight={18}>
Opt out of all developer news, product releases, promotions, and newsletter issues.
</Text>
</YStack>
</XStack>
{/* Custom Preferences Radio */}
<XStack
items="flex-start"
gap={12}
p={14}
rounded={12}
bg={!unsubscribeAll ? 'var(--secondary)' : 'transparent'}
borderWidth={1}
borderColor={!unsubscribeAll ? 'var(--border-strong)' : 'var(--border)'}
cursor="pointer"
onClick={() => setUnsubscribeAll(false)}
$platform-web={{ borderRadius: '12px', transition: 'all 0.15s ease' }}
>
<View
render={<input type="radio" name="optout-choice" id="optout-custom" checked={!unsubscribeAll} onChange={() => setUnsubscribeAll(false)} />}
mt={3}
$platform-web={{ accentColor: "var(--foreground)", display: "inline-block" }}
/>
<YStack gap={2} flex={1}>
<Text
render={<label htmlFor="optout-custom" />}
fontSize="$3"
fontWeight="600"
color="var(--foreground)"
cursor="pointer"
>
Customize my email topics
</Text>
<Text fontSize="$1" color="var(--muted-foreground)" lineHeight={18}>
Keep receiving specific topics and turn off others.
</Text>
</YStack>
</XStack>
</YStack>
{/* Sub-Preferences Checklist if Custom Selected */}
{!unsubscribeAll ? (
<YStack gap={10} pl={28} pr={8}>
{PREFERENCES.map((pref) => {
const checked = Boolean(selectedPrefs[pref.id])
return (
<XStack
key={pref.id}
items="flex-start"
gap={10}
p={10}
rounded={10}
bg="var(--secondary)"
borderWidth={1}
borderColor="var(--border)"
>
<View
render={<input type="checkbox" id={`pref-${pref.id}`} checked={checked} onChange={(e) => {
setSelectedPrefs((prev) => ({ ...prev, [pref.id]: e.target.checked }))
}} />}
mt={3}
$platform-web={{ accentColor: "var(--foreground)", display: "inline-block" }}
/>
<YStack gap={2} flex={1}>
<Text
render={<label htmlFor={`pref-${pref.id}`} />}
fontSize="$2"
fontWeight="500"
color="var(--foreground)"
cursor="pointer"
>
{pref.label}
</Text>
<Text fontSize="$1" color="var(--muted-foreground)">
{pref.desc}
</Text>
</YStack>
</XStack>
)
})}
</YStack>
) : null}
{errorMessage ? (
<Text fontSize="$2" color="var(--destructive)">
{errorMessage}
</Text>
) : null}
{/* Action Submit */}
<Action
fill
render="button"
type="submit"
disabled={status === 'submitting' || !email}
$platform-web={{ width: '100%', textAlign: 'center', justifyContent: 'center' }}
>
<XStack items="center" justify="center" gap={8}>
<MailX size={16} />
<span>{status === 'submitting' ? 'Updating Preferences...' : (unsubscribeAll ? 'Unsubscribe from All Emails' : 'Save Email Preferences')}</span>
</XStack>
</Action>
{/* Legal & Compliance Footer */}
<YStack gap={6} pt={10} borderTopWidth={1} borderColor="var(--border)">
<Text fontSize="$1" color="var(--muted-foreground)" lineHeight={18}>
Compliant with the CAN-SPAM Act, CASL, and EU GDPR Art. 7. Unsubscribe requests are processed immediately.
</Text>
<Text fontSize="$1" color="var(--muted-foreground)" lineHeight={18}>
Hanzo AI Inc. · 995 Market Street, San Francisco, CA 94103 · Questions? Contact <Text
render={<a href="mailto:[email protected]" />}
color="var(--foreground)"
textDecorationLine="underline"
>privacy@hanzo.ai</Text>
</Text>
</YStack>
</View>
)}
</YStack>
</Win>
</View>
</YStack>
</View>
</View>
)
}
export default function UnsubscribePage() {
return (
<Suspense fallback={null}>
<UnsubscribeContent />
</Suspense>
)
}