better sub
This commit is contained in:
parent
c5c69645c5
commit
2df51e501c
185
docs/pricing-page-improvements.md
Normal file
185
docs/pricing-page-improvements.md
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
# 定价页面改进说明
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
定价页面现在实现了智能的套餐过滤和按钮显示逻辑,确保只显示有效的可订阅套餐,并提供清晰的用户体验。
|
||||||
|
|
||||||
|
## 主要改进
|
||||||
|
|
||||||
|
### 1. 智能套餐过滤
|
||||||
|
|
||||||
|
定价页面现在只显示以下套餐:
|
||||||
|
|
||||||
|
1. **免费套餐** - 总是显示
|
||||||
|
2. **用户当前套餐** - 总是显示(即使没有 stripePriceId)
|
||||||
|
3. **有效的付费套餐** - 必须有 `stripePriceId` 才显示
|
||||||
|
|
||||||
|
#### 过滤逻辑
|
||||||
|
```typescript
|
||||||
|
const filteredPlans = plans.filter((plan: SubscriptionPlan) => {
|
||||||
|
// 免费套餐总是显示
|
||||||
|
if (isPlanFree(plan)) return true
|
||||||
|
|
||||||
|
// 用户当前套餐总是显示
|
||||||
|
if (userData && isCurrentPlan(plan.id)) return true
|
||||||
|
|
||||||
|
// 其他套餐必须有 stripePriceId 才能显示(可订阅)
|
||||||
|
return plan.stripePriceId && plan.stripePriceId.trim() !== ''
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 智能按钮显示
|
||||||
|
|
||||||
|
按钮显示遵循以下逻辑:
|
||||||
|
|
||||||
|
#### 免费套餐
|
||||||
|
- **当前套餐**: 显示 "当前方案" 按钮(禁用)
|
||||||
|
- **非当前套餐**: 不显示任何按钮
|
||||||
|
|
||||||
|
#### 付费套餐
|
||||||
|
- **当前套餐**: 显示 "当前方案" 按钮(禁用)
|
||||||
|
- **可订阅套餐**: 显示 "立即订阅" 按钮
|
||||||
|
- **无价格ID套餐**: 不显示按钮(理论上不会出现,因为已过滤)
|
||||||
|
|
||||||
|
#### 按钮逻辑代码
|
||||||
|
```typescript
|
||||||
|
{(() => {
|
||||||
|
// 免费套餐逻辑
|
||||||
|
if (isFree) {
|
||||||
|
if (isCurrent) {
|
||||||
|
return <Button variant="outline" disabled>{t('currentPlan')}</Button>
|
||||||
|
}
|
||||||
|
return null // 免费套餐且非当前套餐,不显示按钮
|
||||||
|
}
|
||||||
|
|
||||||
|
// 付费套餐逻辑
|
||||||
|
if (isCurrent) {
|
||||||
|
return <Button variant="outline" disabled>{t('currentPlan')}</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可订阅的付费套餐
|
||||||
|
if (plan.stripePriceId && plan.stripePriceId.trim() !== '') {
|
||||||
|
return (
|
||||||
|
<SubscribeButton priceId={plan.stripePriceId} planName={plan.displayName}>
|
||||||
|
{t('subscribeNow')}
|
||||||
|
</SubscribeButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
})()}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 多语言支持
|
||||||
|
|
||||||
|
添加了新的翻译键:
|
||||||
|
|
||||||
|
#### 英文 (en.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pricing": {
|
||||||
|
"subscribeNow": "Subscribe Now",
|
||||||
|
"currentPlan": "Current Plan"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 中文 (zh.json)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pricing": {
|
||||||
|
"subscribeNow": "立即订阅",
|
||||||
|
"currentPlan": "当前方案"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 用户体验场景
|
||||||
|
|
||||||
|
### 1. 匿名用户
|
||||||
|
- 看到:免费套餐(无按钮)+ 有价格ID的付费套餐(立即订阅按钮)
|
||||||
|
- 不看到:没有价格ID的付费套餐
|
||||||
|
|
||||||
|
### 2. 免费用户
|
||||||
|
- 看到:免费套餐(当前方案按钮)+ 有价格ID的付费套餐(立即订阅按钮)
|
||||||
|
- 不看到:没有价格ID的付费套餐
|
||||||
|
|
||||||
|
### 3. Pro 用户
|
||||||
|
- 看到:免费套餐(无按钮)+ 当前Pro套餐(当前方案按钮)+ 其他有价格ID的套餐(立即订阅按钮)
|
||||||
|
- 不看到:没有价格ID的付费套餐
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 1. 动态数据获取
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPlans()
|
||||||
|
}, [userData]) // 依赖 userData,确保用户数据加载后再过滤套餐
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 套餐判定工具
|
||||||
|
使用统一的工具函数:
|
||||||
|
- `isPlanFree(plan)` - 判断是否为免费套餐
|
||||||
|
- `isPlanPro(plan)` - 判断是否为Pro套餐
|
||||||
|
- `isCurrentPlan(planId)` - 判断是否为用户当前套餐
|
||||||
|
|
||||||
|
### 3. 类型安全
|
||||||
|
```typescript
|
||||||
|
const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => {
|
||||||
|
// 过滤逻辑
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置要求
|
||||||
|
|
||||||
|
### 1. 套餐配置
|
||||||
|
确保付费套餐有有效的 `stripePriceId`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 检查套餐配置
|
||||||
|
SELECT id, name, "displayName", price, "stripePriceId", "isActive"
|
||||||
|
FROM subscription_plans
|
||||||
|
WHERE "isActive" = true;
|
||||||
|
|
||||||
|
-- 设置价格ID
|
||||||
|
UPDATE subscription_plans
|
||||||
|
SET "stripePriceId" = 'price_your_stripe_price_id'
|
||||||
|
WHERE name = 'pro';
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 验证工具
|
||||||
|
使用测试脚本验证配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx tsx scripts/test-pricing-page-filtering.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势
|
||||||
|
|
||||||
|
1. **用户友好**: 只显示用户可以实际订阅的套餐
|
||||||
|
2. **防止错误**: 避免显示无法订阅的套餐
|
||||||
|
3. **清晰导航**: 明确的按钮状态和操作
|
||||||
|
4. **灵活配置**: 支持动态套餐管理
|
||||||
|
5. **多语言**: 完整的国际化支持
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 问题:套餐不显示
|
||||||
|
**原因**: 付费套餐没有设置 `stripePriceId`
|
||||||
|
**解决**: 在数据库中设置正确的 Stripe 价格 ID
|
||||||
|
|
||||||
|
### 问题:按钮不显示
|
||||||
|
**原因**: 套餐被过滤掉或逻辑判断问题
|
||||||
|
**解决**: 检查套餐配置和用户状态
|
||||||
|
|
||||||
|
### 问题:翻译缺失
|
||||||
|
**原因**: 缺少 `subscribeNow` 翻译键
|
||||||
|
**解决**: 在对应语言文件中添加翻译
|
||||||
|
|
||||||
|
## 测试建议
|
||||||
|
|
||||||
|
1. 测试不同用户状态下的页面显示
|
||||||
|
2. 验证按钮功能和状态
|
||||||
|
3. 检查多语言显示
|
||||||
|
4. 确认套餐过滤逻辑
|
||||||
|
5. 测试订阅流程
|
@ -220,6 +220,7 @@
|
|||||||
"startProTrial": "Upgrade to Pro",
|
"startProTrial": "Upgrade to Pro",
|
||||||
"currentPlan": "Current Plan",
|
"currentPlan": "Current Plan",
|
||||||
"upgradeToPro": "Upgrade to Pro",
|
"upgradeToPro": "Upgrade to Pro",
|
||||||
|
"subscribeNow": "Subscribe Now",
|
||||||
"manageSubscription": "Manage Subscription"
|
"manageSubscription": "Manage Subscription"
|
||||||
},
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
|
@ -220,6 +220,7 @@
|
|||||||
"startProTrial": "升级到专业版",
|
"startProTrial": "升级到专业版",
|
||||||
"currentPlan": "当前方案",
|
"currentPlan": "当前方案",
|
||||||
"upgradeToPro": "升级到专业版",
|
"upgradeToPro": "升级到专业版",
|
||||||
|
"subscribeNow": "立即订阅",
|
||||||
"manageSubscription": "管理订阅"
|
"manageSubscription": "管理订阅"
|
||||||
},
|
},
|
||||||
"subscription": {
|
"subscription": {
|
||||||
|
122
scripts/test-pricing-page-filtering.ts
Normal file
122
scripts/test-pricing-page-filtering.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import { SubscriptionService } from '../src/lib/subscription-service'
|
||||||
|
import { isPlanFree } from '../src/lib/subscription-utils'
|
||||||
|
|
||||||
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
|
async function testPricingPageFiltering() {
|
||||||
|
console.log('🧪 Testing pricing page filtering logic...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 获取所有套餐
|
||||||
|
console.log('\n1. Getting all available plans...')
|
||||||
|
const allPlans = await SubscriptionService.getAvailablePlans()
|
||||||
|
console.log(`📊 Found ${allPlans.length} active plans:`)
|
||||||
|
|
||||||
|
allPlans.forEach(plan => {
|
||||||
|
console.log(` - ${plan.displayName} (${plan.id}):`)
|
||||||
|
console.log(` * Price: $${plan.price}`)
|
||||||
|
console.log(` * Stripe Price ID: ${plan.stripePriceId || 'Not set'}`)
|
||||||
|
console.log(` * Is Free: ${isPlanFree(plan)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. 模拟定价页面的过滤逻辑
|
||||||
|
console.log('\n2. Testing pricing page filtering logic...')
|
||||||
|
|
||||||
|
// 模拟不同用户场景
|
||||||
|
const testScenarios = [
|
||||||
|
{ name: 'Anonymous User', userData: null },
|
||||||
|
{ name: 'Free User', userData: { subscriptionPlanId: 'free' } },
|
||||||
|
{ name: 'Pro User', userData: { subscriptionPlanId: 'pro' } }
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const scenario of testScenarios) {
|
||||||
|
console.log(`\n📋 Scenario: ${scenario.name}`)
|
||||||
|
|
||||||
|
const filteredPlans = allPlans.filter(plan => {
|
||||||
|
// 免费套餐总是显示
|
||||||
|
if (isPlanFree(plan)) return true
|
||||||
|
|
||||||
|
// 用户当前套餐总是显示
|
||||||
|
if (scenario.userData && scenario.userData.subscriptionPlanId === plan.id) return true
|
||||||
|
|
||||||
|
// 其他套餐必须有 stripePriceId 才能显示(可订阅)
|
||||||
|
return plan.stripePriceId && plan.stripePriceId.trim() !== ''
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(` Visible plans: ${filteredPlans.length}`)
|
||||||
|
filteredPlans.forEach(plan => {
|
||||||
|
const isCurrent = scenario.userData?.subscriptionPlanId === plan.id
|
||||||
|
const canSubscribe = !isPlanFree(plan) && plan.stripePriceId && !isCurrent
|
||||||
|
|
||||||
|
console.log(` - ${plan.displayName}:`)
|
||||||
|
console.log(` * Current plan: ${isCurrent}`)
|
||||||
|
console.log(` * Can subscribe: ${canSubscribe}`)
|
||||||
|
console.log(` * Button action: ${getButtonAction(plan, isCurrent, canSubscribe)}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查套餐配置问题
|
||||||
|
console.log('\n3. Checking for potential issues...')
|
||||||
|
|
||||||
|
const plansWithoutPriceId = allPlans.filter(plan =>
|
||||||
|
!isPlanFree(plan) && (!plan.stripePriceId || plan.stripePriceId.trim() === '')
|
||||||
|
)
|
||||||
|
|
||||||
|
if (plansWithoutPriceId.length > 0) {
|
||||||
|
console.log(`⚠️ Found ${plansWithoutPriceId.length} paid plans without Stripe Price ID:`)
|
||||||
|
plansWithoutPriceId.forEach(plan => {
|
||||||
|
console.log(` - ${plan.displayName} ($${plan.price}) - will not be visible to non-subscribers`)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
console.log('✅ All paid plans have valid Stripe Price IDs')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证免费套餐
|
||||||
|
const freePlans = allPlans.filter(isPlanFree)
|
||||||
|
console.log(`\n4. Free plans validation:`)
|
||||||
|
console.log(` Found ${freePlans.length} free plans`)
|
||||||
|
freePlans.forEach(plan => {
|
||||||
|
console.log(` - ${plan.displayName}: Always visible, no subscription button`)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('\n🎉 Pricing page filtering test completed!')
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error)
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getButtonAction(plan: any, isCurrent: boolean, canSubscribe: boolean): string {
|
||||||
|
if (isPlanFree(plan)) {
|
||||||
|
return isCurrent ? 'Current Plan (disabled)' : 'No button'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrent) {
|
||||||
|
return 'Current Plan (disabled)'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canSubscribe) {
|
||||||
|
return 'Subscribe Now'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'No button (no price ID)'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
if (require.main === module) {
|
||||||
|
testPricingPageFiltering()
|
||||||
|
.then(() => {
|
||||||
|
console.log('✅ All tests completed!')
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('❌ Tests failed:', error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export { testPricingPageFiltering }
|
@ -25,16 +25,23 @@ export default function PricingPage() {
|
|||||||
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
|
const [plans, setPlans] = useState<SubscriptionPlan[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchPlans()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchPlans = async () => {
|
const fetchPlans = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/subscription-plans')
|
const response = await fetch('/api/subscription-plans')
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setPlans(data.plans || [])
|
// 过滤套餐:只显示免费套餐、用户当前套餐,以及有 stripePriceId 的套餐
|
||||||
|
const filteredPlans = (data.plans || []).filter((plan: SubscriptionPlan) => {
|
||||||
|
// 免费套餐总是显示
|
||||||
|
if (isPlanFree(plan)) return true
|
||||||
|
|
||||||
|
// 用户当前套餐总是显示
|
||||||
|
if (userData && isCurrentPlan(plan.id)) return true
|
||||||
|
|
||||||
|
// 其他套餐必须有 stripePriceId 才能显示(可订阅)
|
||||||
|
return plan.stripePriceId && plan.stripePriceId.trim() !== ''
|
||||||
|
})
|
||||||
|
setPlans(filteredPlans)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching plans:', error)
|
console.error('Error fetching plans:', error)
|
||||||
@ -43,13 +50,11 @@ export default function PricingPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleGetStarted = () => {
|
useEffect(() => {
|
||||||
if (user) {
|
fetchPlans()
|
||||||
window.location.href = '/studio'
|
}, [userData]) // 依赖 userData,确保用户数据加载后再过滤套餐
|
||||||
} else {
|
|
||||||
window.location.href = '/signup'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isCurrentPlan = (planId: string) => {
|
const isCurrentPlan = (planId: string) => {
|
||||||
if (!userData) return false
|
if (!userData) return false
|
||||||
@ -170,25 +175,55 @@ export default function PricingPage() {
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{isFree ? (
|
{(() => {
|
||||||
<Button
|
// 免费套餐逻辑
|
||||||
className="w-full"
|
if (isFree) {
|
||||||
variant={isCurrent ? 'outline' : 'default'}
|
// 如果是当前套餐,显示"当前套餐"按钮
|
||||||
onClick={handleGetStarted}
|
if (isCurrent) {
|
||||||
disabled={isCurrent}
|
return (
|
||||||
>
|
<Button
|
||||||
{isCurrent ? t('currentPlan') : t('getStartedFree')}
|
className="w-full"
|
||||||
</Button>
|
variant="outline"
|
||||||
) : (
|
disabled
|
||||||
<SubscribeButton
|
>
|
||||||
priceId={plan.stripePriceId || ''}
|
{t('currentPlan')}
|
||||||
planName={plan.displayName}
|
</Button>
|
||||||
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
|
)
|
||||||
disabled={isCurrent}
|
}
|
||||||
>
|
// 免费套餐且非当前套餐,不显示按钮
|
||||||
{isCurrent ? t('currentPlan') : t('upgradePlan')}
|
return null
|
||||||
</SubscribeButton>
|
}
|
||||||
)}
|
|
||||||
|
// 付费套餐逻辑
|
||||||
|
if (isCurrent) {
|
||||||
|
// 当前套餐,显示"当前套餐"按钮
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant="outline"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{t('currentPlan')}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可订阅的付费套餐,显示"立即订阅"按钮
|
||||||
|
if (plan.stripePriceId && plan.stripePriceId.trim() !== '') {
|
||||||
|
return (
|
||||||
|
<SubscribeButton
|
||||||
|
priceId={plan.stripePriceId}
|
||||||
|
planName={plan.displayName}
|
||||||
|
className={`w-full ${isPro ? 'bg-primary hover:bg-primary/90' : ''}`}
|
||||||
|
>
|
||||||
|
{t('subscribeNow')}
|
||||||
|
</SubscribeButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有 stripePriceId 的套餐不显示按钮(理论上不会到这里,因为已经过滤了)
|
||||||
|
return null
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
Loading…
Reference in New Issue
Block a user