fix: Seamless user journey for register/login/Stripe

PROBLEM: Redirect parameters were getting lost during user flows

FIXES APPLIED:

1. Register Page:
   - Default redirect: /command/dashboard (was /dashboard)
   - Stores redirect in localStorage before email verification
   - Preserves redirect when linking to login page

2. Login Page:
   - Checks localStorage for stored redirect (from registration)
   - Clears stored redirect after successful login
   - Uses useState for dynamic redirect handling

3. OAuth Callback:
   - Default redirect: /command/dashboard (was /dashboard)
   - Backend OAuth endpoints also updated

4. Fixed all /dashboard → /command/dashboard links:
   - pricing/page.tsx
   - page.tsx (landing page)
   - AdminLayout.tsx
   - DomainChecker.tsx
   - command/dashboard/page.tsx
   - Header.tsx (simplified check)

5. Backend OAuth:
   - Default redirect_path: /command/dashboard

NEW USER JOURNEY:

Pricing → Register → Email Verify → Login → Pricing → Stripe
                                                    ↓
                                            Welcome Page
                                                    ↓
                                              Dashboard

The redirect is preserved throughout:
- Query param ?redirect=/pricing passed through register/login
- Stored in localStorage during email verification gap
- Cleaned up after successful login

STRIPE FLOW CLARIFICATION:
- Stripe does NOT create users
- Users must register FIRST with email/password
- Then they can upgrade via Stripe checkout
- This is by design for security and flexibility
This commit is contained in:
yves.gugger
2025-12-10 16:23:16 +01:00
parent bc8d9cc8a3
commit a41e28c420
10 changed files with 34 additions and 17 deletions

View File

@ -203,7 +203,7 @@ async def google_callback(
)
# Parse redirect from state
redirect_path = "/dashboard"
redirect_path = "/command/dashboard"
if ":" in state:
_, redirect_path = state.split(":", 1)
@ -312,7 +312,7 @@ async def github_callback(
)
# Parse redirect from state
redirect_path = "/dashboard"
redirect_path = "/command/dashboard"
if ":" in state:
_, redirect_path = state.split(":", 1)

View File

@ -63,7 +63,7 @@ export default function DashboardPage() {
useEffect(() => {
if (searchParams.get('upgraded') === 'true') {
showToast('Welcome to your upgraded plan! 🎉', 'success')
window.history.replaceState({}, '', '/dashboard')
window.history.replaceState({}, '', '/command/dashboard')
}
}, [searchParams])

View File

@ -54,8 +54,17 @@ function LoginForm() {
const [oauthProviders, setOauthProviders] = useState({ google_enabled: false, github_enabled: false })
const [verified, setVerified] = useState(false)
// Get redirect URL from query params
const redirectTo = searchParams.get('redirect') || '/command/dashboard'
// Get redirect URL from query params or localStorage (set during registration)
const paramRedirect = searchParams.get('redirect')
const [redirectTo, setRedirectTo] = useState(paramRedirect || '/command/dashboard')
// Check localStorage for redirect (set during registration before email verification)
useEffect(() => {
const storedRedirect = localStorage.getItem('pounce_redirect_after_login')
if (storedRedirect && !paramRedirect) {
setRedirectTo(storedRedirect)
}
}, [paramRedirect])
// Check for verified status
useEffect(() => {
@ -88,6 +97,9 @@ function LoginForm() {
return
}
// Clear stored redirect (was set during registration)
localStorage.removeItem('pounce_redirect_after_login')
// Redirect to intended destination or dashboard
router.push(redirectTo)
} catch (err: unknown) {

View File

@ -12,7 +12,7 @@ function OAuthCallbackContent() {
useEffect(() => {
const token = searchParams.get('token')
const redirect = searchParams.get('redirect') || '/dashboard'
const redirect = searchParams.get('redirect') || '/command/dashboard'
const isNew = searchParams.get('new') === 'true'
const error = searchParams.get('error')

View File

@ -768,7 +768,7 @@ export default function HomePage() {
<ArrowRight className="w-4 h-4" />
</Link>
<Link
href={isAuthenticated ? "/dashboard" : "/register"}
href={isAuthenticated ? "/command/dashboard" : "/register"}
className="inline-flex items-center gap-2 px-8 py-4 text-foreground-muted hover:text-foreground transition-colors"
>
{isAuthenticated ? "Go to Dashboard" : "Start Free"}
@ -789,7 +789,7 @@ export default function HomePage() {
Track your first domain in under a minute. Free forever, no credit card.
</p>
<Link
href={isAuthenticated ? "/dashboard" : "/register"}
href={isAuthenticated ? "/command/dashboard" : "/register"}
className="group inline-flex items-center gap-3 px-10 py-5 bg-accent text-background rounded-2xl
text-lg font-semibold hover:bg-accent-hover transition-all duration-300
shadow-[0_0_40px_rgba(16,185,129,0.2)] hover:shadow-[0_0_60px_rgba(16,185,129,0.3)]"

View File

@ -395,7 +395,7 @@ export default function PricingPage() {
Start with Scout. It&apos;s free forever. Upgrade when you need more.
</p>
<Link
href={isAuthenticated ? "/dashboard" : "/register"}
href={isAuthenticated ? "/command/dashboard" : "/register"}
className="btn-primary inline-flex items-center gap-2 px-6 py-3"
>
{isAuthenticated ? "Command Center" : "Join the Hunt"}

View File

@ -62,7 +62,7 @@ function RegisterForm() {
const [registered, setRegistered] = useState(false)
// Get redirect URL from query params
const redirectTo = searchParams.get('redirect') || '/dashboard'
const redirectTo = searchParams.get('redirect') || '/command/dashboard'
// Load OAuth providers
useEffect(() => {
@ -76,6 +76,13 @@ function RegisterForm() {
try {
await register(email, password)
// Store redirect URL for after email verification
// This will be picked up by the login page after verification
if (redirectTo !== '/command/dashboard') {
localStorage.setItem('pounce_redirect_after_login', redirectTo)
}
// Show verification message
setRegistered(true)
} catch (err) {
@ -86,7 +93,7 @@ function RegisterForm() {
}
// Generate login link with redirect preserved
const loginLink = redirectTo !== '/dashboard'
const loginLink = redirectTo !== '/command/dashboard'
? `/login?redirect=${encodeURIComponent(redirectTo)}`
: '/login'

View File

@ -286,7 +286,7 @@ function AdminSidebar({
<div className="border-t border-border/30 py-4 px-3 space-y-2">
{/* Back to User Dashboard */}
<Link
href="/dashboard"
href="/command/dashboard"
className={clsx(
"flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
"text-accent hover:bg-accent/10 border border-transparent hover:border-accent/20"

View File

@ -152,7 +152,7 @@ export function DomainChecker() {
Grab it now or track it in your watchlist.
</p>
<Link
href={isAuthenticated ? '/dashboard' : '/register'}
href={isAuthenticated ? '/command/dashboard' : '/register'}
className="shrink-0 flex items-center justify-center sm:justify-start gap-2 px-5 py-2.5
bg-accent text-background text-ui font-medium rounded-lg
hover:bg-accent-hover transition-all duration-300"
@ -268,7 +268,7 @@ export function DomainChecker() {
<span className="text-left">We&apos;ll alert you the moment it drops.</span>
</div>
<Link
href={isAuthenticated ? '/dashboard' : '/register'}
href={isAuthenticated ? '/command/dashboard' : '/register'}
className="shrink-0 flex items-center justify-center sm:justify-start gap-2 px-4 py-2.5
bg-background-tertiary text-foreground text-ui font-medium rounded-lg
border border-border hover:border-border-hover transition-all duration-300"

View File

@ -51,9 +51,7 @@ export function Header() {
}
// Check if we're on a Command Center page (should use Sidebar instead)
const isCommandCenterPage = ['/dashboard', '/watchlist', '/portfolio', '/market', '/intelligence', '/settings', '/admin'].some(
path => pathname.startsWith(path)
)
const isCommandCenterPage = pathname.startsWith('/command') || pathname.startsWith('/admin')
// If logged in and on Command Center page, don't render this header
if (isAuthenticated && isCommandCenterPage) {