docs(01): create phase 1 plans - Foundation & Auth

Phase 01: Foundation & Auth
- 6 plans in 4 execution waves
- Wave 1: Project setup (01) + Database schema (02) [parallel]
- Wave 2: Email/password auth (03) + Google OAuth (04) [parallel]
- Wave 3: Middleware & route protection (05)
- Wave 4: Subscription management UI (06)

Requirements covered: AUTH-01, AUTH-02, AUTH-03
Ready for execution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Michele
2026-01-31 03:12:38 +01:00
parent 6a969bccc8
commit bd3e1074a8
6 changed files with 3388 additions and 0 deletions

View File

@@ -0,0 +1,639 @@
---
phase: 01-foundation-auth
plan: 06
type: execute
wave: 4
depends_on: ["01-05"]
files_modified:
- src/app/(dashboard)/subscription/page.tsx
- src/app/actions/subscription.ts
- src/components/subscription/plan-card.tsx
- src/lib/plans.ts
autonomous: true
must_haves:
truths:
- "User can view all available plans (Free, Creator, Pro)"
- "User can see their current plan highlighted"
- "User can switch to a different plan"
- "Plan change updates profile in database"
- "Plan features are displayed clearly"
artifacts:
- path: "src/app/(dashboard)/subscription/page.tsx"
provides: "Subscription management page"
min_lines: 30
- path: "src/app/actions/subscription.ts"
provides: "Server action for plan switching"
exports: ["switchPlan"]
- path: "src/components/subscription/plan-card.tsx"
provides: "Reusable plan display component"
exports: ["PlanCard"]
key_links:
- from: "src/components/subscription/plan-card.tsx"
to: "src/app/actions/subscription.ts"
via: "switchPlan action"
pattern: "switchPlan"
- from: "src/app/actions/subscription.ts"
to: "profiles table"
via: "update plan_id"
pattern: "update.*plan_id"
---
<objective>
Implement subscription management allowing users to view plans and switch between them.
Purpose: Enable users to view and change their subscription plan per AUTH-03 requirement. Payment integration deferred to later phase.
Output: Working subscription page where users can view all plans and switch their plan.
</objective>
<execution_context>
@C:\Users\miche\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\miche\.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/01-foundation-auth/01-RESEARCH.md
@.planning/phases/01-foundation-auth/01-02-SUMMARY.md
@.planning/phases/01-foundation-auth/01-05-SUMMARY.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Create plan utilities and types</name>
<files>
src/lib/plans.ts
src/types/database.ts
</files>
<action>
Create type definitions at src/types/database.ts:
```typescript
export interface Plan {
id: string
name: 'free' | 'creator' | 'pro'
display_name: string
display_name_it: string
price_monthly: number
features: PlanFeatures
created_at: string
}
export interface PlanFeatures {
posts_per_month: number
ai_models: string[]
social_accounts: number
image_generation: boolean
automation: boolean | 'manual' | 'full'
}
export interface Profile {
id: string
tenant_id: string
plan_id: string
email: string
full_name: string | null
avatar_url: string | null
created_at: string
updated_at: string
plans?: Plan
}
```
Create plan utilities at src/lib/plans.ts:
```typescript
import { PlanFeatures } from '@/types/database'
export const PLAN_DISPLAY_ORDER = ['free', 'creator', 'pro'] as const
// Feature display names in Italian
export const FEATURE_LABELS: Record<keyof PlanFeatures, string> = {
posts_per_month: 'Post al mese',
ai_models: 'Modelli AI',
social_accounts: 'Account social',
image_generation: 'Generazione immagini',
automation: 'Automazione',
}
export function formatFeatureValue(
key: keyof PlanFeatures,
value: PlanFeatures[keyof PlanFeatures]
): string {
if (typeof value === 'boolean') {
return value ? 'Incluso' : 'Non incluso'
}
if (Array.isArray(value)) {
return value.length.toString()
}
if (key === 'automation') {
if (value === 'manual') return 'Solo manuale'
if (value === 'full') return 'Completa'
return 'Non inclusa'
}
return value.toString()
}
export function formatPrice(cents: number): string {
if (cents === 0) return 'Gratis'
return `€${(cents / 100).toFixed(0)}/mese`
}
export function getPlanBadgeColor(planName: string): string {
switch (planName) {
case 'pro':
return 'bg-purple-100 text-purple-800 border-purple-200'
case 'creator':
return 'bg-blue-100 text-blue-800 border-blue-200'
default:
return 'bg-gray-100 text-gray-800 border-gray-200'
}
}
```
These utilities:
- Define TypeScript types for plans
- Provide Italian labels for features
- Format prices and feature values
- Handle plan badge colors
</action>
<verify>
- Both files exist
- Types are correctly defined
- Utility functions work
- No TypeScript errors
</verify>
<done>
Plan types and utility functions created.
</done>
</task>
<task type="auto">
<name>Task 2: Create plan card component and switch action</name>
<files>
src/components/subscription/plan-card.tsx
src/app/actions/subscription.ts
</files>
<action>
Create plan card component at src/components/subscription/plan-card.tsx:
```typescript
'use client'
import { Plan, PlanFeatures } from '@/types/database'
import { formatFeatureValue, formatPrice, FEATURE_LABELS, getPlanBadgeColor } from '@/lib/plans'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
import { switchPlan } from '@/app/actions/subscription'
import { useTransition } from 'react'
interface PlanCardProps {
plan: Plan
isCurrentPlan: boolean
onPlanChange?: () => void
}
export function PlanCard({ plan, isCurrentPlan, onPlanChange }: PlanCardProps) {
const [isPending, startTransition] = useTransition()
const features = plan.features as PlanFeatures
function handleSwitchPlan() {
startTransition(async () => {
const result = await switchPlan(plan.id)
if (result.success && onPlanChange) {
onPlanChange()
}
})
}
// Highlight features to show
const displayFeatures: (keyof PlanFeatures)[] = [
'posts_per_month',
'social_accounts',
'ai_models',
'image_generation',
'automation',
]
return (
<Card className={`relative ${isCurrentPlan ? 'ring-2 ring-blue-500' : ''}`}>
{isCurrentPlan && (
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
<span className="bg-blue-500 text-white text-xs font-medium px-3 py-1 rounded-full">
Piano attuale
</span>
</div>
)}
<CardHeader className="text-center pt-8">
<div className="mb-2">
<span className={`inline-block px-3 py-1 text-xs font-medium rounded-full border ${getPlanBadgeColor(plan.name)}`}>
{plan.display_name_it}
</span>
</div>
<CardTitle className="text-3xl">
{formatPrice(plan.price_monthly)}
</CardTitle>
<CardDescription>
{plan.name === 'free' && 'Perfetto per iniziare'}
{plan.name === 'creator' && 'Per creator seri'}
{plan.name === 'pro' && 'Per professionisti'}
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-3">
{displayFeatures.map((featureKey) => {
const value = features[featureKey]
const isIncluded = value !== false && value !== 'non incluso'
return (
<li key={featureKey} className="flex items-center gap-2">
<span className={`w-5 h-5 rounded-full flex items-center justify-center text-xs ${
isIncluded ? 'bg-green-100 text-green-600' : 'bg-gray-100 text-gray-400'
}`}>
{isIncluded ? '✓' : '—'}
</span>
<span className="text-sm">
<span className="font-medium">
{formatFeatureValue(featureKey, value)}
</span>
{' '}
<span className="text-gray-500">
{FEATURE_LABELS[featureKey].toLowerCase()}
</span>
</span>
</li>
)
})}
</ul>
</CardContent>
<CardFooter>
{isCurrentPlan ? (
<Button variant="outline" className="w-full" disabled>
Piano attuale
</Button>
) : (
<Button
className="w-full"
onClick={handleSwitchPlan}
disabled={isPending}
variant={plan.name === 'pro' ? 'default' : 'outline'}
>
{isPending ? 'Cambio in corso...' : (
plan.price_monthly === 0 ? 'Passa a Gratuito' : `Passa a ${plan.display_name_it}`
)}
</Button>
)}
</CardFooter>
</Card>
)
}
```
Create subscription action at src/app/actions/subscription.ts:
```typescript
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
export type SubscriptionActionState = {
success?: boolean
error?: string
message?: string
}
export async function switchPlan(planId: string): Promise<SubscriptionActionState> {
const supabase = await createClient()
// Get current user
const { data: { user }, error: authError } = await supabase.auth.getUser()
if (authError || !user) {
return { error: 'Devi essere autenticato per cambiare piano' }
}
// Verify plan exists
const { data: plan, error: planError } = await supabase
.from('plans')
.select('id, name, display_name_it')
.eq('id', planId)
.single()
if (planError || !plan) {
return { error: 'Piano non trovato' }
}
// Update user's plan
const { error: updateError } = await supabase
.from('profiles')
.update({ plan_id: planId })
.eq('id', user.id)
if (updateError) {
console.error('Failed to update plan:', updateError)
return { error: 'Errore durante il cambio piano. Riprova.' }
}
// Revalidate pages that show plan info
revalidatePath('/dashboard')
revalidatePath('/subscription')
return {
success: true,
message: `Piano cambiato a ${plan.display_name_it}`
}
}
export async function getCurrentPlan() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return null
}
const { data: profile } = await supabase
.from('profiles')
.select(`
plan_id,
plans (
id,
name,
display_name,
display_name_it,
price_monthly,
features
)
`)
.eq('id', user.id)
.single()
return profile?.plans || null
}
```
NOTE: This is a simplified plan switching for v1. In production:
- Payment would be processed before switching to paid plans
- Downgrade might be scheduled for end of billing period
- Proration logic would be needed
- These complexities are deferred per CONTEXT.md
</action>
<verify>
- PlanCard component renders all plan features
- switchPlan action updates database
- Current plan is highlighted
- Non-current plans have switch button
</verify>
<done>
Plan card component and switch action created.
</done>
</task>
<task type="auto">
<name>Task 3: Create subscription page</name>
<files>
src/app/(dashboard)/subscription/page.tsx
</files>
<action>
Create subscription management page at src/app/(dashboard)/subscription/page.tsx:
```typescript
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
import { PlanCard } from '@/components/subscription/plan-card'
import { Plan } from '@/types/database'
import { PLAN_DISPLAY_ORDER } from '@/lib/plans'
export default async function SubscriptionPage() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
redirect('/login')
}
// Get user's current plan
const { data: profile } = await supabase
.from('profiles')
.select('plan_id')
.eq('id', user.id)
.single()
// Get all plans
const { data: plans, error: plansError } = await supabase
.from('plans')
.select('*')
.order('price_monthly', { ascending: true })
if (plansError || !plans) {
return (
<div className="text-center py-12">
<p className="text-red-600">Errore nel caricamento dei piani</p>
</div>
)
}
// Sort plans by our display order
const sortedPlans = [...plans].sort((a, b) => {
return PLAN_DISPLAY_ORDER.indexOf(a.name as typeof PLAN_DISPLAY_ORDER[number]) -
PLAN_DISPLAY_ORDER.indexOf(b.name as typeof PLAN_DISPLAY_ORDER[number])
})
return (
<div className="space-y-8">
<div>
<h1 className="text-2xl font-bold text-gray-900">Il tuo abbonamento</h1>
<p className="text-gray-500 mt-1">
Scegli il piano piu adatto alle tue esigenze
</p>
</div>
{/* Info banner */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>Nota:</strong> Il pagamento verra implementato nelle prossime versioni.
Per ora puoi passare liberamente tra i piani per testare le funzionalita.
</p>
</div>
{/* Plans grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{sortedPlans.map((plan) => (
<PlanCard
key={plan.id}
plan={plan as Plan}
isCurrentPlan={plan.id === profile?.plan_id}
/>
))}
</div>
{/* Feature comparison */}
<div className="mt-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Confronto funzionalita
</h2>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-4 font-medium text-gray-600">
Funzionalita
</th>
{sortedPlans.map((plan) => (
<th key={plan.id} className="text-center py-3 px-4 font-medium text-gray-900">
{plan.display_name_it}
</th>
))}
</tr>
</thead>
<tbody>
<FeatureRow
feature="Post al mese"
plans={sortedPlans}
getValue={(p) => (p.features as Plan['features']).posts_per_month.toString()}
/>
<FeatureRow
feature="Account social"
plans={sortedPlans}
getValue={(p) => (p.features as Plan['features']).social_accounts.toString()}
/>
<FeatureRow
feature="Modelli AI"
plans={sortedPlans}
getValue={(p) => (p.features as Plan['features']).ai_models.length.toString()}
/>
<FeatureRow
feature="Generazione immagini"
plans={sortedPlans}
getValue={(p) => (p.features as Plan['features']).image_generation ? '✓' : '—'}
/>
<FeatureRow
feature="Automazione"
plans={sortedPlans}
getValue={(p) => {
const auto = (p.features as Plan['features']).automation
if (auto === false) return '—'
if (auto === 'manual') return 'Manuale'
if (auto === 'full') return 'Completa'
return '—'
}}
/>
</tbody>
</table>
</div>
</div>
{/* FAQ */}
<div className="mt-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">
Domande frequenti
</h2>
<div className="space-y-4">
<FaqItem
question="Posso cambiare piano in qualsiasi momento?"
answer="Si, puoi passare a un piano superiore o inferiore quando vuoi. Le modifiche sono immediate."
/>
<FaqItem
question="Cosa succede se supero i limiti del mio piano?"
answer="Riceverai un avviso quando ti avvicini al limite mensile. Non potrai creare nuovi post fino al rinnovo o all'upgrade."
/>
<FaqItem
question="Come funziona il pagamento?"
answer="Il sistema di pagamento verra implementato a breve. Per ora tutti i piani sono disponibili gratuitamente per i test."
/>
</div>
</div>
</div>
)
}
function FeatureRow({
feature,
plans,
getValue,
}: {
feature: string
plans: Plan[]
getValue: (plan: Plan) => string
}) {
return (
<tr className="border-b">
<td className="py-3 px-4 text-gray-600">{feature}</td>
{plans.map((plan) => (
<td key={plan.id} className="text-center py-3 px-4">
{getValue(plan)}
</td>
))}
</tr>
)
}
function FaqItem({ question, answer }: { question: string; answer: string }) {
return (
<div className="bg-gray-50 rounded-lg p-4">
<h3 className="font-medium text-gray-900 mb-2">{question}</h3>
<p className="text-sm text-gray-600">{answer}</p>
</div>
)
}
```
This page:
- Shows all three plans in cards
- Highlights current plan
- Allows switching between plans
- Shows feature comparison table
- Includes FAQ section
- Notes that payment is deferred (transparency)
- All text in Italian
</action>
<verify>
- Page loads at /subscription
- All three plans display
- Current plan is highlighted
- Can click to switch plans
- Feature comparison table is accurate
</verify>
<done>
Complete subscription management page with plan display and switching.
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. Visit /subscription when logged in
2. See all three plans (Free, Creator, Pro)
3. Current plan is highlighted with "Piano attuale" badge
4. Click switch button on different plan -> plan changes
5. Dashboard reflects new plan after switch
6. Feature comparison table shows correct values
</verification>
<success_criteria>
- All three plans display with correct pricing
- User can view their current plan
- User can switch to a different plan
- Plan switch updates database (verify in Supabase)
- Feature limits are clearly displayed
- All text is in Italian
- Note about payment deferral is visible
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation-auth/01-06-SUMMARY.md`
</output>