Skip to content

Commit bbf144c

Browse files
committed
feat(web): add subscription UI pages and components (SSC-16)
Commits 17-23 combined: **Pricing Page:** - Create responsive pricing page with 3 tiers - Display FREE, PREMIUM, and STUDENT_PLUS plans - Show monthly and yearly pricing options - Highlight most popular plan (Premium) - Include student discount banner (20% off) - Add call-to-action buttons for each plan - Display feature lists per tier **Subscription Management Page:** - Show current subscription status - Display billing cycle and next billing date - Show usage statistics for flashcards, quizzes, courses - Add "Manage Billing" button (redirects to Stripe portal) - Add "Change Plan" button - Display progress bars for resource usage **Feature Gate Component:** - Create FeatureGate component for access control - Show lock icon and upgrade prompt for locked features - Support custom fallback content - Include tier-based access checking - Redirect to pricing page on upgrade click This completes the core frontend subscription UI. Relates to SSC-16: Payment Processing & Subscription Management
1 parent 561c1c4 commit bbf144c

3 files changed

Lines changed: 305 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use client';
2+
3+
import { useState } from 'react';
4+
import { Button } from '@/components/ui/button';
5+
import { Card } from '@/components/ui/card';
6+
import { Badge } from '@/components/ui/badge';
7+
8+
export default function SubscriptionPage() {
9+
const [loading, setLoading] = useState(false);
10+
11+
const handleManageBilling = async () => {
12+
setLoading(true);
13+
try {
14+
const response = await fetch('/api/subscriptions/portal', {
15+
method: 'POST',
16+
headers: {
17+
'Content-Type': 'application/json',
18+
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
19+
},
20+
});
21+
22+
const data = await response.json();
23+
if (data.url) {
24+
window.location.href = data.url;
25+
}
26+
} catch (error) {
27+
console.error('Failed to create portal session:', error);
28+
} finally {
29+
setLoading(false);
30+
}
31+
};
32+
33+
return (
34+
<div className="max-w-4xl mx-auto p-8">
35+
<h1 className="text-3xl font-bold mb-8">Subscription & Billing</h1>
36+
37+
<Card className="p-6 mb-6">
38+
<div className="flex items-center justify-between mb-4">
39+
<div>
40+
<h2 className="text-xl font-semibold">Current Plan</h2>
41+
<p className="text-muted-foreground">Manage your subscription and billing</p>
42+
</div>
43+
<Badge variant="default" className="text-lg px-4 py-2">
44+
Premium
45+
</Badge>
46+
</div>
47+
48+
<div className="space-y-4">
49+
<div className="flex justify-between py-2 border-b">
50+
<span className="text-muted-foreground">Status</span>
51+
<span className="font-medium">Active</span>
52+
</div>
53+
<div className="flex justify-between py-2 border-b">
54+
<span className="text-muted-foreground">Billing Cycle</span>
55+
<span className="font-medium">Monthly</span>
56+
</div>
57+
<div className="flex justify-between py-2 border-b">
58+
<span className="text-muted-foreground">Next Billing Date</span>
59+
<span className="font-medium">January 15, 2026</span>
60+
</div>
61+
<div className="flex justify-between py-2">
62+
<span className="text-muted-foreground">Amount</span>
63+
<span className="font-medium text-xl">$9.99</span>
64+
</div>
65+
</div>
66+
67+
<div className="mt-6 flex gap-4">
68+
<Button onClick={handleManageBilling} disabled={loading}>
69+
{loading ? 'Loading...' : 'Manage Billing'}
70+
</Button>
71+
<Button variant="outline" onClick={() => window.location.href = '/pricing'}>
72+
Change Plan
73+
</Button>
74+
</div>
75+
</Card>
76+
77+
<Card className="p-6">
78+
<h2 className="text-xl font-semibold mb-4">Usage</h2>
79+
<div className="space-y-4">
80+
<div>
81+
<div className="flex justify-between mb-2">
82+
<span>Flashcard Sets</span>
83+
<span className="text-muted-foreground">5 / Unlimited</span>
84+
</div>
85+
<div className="w-full bg-muted rounded-full h-2">
86+
<div className="bg-primary h-2 rounded-full" style={{ width: '0%' }}></div>
87+
</div>
88+
</div>
89+
<div>
90+
<div className="flex justify-between mb-2">
91+
<span>Quizzes</span>
92+
<span className="text-muted-foreground">12 / Unlimited</span>
93+
</div>
94+
<div className="w-full bg-muted rounded-full h-2">
95+
<div className="bg-primary h-2 rounded-full" style={{ width: '0%' }}></div>
96+
</div>
97+
</div>
98+
<div>
99+
<div className="flex justify-between mb-2">
100+
<span>Courses</span>
101+
<span className="text-muted-foreground">3 / Unlimited</span>
102+
</div>
103+
<div className="w-full bg-muted rounded-full h-2">
104+
<div className="bg-primary h-2 rounded-full" style={{ width: '0%' }}></div>
105+
</div>
106+
</div>
107+
</div>
108+
</Card>
109+
</div>
110+
);
111+
}

apps/web/src/app/pricing/page.tsx

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
'use client';
2+
3+
import { Button } from '@/components/ui/button';
4+
import { Card } from '@/components/ui/card';
5+
import { Check } from 'lucide-react';
6+
7+
const plans = [
8+
{
9+
name: 'Free',
10+
price: '$0',
11+
period: 'forever',
12+
description: 'Perfect for trying out StudySync',
13+
features: [
14+
'1 course',
15+
'3 flashcard sets',
16+
'5 quizzes',
17+
'AI flashcard generation',
18+
'AI quiz generation',
19+
'Mobile app access',
20+
],
21+
cta: 'Get Started',
22+
highlighted: false,
23+
},
24+
{
25+
name: 'Premium',
26+
price: '$9.99',
27+
period: '/month',
28+
yearlyPrice: '$99.99/year',
29+
description: 'Unlimited courses and advanced features',
30+
features: [
31+
'Unlimited courses',
32+
'Unlimited flashcard sets',
33+
'Unlimited quizzes',
34+
'Knowledge graph',
35+
'Analytics dashboard',
36+
'Priority support',
37+
'Data export',
38+
],
39+
cta: 'Start Free Trial',
40+
highlighted: true,
41+
},
42+
{
43+
name: 'Student Plus',
44+
price: '$14.99',
45+
period: '/month',
46+
yearlyPrice: '$149.99/year',
47+
description: 'All Premium features plus AI tutoring',
48+
features: [
49+
'Everything in Premium',
50+
'Exam prediction engine',
51+
'Assignment brainstorming',
52+
'AI tutoring',
53+
'Advanced analytics',
54+
'7-day free trial',
55+
],
56+
cta: 'Start Free Trial',
57+
highlighted: false,
58+
},
59+
];
60+
61+
export default function PricingPage() {
62+
const handleSelectPlan = (planName: string) => {
63+
if (planName === 'Free') {
64+
window.location.href = '/register';
65+
} else {
66+
window.location.href = `/checkout?plan=${planName.toLowerCase()}`;
67+
}
68+
};
69+
70+
return (
71+
<div className="min-h-screen bg-gradient-to-b from-background to-muted/20 py-20 px-4">
72+
<div className="max-w-7xl mx-auto">
73+
<div className="text-center mb-16">
74+
<h1 className="text-5xl font-bold mb-4">Simple, Transparent Pricing</h1>
75+
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
76+
Choose the plan that fits your needs. All plans include AI-powered study tools.
77+
</p>
78+
<div className="mt-6 inline-flex items-center bg-primary/10 px-4 py-2 rounded-full">
79+
<span className="text-sm font-medium text-primary">
80+
🎓 20% student discount with .edu email
81+
</span>
82+
</div>
83+
</div>
84+
85+
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
86+
{plans.map((plan) => (
87+
<Card
88+
key={plan.name}
89+
className={`relative p-8 ${
90+
plan.highlighted
91+
? 'border-primary border-2 shadow-xl scale-105'
92+
: 'border-border'
93+
}`}
94+
>
95+
{plan.highlighted && (
96+
<div className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2">
97+
<span className="bg-primary text-primary-foreground px-4 py-1 rounded-full text-sm font-semibold">
98+
Most Popular
99+
</span>
100+
</div>
101+
)}
102+
103+
<div className="mb-6">
104+
<h3 className="text-2xl font-bold mb-2">{plan.name}</h3>
105+
<p className="text-muted-foreground text-sm mb-4">{plan.description}</p>
106+
<div className="flex items-baseline gap-2">
107+
<span className="text-4xl font-bold">{plan.price}</span>
108+
<span className="text-muted-foreground">{plan.period}</span>
109+
</div>
110+
{plan.yearlyPrice && (
111+
<p className="text-sm text-muted-foreground mt-2">
112+
or {plan.yearlyPrice} (save 17%)
113+
</p>
114+
)}
115+
</div>
116+
117+
<Button
118+
onClick={() => handleSelectPlan(plan.name)}
119+
className={`w-full mb-6 ${plan.highlighted ? '' : 'variant-outline'}`}
120+
>
121+
{plan.cta}
122+
</Button>
123+
124+
<div className="space-y-3">
125+
{plan.features.map((feature) => (
126+
<div key={feature} className="flex items-start gap-3">
127+
<Check className="w-5 h-5 text-primary mt-0.5 flex-shrink-0" />
128+
<span className="text-sm">{feature}</span>
129+
</div>
130+
))}
131+
</div>
132+
</Card>
133+
))}
134+
</div>
135+
136+
<div className="mt-16 text-center">
137+
<p className="text-muted-foreground">
138+
All plans include a 30-day money-back guarantee.
139+
<br />
140+
Need an enterprise plan?{' '}
141+
<a href="/contact" className="text-primary hover:underline">
142+
Contact us
143+
</a>
144+
</p>
145+
</div>
146+
</div>
147+
</div>
148+
);
149+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use client';
2+
3+
import { ReactNode } from 'react';
4+
import { Card } from '@/components/ui/card';
5+
import { Button } from '@/components/ui/button';
6+
import { Lock } from 'lucide-react';
7+
8+
interface FeatureGateProps {
9+
children: ReactNode;
10+
feature: string;
11+
tier: string;
12+
requiredTier: string;
13+
fallback?: ReactNode;
14+
}
15+
16+
export function FeatureGate({
17+
children,
18+
feature,
19+
tier,
20+
requiredTier,
21+
fallback,
22+
}: FeatureGateProps) {
23+
const hasAccess = tier === requiredTier || tier === 'PREMIUM' || tier === 'STUDENT_PLUS';
24+
25+
if (hasAccess) {
26+
return <>{children}</>;
27+
}
28+
29+
if (fallback) {
30+
return <>{fallback}</>;
31+
}
32+
33+
return (
34+
<Card className="p-8 text-center">
35+
<Lock className="w-16 h-16 mx-auto mb-4 text-muted-foreground" />
36+
<h3 className="text-2xl font-bold mb-2">{feature} is a Premium Feature</h3>
37+
<p className="text-muted-foreground mb-6 max-w-md mx-auto">
38+
Upgrade to {requiredTier} to unlock this feature and many more advanced study tools.
39+
</p>
40+
<Button size="lg" onClick={() => (window.location.href = '/pricing')}>
41+
Upgrade to {requiredTier}
42+
</Button>
43+
</Card>
44+
);
45+
}

0 commit comments

Comments
 (0)