mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
POST /v1/marketing/subscribe answers pending whatever the address and mails a link to /newsletter/confirm#t=<token>; that page spends the token only on a press, so a mail scanner subscribes nobody, and drops the fragment from the address. Every page's footer, /newsletter and /subscribe draw the one form; the footer's chunk loads at low priority. The old form filed /v1/marketing/leads, mailing sales for every subscriber, and said subscribed when the request failed; it is gone, with the unused NewsletterSignup stub whose track was a no-op.
90 lines
3.5 KiB
TypeScript
90 lines
3.5 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState, useSyncExternalStore } from 'react'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
import { Text, YStack } from '@hanzo/ui'
|
|
import { Action } from '@hanzo/ui/marketing'
|
|
import { track } from '@hanzo/app/lib/tags'
|
|
import { BAD } from '@hanzo/app/lib/mix'
|
|
import { Focus, LARGE } from '@/components/shell/focus'
|
|
import { Subscribe } from '@/components/newsletter/Subscribe'
|
|
import { confirm, Refused } from '@/lib/newsletter'
|
|
|
|
/**
|
|
* Where the newsletter's confirmation mail lands: `/newsletter/confirm#t=<token>`.
|
|
* The token rides the fragment, so it reaches neither a server log nor a
|
|
* Referer, and it is spent only by the press below: a mail scanner that opens
|
|
* the link subscribes nobody. Once read, the fragment leaves the address.
|
|
*
|
|
* Events: newsletter_confirmed, newsletter_confirm_refused { status }.
|
|
*/
|
|
type State = { at: 'ready' | 'busy' | 'done' } | { at: 'refused'; status: number; detail: string }
|
|
|
|
/** The link's token, read off the fragment the first time it is asked for and kept: the fragment leaves the address once read. */
|
|
let kept: string | null = null
|
|
const fromHash = () => (kept ??= new URLSearchParams(window.location.hash.slice(1)).get('t') ?? '')
|
|
/** Nothing changes the token while the page is open. */
|
|
const still = () => () => {}
|
|
|
|
export default function ConfirmNewsletter() {
|
|
const { t } = useI18n()
|
|
// Null until the page runs: the export cannot know the token.
|
|
const token = useSyncExternalStore(still, fromHash, () => null)
|
|
const [state, setState] = useState<State>({ at: 'ready' })
|
|
|
|
useEffect(() => {
|
|
if (window.location.hash) history.replaceState(history.state, '', window.location.pathname)
|
|
}, [])
|
|
|
|
const press = async () => {
|
|
setState({ at: 'busy' })
|
|
try {
|
|
await confirm(token ?? '')
|
|
setState({ at: 'done' })
|
|
track('newsletter_confirmed', {})
|
|
} catch (err) {
|
|
const status = err instanceof Refused ? err.status : 0
|
|
track('newsletter_confirm_refused', { status })
|
|
setState({ at: 'refused', status, detail: err instanceof Refused ? err.detail : '' })
|
|
}
|
|
}
|
|
|
|
if (state.at === 'done') {
|
|
return (
|
|
<Focus id="confirm-title" title={t('You are subscribed.')} line={t('The next issue comes to your inbox. Every one has a one-click unsubscribe.')}>
|
|
<Action href="/" fill {...LARGE}>
|
|
{t('Back to hanzo.ai')}
|
|
</Action>
|
|
</Focus>
|
|
)
|
|
}
|
|
|
|
if (token === '' || (state.at === 'refused' && (state.status === 400 || state.status === 410))) {
|
|
const expired = state.at === 'refused' && state.status === 410
|
|
return (
|
|
<Focus
|
|
id="confirm-title"
|
|
title={expired ? t('This link has expired.') : t('This link does not work.')}
|
|
line={t('Enter your address again and we will send a new one.')}
|
|
>
|
|
<Subscribe source="hanzo.ai/newsletter/confirm" id="again" />
|
|
</Focus>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Focus id="confirm-title" title={t('Confirm your subscription')} line={t('One press and the Hanzo newsletter starts coming to your inbox.')}>
|
|
<YStack gap="$3">
|
|
<Action render="button" fill disabled={!token || state.at === 'busy'} onClick={press} {...LARGE}>
|
|
{state.at === 'busy' ? t('Confirming…') : t('Confirm subscription')}
|
|
</Action>
|
|
{state.at === 'refused' ? (
|
|
<Text role="alert" text="center" fontSize="$2" lineHeight="1.25rem" color={BAD}>
|
|
{state.detail || t('We could not reach our mail service. Try again in a minute.')}
|
|
</Text>
|
|
) : null}
|
|
</YStack>
|
|
</Focus>
|
|
)
|
|
}
|