Add discovery modes, personalization controls, taste profile page, updated pricing
- Discovery modes: Sonic Twin, Era Bridge, Deep Cuts, Rising Artists - Discovery dial (Safe to Adventurous slider) - Block genres/moods exclusion - Thumbs down/dislike on recommendations - My Taste page with Genre DNA breakdown, audio feature meters, listening personality - Updated pricing: Free (5/week), Premium ($6.99/mo), Family ($12.99/mo coming soon) - Weekly rate limiting instead of daily - Alembic migration for new fields
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Crown, Check, Loader2, ExternalLink, Sparkles, Music, Infinity, Download } from 'lucide-react'
|
||||
import { Crown, Check, Loader2, ExternalLink, Sparkles, Music, Infinity, Download, Users, Fingerprint, X } from 'lucide-react'
|
||||
import { useAuth } from '../lib/auth'
|
||||
import { createCheckout, createBillingPortal, getBillingStatus } from '../lib/api'
|
||||
|
||||
@@ -10,11 +10,36 @@ interface BillingInfo {
|
||||
current_period_end: number | null
|
||||
}
|
||||
|
||||
const proFeatures = [
|
||||
{ icon: Infinity, text: 'Unlimited recommendations per day' },
|
||||
{ icon: Music, text: 'Unlimited playlist imports' },
|
||||
{ icon: Sparkles, text: 'Advanced taste analysis' },
|
||||
{ icon: Download, text: 'Export playlists to any platform' },
|
||||
interface TierFeature {
|
||||
text: string
|
||||
included: boolean
|
||||
}
|
||||
|
||||
const freeTierFeatures: TierFeature[] = [
|
||||
{ text: '1 platform sync', included: true },
|
||||
{ text: '5 discoveries per week', included: true },
|
||||
{ text: 'Basic taste profile', included: true },
|
||||
{ text: 'All platforms', included: false },
|
||||
{ text: 'Unlimited discovery', included: false },
|
||||
{ text: 'Full AI insights', included: false },
|
||||
{ text: 'Export playlists', included: false },
|
||||
]
|
||||
|
||||
const premiumTierFeatures: TierFeature[] = [
|
||||
{ text: 'All platform syncs', included: true },
|
||||
{ text: 'Unlimited discovery', included: true },
|
||||
{ text: 'Full taste DNA profile', included: true },
|
||||
{ text: 'Full AI insights & explanations', included: true },
|
||||
{ text: 'Export to any platform', included: true },
|
||||
{ text: 'All discovery modes', included: true },
|
||||
{ text: 'Priority recommendations', included: true },
|
||||
]
|
||||
|
||||
const familyTierFeatures: TierFeature[] = [
|
||||
{ text: 'Everything in Premium', included: true },
|
||||
{ text: 'Up to 5 profiles', included: true },
|
||||
{ text: 'Family taste overlap feature', included: true },
|
||||
{ text: 'Shared discovery feed', included: true },
|
||||
]
|
||||
|
||||
export default function Billing() {
|
||||
@@ -72,15 +97,15 @@ export default function Billing() {
|
||||
const isPro = billing?.is_pro || false
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-charcoal">Billing</h1>
|
||||
<p className="text-charcoal-muted mt-1">Manage your subscription</p>
|
||||
<h1 className="text-3xl font-bold text-charcoal">Plans & Pricing</h1>
|
||||
<p className="text-charcoal-muted mt-1">Choose the plan that fits your music discovery journey</p>
|
||||
</div>
|
||||
|
||||
{success && (
|
||||
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-xl">
|
||||
<p className="text-green-800 font-medium">Welcome to Vynl Pro! Your subscription is now active.</p>
|
||||
<p className="text-green-800 font-medium">Welcome to Vynl Premium! Your subscription is now active.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -90,90 +115,165 @@ export default function Billing() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current Plan */}
|
||||
<div className="bg-white rounded-2xl border border-purple-100 overflow-hidden">
|
||||
<div className="p-6 border-b border-purple-50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${isPro ? 'bg-purple' : 'bg-purple-50'}`}>
|
||||
<Crown className={`w-5 h-5 ${isPro ? 'text-white' : 'text-purple'}`} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-charcoal">
|
||||
{isPro ? 'Vynl Pro' : 'Free Plan'}
|
||||
</h2>
|
||||
<p className="text-sm text-charcoal-muted">
|
||||
{isPro ? '$4.99/month' : '10 recommendations/day, 1 playlist'}
|
||||
</p>
|
||||
</div>
|
||||
{/* Active subscription banner */}
|
||||
{isPro && billing?.subscription_status && (
|
||||
<div className="mb-6 p-4 bg-purple-50 border border-purple-200 rounded-xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-purple flex items-center justify-center">
|
||||
<Crown className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
{isPro && billing?.subscription_status && (
|
||||
<span className="px-3 py-1 bg-green-50 text-green-700 text-sm font-medium rounded-full">
|
||||
{billing.subscription_status === 'active' ? 'Active' : billing.subscription_status}
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-charcoal">Vynl Premium Active</p>
|
||||
{billing.current_period_end && (
|
||||
<p className="text-xs text-charcoal-muted">
|
||||
Next billing: {new Date(billing.current_period_end * 1000).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleManage}
|
||||
disabled={portalLoading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-white border border-purple-200 text-purple text-sm font-medium rounded-xl hover:bg-purple-50 transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{portalLoading ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Manage
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pricing Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||
{/* Free Tier */}
|
||||
<div className="bg-white rounded-2xl border border-purple-100 overflow-hidden flex flex-col">
|
||||
<div className="p-6 border-b border-purple-50">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Music className="w-5 h-5 text-charcoal-muted" />
|
||||
<h3 className="text-lg font-semibold text-charcoal">Free</h3>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-bold text-charcoal">$0</span>
|
||||
<span className="text-sm text-charcoal-muted">/month</span>
|
||||
</div>
|
||||
<p className="text-sm text-charcoal-muted mt-2">
|
||||
Get started with basic music discovery
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 flex-1">
|
||||
<ul className="space-y-3">
|
||||
{freeTierFeatures.map((f) => (
|
||||
<li key={f.text} className="flex items-start gap-2.5">
|
||||
{f.included ? (
|
||||
<Check className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
|
||||
) : (
|
||||
<X className="w-4 h-4 text-charcoal-muted/30 mt-0.5 flex-shrink-0" />
|
||||
)}
|
||||
<span className={`text-sm ${f.included ? 'text-charcoal' : 'text-charcoal-muted/50'}`}>
|
||||
{f.text}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-6 pt-0">
|
||||
<div className="w-full py-3 bg-cream text-charcoal-muted font-medium rounded-xl text-sm text-center">
|
||||
{isPro ? 'Previous plan' : 'Current plan'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Premium Tier — Recommended */}
|
||||
<div className="bg-white rounded-2xl border-2 border-purple shadow-lg shadow-purple/10 overflow-hidden flex flex-col relative">
|
||||
<div className="absolute top-0 left-0 right-0 bg-purple text-white text-xs font-semibold text-center py-1.5 uppercase tracking-wider">
|
||||
Recommended
|
||||
</div>
|
||||
<div className="p-6 border-b border-purple-50 pt-10">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Sparkles className="w-5 h-5 text-purple" />
|
||||
<h3 className="text-lg font-semibold text-charcoal">Premium</h3>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-bold text-charcoal">$6.99</span>
|
||||
<span className="text-sm text-charcoal-muted">/month</span>
|
||||
</div>
|
||||
<p className="text-sm text-charcoal-muted mt-2">
|
||||
Unlock the full power of AI music discovery
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 flex-1">
|
||||
<ul className="space-y-3">
|
||||
{premiumTierFeatures.map((f) => (
|
||||
<li key={f.text} className="flex items-start gap-2.5">
|
||||
<Check className="w-4 h-4 text-purple mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-charcoal">{f.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-6 pt-0">
|
||||
{isPro ? (
|
||||
<div className="w-full py-3 bg-purple/10 text-purple font-semibold rounded-xl text-sm text-center flex items-center justify-center gap-2">
|
||||
<Check className="w-4 h-4" />
|
||||
Your current plan
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleUpgrade}
|
||||
disabled={checkoutLoading}
|
||||
className="w-full flex items-center justify-center gap-2 py-3 bg-purple text-white font-semibold rounded-xl hover:bg-purple-dark transition-colors cursor-pointer disabled:opacity-50 border-none text-sm"
|
||||
>
|
||||
{checkoutLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Crown className="w-4 h-4" />
|
||||
Upgrade to Premium
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{isPro && billing?.current_period_end && (
|
||||
<p className="text-sm text-charcoal-muted mt-3">
|
||||
Next billing date: {new Date(billing.current_period_end * 1000).toLocaleDateString()}
|
||||
</div>
|
||||
|
||||
{/* Family Tier — Coming Soon */}
|
||||
<div className="bg-white rounded-2xl border border-purple-100 overflow-hidden flex flex-col opacity-80">
|
||||
<div className="p-6 border-b border-purple-50">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Users className="w-5 h-5 text-charcoal-muted" />
|
||||
<h3 className="text-lg font-semibold text-charcoal">Family</h3>
|
||||
<span className="ml-auto px-2 py-0.5 bg-amber-50 text-amber-700 text-xs font-medium rounded-full">
|
||||
Coming Soon
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-3xl font-bold text-charcoal">$12.99</span>
|
||||
<span className="text-sm text-charcoal-muted">/month</span>
|
||||
</div>
|
||||
<p className="text-sm text-charcoal-muted mt-2">
|
||||
Share discovery with up to 5 family members
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pro Features */}
|
||||
<div className="p-6">
|
||||
<h3 className="text-sm font-semibold text-charcoal uppercase tracking-wider mb-4">
|
||||
{isPro ? 'Your Pro features' : 'Upgrade to Pro'}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{proFeatures.map((feature) => {
|
||||
const Icon = feature.icon
|
||||
return (
|
||||
<div key={feature.text} className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-lg bg-purple-50 flex items-center justify-center flex-shrink-0">
|
||||
<Icon className="w-4 h-4 text-purple" />
|
||||
</div>
|
||||
<span className="text-sm text-charcoal">{feature.text}</span>
|
||||
{isPro && <Check className="w-4 h-4 text-green-500 ml-auto" />}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action */}
|
||||
<div className="p-6 bg-purple-50/50 border-t border-purple-100">
|
||||
{isPro ? (
|
||||
<button
|
||||
onClick={handleManage}
|
||||
disabled={portalLoading}
|
||||
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-white border border-purple-200 text-purple font-semibold rounded-xl hover:bg-purple-50 transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{portalLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Manage Subscription
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleUpgrade}
|
||||
disabled={checkoutLoading}
|
||||
className="w-full flex items-center justify-center gap-2 px-6 py-3 bg-purple text-white font-semibold rounded-xl hover:bg-purple-dark transition-colors cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
{checkoutLoading ? (
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Crown className="w-4 h-4" />
|
||||
Upgrade to Pro — $4.99/mo
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div className="p-6 flex-1">
|
||||
<ul className="space-y-3">
|
||||
{familyTierFeatures.map((f) => (
|
||||
<li key={f.text} className="flex items-start gap-2.5">
|
||||
<Check className="w-4 h-4 text-charcoal-muted/50 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-sm text-charcoal-muted">{f.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-6 pt-0">
|
||||
<div className="w-full py-3 bg-cream text-charcoal-muted font-medium rounded-xl text-sm text-center">
|
||||
Coming soon
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user