diff --git a/backend/app/api/endpoints/beta_signup.py b/backend/app/api/endpoints/beta_signup.py index d4b2c7bc..b0eee7d9 100644 --- a/backend/app/api/endpoints/beta_signup.py +++ b/backend/app/api/endpoints/beta_signup.py @@ -1,31 +1,44 @@ -"""Public beta signup endpoint — no auth required.""" +"""Legacy beta signup endpoint — redirects to /register?from=beta. + +Phase 2 (self-serve signup) makes the public register flow the canonical +front door. The old `/api/v1/beta-signup` POST endpoint is kept mounted to +preserve any external links that still hit it, but now responds with a +307 Temporary Redirect to `/register?from=beta` so the user lands in the +real signup flow. The `?from=beta` marker lets the frontend tag the +signup origin for analytics. + +Note: there is no `beta_signup` database table — the original endpoint +only fired a notification email. There is therefore no waitlist to email +and no migration to run when retiring the endpoint. +""" import logging -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel, EmailStr -from app.core.email import EmailService + +from fastapi import APIRouter +from fastapi.responses import RedirectResponse + +from app.core.config import settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/beta-signup", tags=["beta"]) - -class BetaSignupRequest(BaseModel): - email: EmailStr +# Local-dev fallback when FRONTEND_URL isn't configured. The redirect must +# be absolute — a relative URL would resolve against the API origin +# (api.resolutionflow.com), which has no /register page. +_DEFAULT_FRONTEND_URL = "http://localhost:5173" -class BetaSignupResponse(BaseModel): - success: bool - message: str +@router.post("", include_in_schema=False) +async def beta_signup_redirect() -> RedirectResponse: + """Redirect legacy beta-signup POST to the public register page. - -@router.post("", response_model=BetaSignupResponse) -async def beta_signup(data: BetaSignupRequest): - """Collect beta interest — sends notification to beta@resolutionflow.com.""" - sent = await EmailService.send_beta_signup_notification(data.email) - if not sent: - logger.warning("Beta signup recorded (email delivery skipped): %s", data.email) - return BetaSignupResponse( - success=True, - message="Thanks! We'll be in touch with beta access details.", + Returns 307 so any client following the redirect preserves the HTTP + method; the frontend treats `/register?from=beta` as the canonical + entry point and reads the `from` query param for analytics. + """ + frontend_url = settings.FRONTEND_URL or _DEFAULT_FRONTEND_URL + return RedirectResponse( + url=f"{frontend_url}/register?from=beta", + status_code=307, ) diff --git a/backend/tests/test_beta_signup_redirect.py b/backend/tests/test_beta_signup_redirect.py new file mode 100644 index 00000000..542235ed --- /dev/null +++ b/backend/tests/test_beta_signup_redirect.py @@ -0,0 +1,43 @@ +"""Integration tests for the legacy /beta-signup redirect. + +Phase 2 retires the public beta-signup form in favor of the regular +register flow. The endpoint stays mounted but answers with a 307 to +the absolute frontend `/register?from=beta` URL so any external links +keep working. There is no `beta_signup` table to migrate — the old +endpoint only fired an email notification — so this test only covers +the redirect contract. +""" + +import pytest + +from app.core.config import settings + + +@pytest.mark.asyncio +async def test_beta_signup_redirects_to_register(client, monkeypatch): + """POST /beta-signup returns 307 to the absolute frontend register URL.""" + monkeypatch.setattr(settings, "FRONTEND_URL", "https://example.com") + + response = await client.post( + "/api/v1/beta-signup", + json={"email": "anyone@example.com"}, + ) + + assert response.status_code == 307, response.text + assert ( + response.headers["location"] + == "https://example.com/register?from=beta" + ) + + +@pytest.mark.asyncio +async def test_beta_signup_redirect_ignores_body(client, monkeypatch): + """Redirect fires regardless of payload — no validation on the legacy route.""" + monkeypatch.setattr(settings, "FRONTEND_URL", "https://example.com") + + response = await client.post("/api/v1/beta-signup", json={}) + assert response.status_code == 307 + assert ( + response.headers["location"] + == "https://example.com/register?from=beta" + ) diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 0989f1d8..c440d591 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -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(null) const mobileMenuRef = useRef(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() {

Ready to stop writing ticket notes?

-

Join the beta. Troubleshoot your next ticket with FlowPilot.

-
-
- { - setBetaEmail(e.target.value) - if (betaStatus === 'error') { setBetaStatus('idle'); setBetaError('') } - }} - required - aria-describedby="beta-status" - /> - -
-
- {betaStatus === 'sent' && ( -

You're in. We'll send beta access details soon.

- )} - {betaStatus === 'error' && betaError && ( -

{betaError}

- )} -
-
+

Get early access. Troubleshoot your next ticket with FlowPilot.

+
+ Get started +

Free to start. No credit card required.