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:
@ -203,7 +203,7 @@ async def google_callback(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Parse redirect from state
|
# Parse redirect from state
|
||||||
redirect_path = "/dashboard"
|
redirect_path = "/command/dashboard"
|
||||||
if ":" in state:
|
if ":" in state:
|
||||||
_, redirect_path = state.split(":", 1)
|
_, redirect_path = state.split(":", 1)
|
||||||
|
|
||||||
@ -312,7 +312,7 @@ async def github_callback(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Parse redirect from state
|
# Parse redirect from state
|
||||||
redirect_path = "/dashboard"
|
redirect_path = "/command/dashboard"
|
||||||
if ":" in state:
|
if ":" in state:
|
||||||
_, redirect_path = state.split(":", 1)
|
_, redirect_path = state.split(":", 1)
|
||||||
|
|
||||||
|
|||||||
@ -63,7 +63,7 @@ export default function DashboardPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get('upgraded') === 'true') {
|
if (searchParams.get('upgraded') === 'true') {
|
||||||
showToast('Welcome to your upgraded plan! 🎉', 'success')
|
showToast('Welcome to your upgraded plan! 🎉', 'success')
|
||||||
window.history.replaceState({}, '', '/dashboard')
|
window.history.replaceState({}, '', '/command/dashboard')
|
||||||
}
|
}
|
||||||
}, [searchParams])
|
}, [searchParams])
|
||||||
|
|
||||||
|
|||||||
@ -54,8 +54,17 @@ function LoginForm() {
|
|||||||
const [oauthProviders, setOauthProviders] = useState({ google_enabled: false, github_enabled: false })
|
const [oauthProviders, setOauthProviders] = useState({ google_enabled: false, github_enabled: false })
|
||||||
const [verified, setVerified] = useState(false)
|
const [verified, setVerified] = useState(false)
|
||||||
|
|
||||||
// Get redirect URL from query params
|
// Get redirect URL from query params or localStorage (set during registration)
|
||||||
const redirectTo = searchParams.get('redirect') || '/command/dashboard'
|
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
|
// Check for verified status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -88,6 +97,9 @@ function LoginForm() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear stored redirect (was set during registration)
|
||||||
|
localStorage.removeItem('pounce_redirect_after_login')
|
||||||
|
|
||||||
// Redirect to intended destination or dashboard
|
// Redirect to intended destination or dashboard
|
||||||
router.push(redirectTo)
|
router.push(redirectTo)
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ function OAuthCallbackContent() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = searchParams.get('token')
|
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 isNew = searchParams.get('new') === 'true'
|
||||||
const error = searchParams.get('error')
|
const error = searchParams.get('error')
|
||||||
|
|
||||||
|
|||||||
@ -768,7 +768,7 @@ export default function HomePage() {
|
|||||||
<ArrowRight className="w-4 h-4" />
|
<ArrowRight className="w-4 h-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<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"
|
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"}
|
{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.
|
Track your first domain in under a minute. Free forever, no credit card.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<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
|
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
|
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)]"
|
shadow-[0_0_40px_rgba(16,185,129,0.2)] hover:shadow-[0_0_60px_rgba(16,185,129,0.3)]"
|
||||||
|
|||||||
@ -395,7 +395,7 @@ export default function PricingPage() {
|
|||||||
Start with Scout. It's free forever. Upgrade when you need more.
|
Start with Scout. It's free forever. Upgrade when you need more.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href={isAuthenticated ? "/dashboard" : "/register"}
|
href={isAuthenticated ? "/command/dashboard" : "/register"}
|
||||||
className="btn-primary inline-flex items-center gap-2 px-6 py-3"
|
className="btn-primary inline-flex items-center gap-2 px-6 py-3"
|
||||||
>
|
>
|
||||||
{isAuthenticated ? "Command Center" : "Join the Hunt"}
|
{isAuthenticated ? "Command Center" : "Join the Hunt"}
|
||||||
|
|||||||
@ -62,7 +62,7 @@ function RegisterForm() {
|
|||||||
const [registered, setRegistered] = useState(false)
|
const [registered, setRegistered] = useState(false)
|
||||||
|
|
||||||
// Get redirect URL from query params
|
// Get redirect URL from query params
|
||||||
const redirectTo = searchParams.get('redirect') || '/dashboard'
|
const redirectTo = searchParams.get('redirect') || '/command/dashboard'
|
||||||
|
|
||||||
// Load OAuth providers
|
// Load OAuth providers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -76,6 +76,13 @@ function RegisterForm() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await register(email, password)
|
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
|
// Show verification message
|
||||||
setRegistered(true)
|
setRegistered(true)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -86,7 +93,7 @@ function RegisterForm() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate login link with redirect preserved
|
// Generate login link with redirect preserved
|
||||||
const loginLink = redirectTo !== '/dashboard'
|
const loginLink = redirectTo !== '/command/dashboard'
|
||||||
? `/login?redirect=${encodeURIComponent(redirectTo)}`
|
? `/login?redirect=${encodeURIComponent(redirectTo)}`
|
||||||
: '/login'
|
: '/login'
|
||||||
|
|
||||||
|
|||||||
@ -286,7 +286,7 @@ function AdminSidebar({
|
|||||||
<div className="border-t border-border/30 py-4 px-3 space-y-2">
|
<div className="border-t border-border/30 py-4 px-3 space-y-2">
|
||||||
{/* Back to User Dashboard */}
|
{/* Back to User Dashboard */}
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard"
|
href="/command/dashboard"
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center gap-3 px-3 py-3 rounded-xl transition-all duration-300",
|
"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"
|
"text-accent hover:bg-accent/10 border border-transparent hover:border-accent/20"
|
||||||
|
|||||||
@ -152,7 +152,7 @@ export function DomainChecker() {
|
|||||||
Grab it now or track it in your watchlist.
|
Grab it now or track it in your watchlist.
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<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
|
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
|
bg-accent text-background text-ui font-medium rounded-lg
|
||||||
hover:bg-accent-hover transition-all duration-300"
|
hover:bg-accent-hover transition-all duration-300"
|
||||||
@ -268,7 +268,7 @@ export function DomainChecker() {
|
|||||||
<span className="text-left">We'll alert you the moment it drops.</span>
|
<span className="text-left">We'll alert you the moment it drops.</span>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<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
|
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
|
bg-background-tertiary text-foreground text-ui font-medium rounded-lg
|
||||||
border border-border hover:border-border-hover transition-all duration-300"
|
border border-border hover:border-border-hover transition-all duration-300"
|
||||||
|
|||||||
@ -51,9 +51,7 @@ export function Header() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're on a Command Center page (should use Sidebar instead)
|
// Check if we're on a Command Center page (should use Sidebar instead)
|
||||||
const isCommandCenterPage = ['/dashboard', '/watchlist', '/portfolio', '/market', '/intelligence', '/settings', '/admin'].some(
|
const isCommandCenterPage = pathname.startsWith('/command') || pathname.startsWith('/admin')
|
||||||
path => pathname.startsWith(path)
|
|
||||||
)
|
|
||||||
|
|
||||||
// If logged in and on Command Center page, don't render this header
|
// If logged in and on Command Center page, don't render this header
|
||||||
if (isAuthenticated && isCommandCenterPage) {
|
if (isAuthenticated && isCommandCenterPage) {
|
||||||
|
|||||||
Reference in New Issue
Block a user