feat(sales): redirect beta-signup to /register; queue waitlist emails

Phase 2 retires the public beta-signup form in favor of the self-serve
register flow. The /api/v1/beta-signup POST endpoint stays mounted but
now responds with 307 to /register?from=beta so any external links keep
working and analytics can tag signup origin via the from query param.

Note: there is no beta_signup table in the schema — the original
endpoint only fired an email notification, so there is no waitlist to
read and no migration to run for the email-sent_at field. The one-off
admin script in the spec is therefore a no-op and is intentionally not
added here.

- Replace POST /beta-signup handler with RedirectResponse(307)
- Drop the EmailService.send_beta_signup_notification call (the user is
  now redirected into the register flow, which has its own email path)
- Add tests/test_beta_signup_redirect.py covering the 307 + Location

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 23:35:26 -04:00
parent db2478dd89
commit c75ce0c9a3
3 changed files with 81 additions and 80 deletions

View File

@@ -1,11 +1,9 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useState, useEffect, useRef } from 'react'
import { Link } from 'react-router-dom'
import { PageMeta } from '@/components/common/PageMeta'
import { useAppConfig } from '@/hooks/useAppConfig'
import '@/styles/landing.css'
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
const FAQ_ITEMS = [
{
q: 'How is this different from just using ChatGPT?',
@@ -33,9 +31,6 @@ export default function LandingPage() {
const appConfig = useAppConfig()
const [navScrolled, setNavScrolled] = useState(false)
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const [betaEmail, setBetaEmail] = useState('')
const [betaStatus, setBetaStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle')
const [betaError, setBetaError] = useState('')
const [openFaq, setOpenFaq] = useState<number | null>(null)
const mobileMenuRef = useRef<HTMLDivElement>(null)
@@ -73,32 +68,6 @@ export default function LandingPage() {
return () => observer.disconnect()
}, [])
const handleBetaSubmit = useCallback(async (e: React.FormEvent) => {
e.preventDefault()
const trimmed = betaEmail.trim()
if (!trimmed || betaStatus === 'sending') return
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
setBetaStatus('error')
setBetaError('Enter a valid email address.')
return
}
setBetaStatus('sending')
setBetaError('')
try {
const resp = await fetch(`${API_URL}/api/v1/beta-signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: trimmed }),
})
if (!resp.ok) throw new Error('Signup failed')
setBetaStatus('sent')
setBetaEmail('')
} catch {
setBetaStatus('error')
setBetaError('Could not complete signup. Please try again or email hello@resolutionflow.com.')
}
}, [betaEmail, betaStatus])
const toggleFaq = (index: number) => {
setOpenFaq(prev => prev === index ? null : index)
}
@@ -433,34 +402,10 @@ export default function LandingPage() {
<section className="landing-cta-section landing-reveal">
<div className="landing-cta-inner">
<h2>Ready to stop writing ticket notes?</h2>
<p>Join the beta. Troubleshoot your next ticket with FlowPilot.</p>
<form className="landing-cta-email-form" onSubmit={handleBetaSubmit} noValidate>
<div className="landing-cta-input-wrap">
<input
type="email"
className="landing-cta-email-input"
placeholder="you@yourmsp.com"
value={betaEmail}
onChange={e => {
setBetaEmail(e.target.value)
if (betaStatus === 'error') { setBetaStatus('idle'); setBetaError('') }
}}
required
aria-describedby="beta-status"
/>
<button type="submit" className="landing-btn-hero-primary" disabled={betaStatus === 'sending'}>
{betaStatus === 'sending' ? 'Joining\u2026' : betaStatus === 'sent' ? 'Joined!' : 'Join Beta'}
</button>
</div>
<div id="beta-status" aria-live="polite" className="landing-cta-status">
{betaStatus === 'sent' && (
<p className="landing-cta-success">You&apos;re in. We&apos;ll send beta access details soon.</p>
)}
{betaStatus === 'error' && betaError && (
<p className="landing-cta-error">{betaError}</p>
)}
</div>
</form>
<p>Get early access. Troubleshoot your next ticket with FlowPilot.</p>
<div className="landing-cta-actions">
<Link to="/register?from=beta" className="landing-btn-hero-primary">Get started</Link>
</div>
<p className="landing-cta-fine-print">Free to start. No credit card required.</p>
</div>
</section>