feat: Perfect onboarding journey after Stripe payment
NEW WELCOME PAGE (/command/welcome):
- Celebratory confetti animation on arrival
- Plan-specific welcome message (Trader/Tycoon)
- Features unlocked section with icons
- Next steps with quick links to key features
- Link to documentation and support
UPDATED USER JOURNEY:
1. Pricing Page (/pricing)
↓ Click plan button
2. (If not logged in) → Register → Back to Pricing
↓ Click plan button
3. Stripe Checkout (external)
↓ Payment successful
4. Welcome Page (/command/welcome?plan=trader)
- Shows unlocked features
- Guided next steps
↓ 'Go to Dashboard'
5. Dashboard (/command/dashboard)
CANCEL FLOW:
- Stripe Cancel → /pricing?cancelled=true
- Shows friendly banner: 'No worries! Card not charged.'
- Dismissible with X button
- URL cleaned up automatically
BACKEND UPDATES:
- Default success URL: /command/welcome?plan={plan}
- Default cancel URL: /pricing?cancelled=true
- Portal return URL: /command/settings (not /dashboard)
This creates a complete, professional onboarding experience
that celebrates the upgrade and guides users to get started.
This commit is contained in:
@ -225,7 +225,7 @@ async def create_checkout_session(
|
||||
# Get site URL from environment
|
||||
site_url = os.getenv("SITE_URL", "http://localhost:3000")
|
||||
|
||||
success_url = request.success_url or f"{site_url}/dashboard?upgraded=true"
|
||||
success_url = request.success_url or f"{site_url}/command/welcome?plan={request.plan}"
|
||||
cancel_url = request.cancel_url or f"{site_url}/pricing?cancelled=true"
|
||||
|
||||
try:
|
||||
@ -285,7 +285,7 @@ async def create_portal_session(
|
||||
)
|
||||
|
||||
site_url = os.getenv("SITE_URL", "http://localhost:3000")
|
||||
return_url = f"{site_url}/dashboard"
|
||||
return_url = f"{site_url}/command/settings"
|
||||
|
||||
try:
|
||||
portal_url = await StripeService.create_portal_session(
|
||||
|
||||
221
frontend/src/app/command/welcome/page.tsx
Normal file
221
frontend/src/app/command/welcome/page.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { CommandCenterLayout } from '@/components/CommandCenterLayout'
|
||||
import { PageContainer } from '@/components/PremiumTable'
|
||||
import { useStore } from '@/lib/store'
|
||||
import {
|
||||
CheckCircle,
|
||||
Zap,
|
||||
Crown,
|
||||
ArrowRight,
|
||||
Eye,
|
||||
Store,
|
||||
Bell,
|
||||
BarChart3,
|
||||
Sparkles,
|
||||
TrendingUp,
|
||||
} from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const planDetails = {
|
||||
trader: {
|
||||
name: 'Trader',
|
||||
icon: TrendingUp,
|
||||
color: 'text-accent',
|
||||
bgColor: 'bg-accent/10',
|
||||
features: [
|
||||
{ icon: Eye, text: '50 domains in watchlist', description: 'Track up to 50 domains at once' },
|
||||
{ icon: Zap, text: 'Hourly availability checks', description: '24x faster than Scout' },
|
||||
{ icon: Store, text: '10 For Sale listings', description: 'List your domains on the marketplace' },
|
||||
{ icon: Bell, text: '5 Sniper Alerts', description: 'Get notified when specific domains drop' },
|
||||
{ icon: BarChart3, text: 'Deal scores & valuations', description: 'Know what domains are worth' },
|
||||
],
|
||||
nextSteps: [
|
||||
{ href: '/command/watchlist', label: 'Add domains to watchlist', icon: Eye },
|
||||
{ href: '/command/alerts', label: 'Set up Sniper Alerts', icon: Bell },
|
||||
{ href: '/command/portfolio', label: 'Track your portfolio', icon: BarChart3 },
|
||||
],
|
||||
},
|
||||
tycoon: {
|
||||
name: 'Tycoon',
|
||||
icon: Crown,
|
||||
color: 'text-amber-400',
|
||||
bgColor: 'bg-amber-400/10',
|
||||
features: [
|
||||
{ icon: Eye, text: '500 domains in watchlist', description: 'Massive tracking capacity' },
|
||||
{ icon: Zap, text: 'Real-time checks (10 min)', description: 'Never miss a drop' },
|
||||
{ icon: Store, text: '50 For Sale listings', description: 'Full marketplace access' },
|
||||
{ icon: Bell, text: 'Unlimited Sniper Alerts', description: 'Set as many as you need' },
|
||||
{ icon: Sparkles, text: 'SEO Juice Detector', description: 'Find domains with backlinks' },
|
||||
],
|
||||
nextSteps: [
|
||||
{ href: '/command/watchlist', label: 'Add domains to watchlist', icon: Eye },
|
||||
{ href: '/command/seo', label: 'Analyze SEO metrics', icon: Sparkles },
|
||||
{ href: '/command/alerts', label: 'Create Sniper Alerts', icon: Bell },
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export default function WelcomePage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const { fetchSubscription, checkAuth } = useStore()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [showConfetti, setShowConfetti] = useState(true)
|
||||
|
||||
const planId = searchParams.get('plan') as 'trader' | 'tycoon' | null
|
||||
const plan = planId && planDetails[planId] ? planDetails[planId] : planDetails.trader
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await checkAuth()
|
||||
await fetchSubscription()
|
||||
setLoading(false)
|
||||
}
|
||||
init()
|
||||
|
||||
// Hide confetti after animation
|
||||
const timer = setTimeout(() => setShowConfetti(false), 3000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [checkAuth, fetchSubscription])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<CommandCenterLayout title="Welcome" subtitle="Loading your new plan...">
|
||||
<PageContainer>
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-2 border-accent border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
</PageContainer>
|
||||
</CommandCenterLayout>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CommandCenterLayout title="Welcome" subtitle="Your upgrade is complete">
|
||||
<PageContainer>
|
||||
{/* Confetti Effect */}
|
||||
{showConfetti && (
|
||||
<div className="fixed inset-0 pointer-events-none z-50 overflow-hidden">
|
||||
{Array.from({ length: 50 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-2 h-2 rounded-full animate-[confetti_3s_ease-out_forwards]"
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: '-10px',
|
||||
backgroundColor: ['#10b981', '#f59e0b', '#3b82f6', '#ec4899', '#8b5cf6'][Math.floor(Math.random() * 5)],
|
||||
animationDelay: `${Math.random() * 0.5}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Success Header */}
|
||||
<div className="text-center mb-12">
|
||||
<div className={clsx(
|
||||
"inline-flex items-center justify-center w-20 h-20 rounded-full mb-6",
|
||||
plan.bgColor
|
||||
)}>
|
||||
<CheckCircle className={clsx("w-10 h-10", plan.color)} />
|
||||
</div>
|
||||
|
||||
<h1 className="text-3xl sm:text-4xl font-semibold text-foreground mb-3">
|
||||
Welcome to {plan.name}!
|
||||
</h1>
|
||||
<p className="text-lg text-foreground-muted max-w-lg mx-auto">
|
||||
Your payment was successful. You now have access to all {plan.name} features.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Features Unlocked */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-6 text-center">
|
||||
Features Unlocked
|
||||
</h2>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{plan.features.map((feature, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="p-5 bg-background-secondary/50 border border-border/50 rounded-xl
|
||||
animate-slide-up"
|
||||
style={{ animationDelay: `${i * 100}ms` }}
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={clsx("w-10 h-10 rounded-xl flex items-center justify-center shrink-0", plan.bgColor)}>
|
||||
<feature.icon className={clsx("w-5 h-5", plan.color)} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{feature.text}</p>
|
||||
<p className="text-sm text-foreground-muted mt-1">{feature.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Next Steps */}
|
||||
<div className="mb-12">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-6 text-center">
|
||||
Get Started
|
||||
</h2>
|
||||
<div className="max-w-2xl mx-auto space-y-3">
|
||||
{plan.nextSteps.map((step, i) => (
|
||||
<Link
|
||||
key={i}
|
||||
href={step.href}
|
||||
className="flex items-center justify-between p-5 bg-background-secondary/50 border border-border/50 rounded-xl
|
||||
hover:border-accent/30 hover:bg-background-secondary transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-foreground/5 flex items-center justify-center
|
||||
group-hover:bg-accent/10 transition-colors">
|
||||
<step.icon className="w-5 h-5 text-foreground-muted group-hover:text-accent transition-colors" />
|
||||
</div>
|
||||
<span className="font-medium text-foreground">{step.label}</span>
|
||||
</div>
|
||||
<ArrowRight className="w-5 h-5 text-foreground-subtle group-hover:text-accent transition-colors" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Go to Dashboard */}
|
||||
<div className="text-center">
|
||||
<Link
|
||||
href="/command/dashboard"
|
||||
className="inline-flex items-center gap-2 px-8 py-4 bg-accent text-background font-medium rounded-xl
|
||||
hover:bg-accent-hover transition-all shadow-lg shadow-accent/20"
|
||||
>
|
||||
Go to Dashboard
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
<p className="text-sm text-foreground-muted mt-4">
|
||||
Need help? Check out our <Link href="/docs" className="text-accent hover:underline">documentation</Link> or{' '}
|
||||
<Link href="mailto:support@pounce.ch" className="text-accent hover:underline">contact support</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</PageContainer>
|
||||
|
||||
{/* Custom CSS for confetti animation */}
|
||||
<style jsx>{`
|
||||
@keyframes confetti {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh) rotate(720deg);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</CommandCenterLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import { Header } from '@/components/Header'
|
||||
import { Footer } from '@/components/Footer'
|
||||
import { useStore } from '@/lib/store'
|
||||
import { api } from '@/lib/api'
|
||||
import { Check, ArrowRight, Zap, TrendingUp, Crown, Loader2, Clock, X } from 'lucide-react'
|
||||
import { Check, ArrowRight, Zap, TrendingUp, Crown, Loader2, Clock, X, AlertCircle } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
@ -119,9 +119,20 @@ export default function PricingPage() {
|
||||
const { checkAuth, isLoading, isAuthenticated } = useStore()
|
||||
const [loadingPlan, setLoadingPlan] = useState<string | null>(null)
|
||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(null)
|
||||
const [showCancelledBanner, setShowCancelledBanner] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
checkAuth()
|
||||
|
||||
// Check if user cancelled checkout
|
||||
if (typeof window !== 'undefined') {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
if (params.get('cancelled') === 'true') {
|
||||
setShowCancelledBanner(true)
|
||||
// Clean up URL
|
||||
window.history.replaceState({}, '', '/pricing')
|
||||
}
|
||||
}
|
||||
}, [checkAuth])
|
||||
|
||||
const handleSelectPlan = async (planId: string, isPaid: boolean) => {
|
||||
@ -139,8 +150,8 @@ export default function PricingPage() {
|
||||
try {
|
||||
const response = await api.createCheckoutSession(
|
||||
planId,
|
||||
`${window.location.origin}/dashboard?upgraded=true`,
|
||||
`${window.location.origin}/pricing`
|
||||
`${window.location.origin}/command/welcome?plan=${planId}`,
|
||||
`${window.location.origin}/pricing?cancelled=true`
|
||||
)
|
||||
window.location.href = response.checkout_url
|
||||
} catch (error) {
|
||||
@ -168,6 +179,26 @@ export default function PricingPage() {
|
||||
|
||||
<main className="flex-1 relative pt-32 sm:pt-40 pb-20 sm:pb-28 px-4 sm:px-6">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Cancelled Banner */}
|
||||
{showCancelledBanner && (
|
||||
<div className="mb-8 p-4 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-3 animate-fade-in">
|
||||
<AlertCircle className="w-5 h-5 text-amber-400 shrink-0 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-amber-400">Checkout cancelled</p>
|
||||
<p className="text-sm text-foreground-muted mt-1">
|
||||
No worries! Your card was not charged. You can try again whenever you're ready,
|
||||
or continue with the free Scout plan.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowCancelledBanner(false)}
|
||||
className="p-1 text-foreground-muted hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hero */}
|
||||
<div className="text-center mb-16 sm:mb-20 animate-fade-in">
|
||||
<span className="text-sm font-semibold text-accent uppercase tracking-wider">Pricing</span>
|
||||
|
||||
Reference in New Issue
Block a user