@cogito.ai/cli 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/dist/templates/web-nextjs/apps/docs/.source/browser.ts +8 -18
- package/dist/templates/web-nextjs/apps/docs/.source/dynamic.ts +5 -11
- package/dist/templates/web-nextjs/apps/docs/.source/server.ts +26 -37
- package/dist/templates/web-nextjs/apps/docs/content/docs/meta.json +1 -1
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/alipay.mdx +276 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/deployment.mdx +32 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/drizzle.mdx +18 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/getting-started.mdx +98 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/meta.json +13 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/stripe.mdx +18 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/supabase.mdx +130 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/troubleshooting.mdx +87 -0
- package/dist/templates/web-nextjs/apps/docs/content/docs/template/wechat-pay.mdx +318 -0
- package/dist/templates/web-nextjs/apps/web/.env.example +35 -0
- package/dist/templates/web-nextjs/apps/web/messages/en.json +70 -0
- package/dist/templates/web-nextjs/apps/web/messages/zh.json +71 -1
- package/dist/templates/web-nextjs/apps/web/next-env.d.ts +1 -1
- package/dist/templates/web-nextjs/apps/web/package.json +4 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(auth)/forgot-password/page.tsx +4 -10
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx +88 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx +170 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/pricing/page.tsx +120 -0
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/layout.tsx +48 -1
- package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/settings/subscription/page.tsx +128 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/create/route.ts +43 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/notify/route.ts +105 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/query/route.ts +54 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/alipay/return/route.ts +20 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/create/route.ts +52 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/create/route.ts +58 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/notify/route.ts +92 -0
- package/dist/templates/web-nextjs/apps/web/src/app/api/payments/wechat/query/route.ts +54 -0
- package/dist/templates/web-nextjs/apps/web/src/app/docs/page.tsx +5 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/client.ts +36 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/alipay/server.ts +83 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/index.ts +4 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-badge.tsx +9 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/pro-feature-comparison.tsx +70 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/quota-warning-banner.tsx +37 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/components/upgrade-button.tsx +42 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/hooks.ts +141 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-helpers.ts +27 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/payment-service.ts +56 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/service.ts +55 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/types.ts +73 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/client.ts +33 -0
- package/dist/templates/web-nextjs/apps/web/src/features/subscription/wechat/server.ts +147 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/supabase/admin.ts +23 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/client.ts +66 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/crypto.ts +99 -0
- package/dist/templates/web-nextjs/apps/web/src/lib/wechat-pay/types.ts +10 -0
- package/dist/templates/web-nextjs/pnpm-lock.yaml +319 -0
- package/dist/templates/web-nextjs/pnpm-workspace.yaml +7 -8
- package/dist/templates/web-nextjs/supabase/migrations/20250608_add_subscription_tables.sql +125 -0
- package/package.json +1 -1
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"verificationCodeLabel": "邮箱验证码",
|
|
109
109
|
"verificationCodePlaceholder": "输入 6 位验证码",
|
|
110
110
|
"resendIn": "{seconds} 秒后重新发送",
|
|
111
|
-
"resendCode": "
|
|
111
|
+
"resendCode": "重新发送",
|
|
112
112
|
"resetPasswordTitle": "设置新密码",
|
|
113
113
|
"resetPasswordSubtitle": "在下方输入您的新密码。",
|
|
114
114
|
"newPasswordLabel": "新密码",
|
|
@@ -124,6 +124,8 @@
|
|
|
124
124
|
"resetPasswordSuccess": "密码已更新,请登录。"
|
|
125
125
|
},
|
|
126
126
|
"settings": {
|
|
127
|
+
"profileTitle": "个人资料",
|
|
128
|
+
"subscriptionTitle": "订阅与账单",
|
|
127
129
|
"profile": {
|
|
128
130
|
"title": "个人资料设置",
|
|
129
131
|
"subtitle": "管理您的账号设置和偏好。",
|
|
@@ -160,5 +162,73 @@
|
|
|
160
162
|
"description": "了解更多关于 AgentDock 的信息。",
|
|
161
163
|
"placeholder": "此页面为占位符。在发布前请替换为实际的关于我们信息。"
|
|
162
164
|
}
|
|
165
|
+
},
|
|
166
|
+
"pricing": {
|
|
167
|
+
"title": "定价",
|
|
168
|
+
"description": "选择适合您的方案",
|
|
169
|
+
"monthly": "月付",
|
|
170
|
+
"yearly": "年付",
|
|
171
|
+
"free": "免费",
|
|
172
|
+
"perMonth": "月",
|
|
173
|
+
"perYear": "年",
|
|
174
|
+
"currentPlan": "当前方案",
|
|
175
|
+
"popular": "热门",
|
|
176
|
+
"upgrade": "升级",
|
|
177
|
+
"getStarted": "开始使用"
|
|
178
|
+
},
|
|
179
|
+
"payment": {
|
|
180
|
+
"checkoutTitle": "结算",
|
|
181
|
+
"orderSummary": "订单摘要",
|
|
182
|
+
"plan": "方案",
|
|
183
|
+
"billingCycle": "计费周期",
|
|
184
|
+
"total": "总计",
|
|
185
|
+
"paymentMethod": "支付方式",
|
|
186
|
+
"alipay": "支付宝",
|
|
187
|
+
"wechatPay": "微信支付",
|
|
188
|
+
"coupon": "优惠码",
|
|
189
|
+
"couponPlaceholder": "输入优惠码",
|
|
190
|
+
"invalidCoupon": "无效优惠码",
|
|
191
|
+
"paymentError": "支付失败,请重试",
|
|
192
|
+
"confirmPayment": "确认支付",
|
|
193
|
+
"processing": "处理中...",
|
|
194
|
+
"statusTitle": "支付状态",
|
|
195
|
+
"paymentStatus": "支付状态",
|
|
196
|
+
"paid": "已支付",
|
|
197
|
+
"pending": "待支付",
|
|
198
|
+
"failed": "失败",
|
|
199
|
+
"paymentSuccess": "支付成功!",
|
|
200
|
+
"paymentFailed": "支付失败",
|
|
201
|
+
"paymentPending": "等待支付...",
|
|
202
|
+
"paymentPendingDescription": "请完成支付,页面将自动更新",
|
|
203
|
+
"goToDashboard": "进入控制台",
|
|
204
|
+
"tryAgain": "重试"
|
|
205
|
+
},
|
|
206
|
+
"subscription": {
|
|
207
|
+
"title": "订阅与账单",
|
|
208
|
+
"description": "管理您的订阅和支付历史",
|
|
209
|
+
"currentPlan": "当前方案",
|
|
210
|
+
"status": "状态",
|
|
211
|
+
"active": "活跃",
|
|
212
|
+
"trial": "试用",
|
|
213
|
+
"expired": "已过期",
|
|
214
|
+
"cancelled": "已取消",
|
|
215
|
+
"pending": "待处理",
|
|
216
|
+
"billingCycle": "计费周期",
|
|
217
|
+
"monthly": "月付",
|
|
218
|
+
"yearly": "年付",
|
|
219
|
+
"expiresOn": "到期时间",
|
|
220
|
+
"cancelRenewal": "取消自动续费",
|
|
221
|
+
"cancelledAtPeriodEnd": "已设置为当前周期结束后取消",
|
|
222
|
+
"noActiveSubscription": "暂无活跃订阅",
|
|
223
|
+
"upgradeNow": "立即升级",
|
|
224
|
+
"paymentHistory": "支付历史",
|
|
225
|
+
"noPayments": "暂无支付记录",
|
|
226
|
+
"feature": "功能",
|
|
227
|
+
"free": "免费版",
|
|
228
|
+
"pro": "专业版",
|
|
229
|
+
"featureProjects": "项目数量",
|
|
230
|
+
"featureApiCalls": "API 调用次数",
|
|
231
|
+
"featureSupport": "优先支持",
|
|
232
|
+
"featureAnalytics": "高级分析"
|
|
163
233
|
}
|
|
164
234
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -33,9 +33,12 @@
|
|
|
33
33
|
"@radix-ui/react-toggle": "^1.1.10",
|
|
34
34
|
"@radix-ui/react-toggle-group": "^1.1.11",
|
|
35
35
|
"@supabase/ssr": "^0.10.3",
|
|
36
|
+
"@supabase/supabase-js": "^2.107.0",
|
|
36
37
|
"@tabler/icons-react": "^3.44.0",
|
|
37
38
|
"@tailwindcss/postcss": "^4.3.0",
|
|
39
|
+
"@tanstack/react-query": "^5.101.0",
|
|
38
40
|
"@tanstack/react-table": "^8.21.3",
|
|
41
|
+
"alipay-sdk": "^4.14.0",
|
|
39
42
|
"class-variance-authority": "^0.7.1",
|
|
40
43
|
"clsx": "^2.1.1",
|
|
41
44
|
"cmdk": "^1.1.1",
|
|
@@ -44,6 +47,7 @@
|
|
|
44
47
|
"next": "16",
|
|
45
48
|
"next-intl": "^4.13.0",
|
|
46
49
|
"next-themes": "^0.4.6",
|
|
50
|
+
"qrcode.react": "^4.2.0",
|
|
47
51
|
"radix-ui": "^1.4.3",
|
|
48
52
|
"react": "19",
|
|
49
53
|
"react-dom": "19",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useActionState, useEffect, useState
|
|
3
|
+
import { useActionState, useEffect, useState } from 'react'
|
|
4
4
|
import { useParams } from 'next/navigation'
|
|
5
5
|
import Link from 'next/link'
|
|
6
6
|
import { useTranslations } from 'next-intl'
|
|
@@ -78,18 +78,14 @@ export default function ForgotPasswordPage() {
|
|
|
78
78
|
formData.append('email', data.email)
|
|
79
79
|
setEmail(data.email)
|
|
80
80
|
setValue('email', data.email)
|
|
81
|
-
|
|
82
|
-
await sendAction(formData)
|
|
83
|
-
})
|
|
81
|
+
sendAction(formData)
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
async function onResendOTP() {
|
|
87
85
|
if (countdown > 0) return
|
|
88
86
|
const formData = new FormData()
|
|
89
87
|
formData.append('email', email)
|
|
90
|
-
|
|
91
|
-
await sendAction(formData)
|
|
92
|
-
})
|
|
88
|
+
sendAction(formData)
|
|
93
89
|
}
|
|
94
90
|
|
|
95
91
|
async function onSubmit(data: ResetPasswordWithOTPInput) {
|
|
@@ -99,9 +95,7 @@ export default function ForgotPasswordPage() {
|
|
|
99
95
|
formData.append('password', data.password)
|
|
100
96
|
formData.append('confirmPassword', data.confirmPassword)
|
|
101
97
|
formData.append('locale', locale)
|
|
102
|
-
|
|
103
|
-
await verifyAction(formData)
|
|
104
|
-
})
|
|
98
|
+
verifyAction(formData)
|
|
105
99
|
}
|
|
106
100
|
|
|
107
101
|
if (step === 'send') {
|
package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/[paymentId]/page.tsx
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useParams, useRouter } from 'next/navigation'
|
|
5
|
+
import { useTranslations } from 'next-intl'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
8
|
+
import { Badge } from '@/components/ui/badge'
|
|
9
|
+
import { usePaymentStatus } from '@/features/subscription/hooks'
|
|
10
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
11
|
+
|
|
12
|
+
export default function PaymentStatusPage() {
|
|
13
|
+
const t = useTranslations('payment')
|
|
14
|
+
const router = useRouter()
|
|
15
|
+
const params = useParams()
|
|
16
|
+
const paymentId = params.paymentId as string
|
|
17
|
+
|
|
18
|
+
const [paymentMethod, setPaymentMethod] = useState<string>('')
|
|
19
|
+
const [orderNo, setOrderNo] = useState<string>('')
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
async function fetchPayment() {
|
|
23
|
+
const supabase = createBrowserClient(
|
|
24
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
25
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
26
|
+
)
|
|
27
|
+
const { data } = await supabase.from('payments').select('*').eq('id', paymentId).single()
|
|
28
|
+
|
|
29
|
+
if (data) {
|
|
30
|
+
setPaymentMethod(data.payment_method)
|
|
31
|
+
setOrderNo(data.order_no)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
fetchPayment()
|
|
35
|
+
}, [paymentId])
|
|
36
|
+
|
|
37
|
+
const { data: paymentStatus } = usePaymentStatus(orderNo, paymentMethod, {
|
|
38
|
+
enabled: !!orderNo && !!paymentMethod,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const status = paymentStatus?.status || 'pending'
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="container mx-auto max-w-2xl px-4 py-10">
|
|
45
|
+
<h1 className="text-2xl font-bold mb-6">{t('statusTitle')}</h1>
|
|
46
|
+
|
|
47
|
+
<Card>
|
|
48
|
+
<CardHeader>
|
|
49
|
+
<CardTitle className="flex items-center gap-2">
|
|
50
|
+
{t('paymentStatus')}
|
|
51
|
+
<Badge
|
|
52
|
+
variant={
|
|
53
|
+
status === 'paid' ? 'default' : status === 'failed' ? 'destructive' : 'secondary'
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
{t(status)}
|
|
57
|
+
</Badge>
|
|
58
|
+
</CardTitle>
|
|
59
|
+
</CardHeader>
|
|
60
|
+
<CardContent className="space-y-6">
|
|
61
|
+
{status === 'paid' && (
|
|
62
|
+
<div className="text-center space-y-4">
|
|
63
|
+
<div className="text-6xl">✓</div>
|
|
64
|
+
<p className="text-lg font-medium">{t('paymentSuccess')}</p>
|
|
65
|
+
<Button onClick={() => router.push('/dashboard')}>{t('goToDashboard')}</Button>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
{status === 'failed' && (
|
|
70
|
+
<div className="text-center space-y-4">
|
|
71
|
+
<div className="text-6xl">✗</div>
|
|
72
|
+
<p className="text-lg font-medium">{t('paymentFailed')}</p>
|
|
73
|
+
<Button onClick={() => router.push('/pricing')}>{t('tryAgain')}</Button>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
{status === 'pending' && (
|
|
78
|
+
<div className="text-center space-y-4">
|
|
79
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent mx-auto" />
|
|
80
|
+
<p className="text-lg font-medium">{t('paymentPending')}</p>
|
|
81
|
+
<p className="text-sm text-muted-foreground">{t('paymentPendingDescription')}</p>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</CardContent>
|
|
85
|
+
</Card>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
package/dist/templates/web-nextjs/apps/web/src/app/[locale]/(protected)/payment/checkout/page.tsx
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
5
|
+
import { useTranslations } from 'next-intl'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
8
|
+
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
|
|
9
|
+
import { Label } from '@/components/ui/label'
|
|
10
|
+
import { Input } from '@/components/ui/input'
|
|
11
|
+
import { useSubscriptionPlan } from '@/features/subscription/hooks'
|
|
12
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
13
|
+
|
|
14
|
+
export default function CheckoutPage() {
|
|
15
|
+
const t = useTranslations('payment')
|
|
16
|
+
const router = useRouter()
|
|
17
|
+
const searchParams = useSearchParams()
|
|
18
|
+
const planCode = searchParams.get('plan') || ''
|
|
19
|
+
const cycle = (searchParams.get('cycle') as 'monthly' | 'yearly') || 'monthly'
|
|
20
|
+
|
|
21
|
+
const { data: plan, isLoading } = useSubscriptionPlan(planCode)
|
|
22
|
+
const [paymentMethod, setPaymentMethod] = useState<'alipay' | 'wechat'>('alipay')
|
|
23
|
+
const [coupon, setCoupon] = useState('')
|
|
24
|
+
const [couponError, setCouponError] = useState('')
|
|
25
|
+
const [isProcessing, setIsProcessing] = useState(false)
|
|
26
|
+
|
|
27
|
+
if (!planCode) {
|
|
28
|
+
router.push('/pricing')
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (isLoading) {
|
|
33
|
+
return (
|
|
34
|
+
<div className="container mx-auto max-w-2xl px-4 py-10">
|
|
35
|
+
<div className="flex justify-center py-20">
|
|
36
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const amount = cycle === 'monthly' ? plan?.price_monthly : plan?.price_yearly
|
|
43
|
+
|
|
44
|
+
const handleSubmit = async () => {
|
|
45
|
+
setIsProcessing(true)
|
|
46
|
+
try {
|
|
47
|
+
// Validate coupon (stub)
|
|
48
|
+
if (coupon && coupon !== 'DISCOUNT10') {
|
|
49
|
+
setCouponError(t('invalidCoupon'))
|
|
50
|
+
setIsProcessing(false)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
setCouponError('')
|
|
54
|
+
|
|
55
|
+
const supabase = createBrowserClient(
|
|
56
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
57
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
58
|
+
)
|
|
59
|
+
const {
|
|
60
|
+
data: { user },
|
|
61
|
+
} = await supabase.auth.getUser()
|
|
62
|
+
if (!user) {
|
|
63
|
+
router.push('/login')
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create payment record
|
|
68
|
+
const res = await fetch('/api/payments/create', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
userId: user.id,
|
|
73
|
+
planId: plan?.id,
|
|
74
|
+
amount,
|
|
75
|
+
paymentMethod,
|
|
76
|
+
billingCycle: cycle,
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!res.ok) throw new Error('Failed to create payment')
|
|
81
|
+
const payment = await res.json()
|
|
82
|
+
|
|
83
|
+
// Initiate payment
|
|
84
|
+
if (paymentMethod === 'alipay') {
|
|
85
|
+
const { initiateAlipayPayment } = await import('@/features/subscription/alipay/client')
|
|
86
|
+
await initiateAlipayPayment(payment.id, user.id)
|
|
87
|
+
} else {
|
|
88
|
+
const { initiateWechatPayment } = await import('@/features/subscription/wechat/client')
|
|
89
|
+
const result = await initiateWechatPayment(payment.id, user.id)
|
|
90
|
+
if (result.type === 'qrcode') {
|
|
91
|
+
router.push(`/payment/${payment.id}`)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
setCouponError(t('paymentError'))
|
|
96
|
+
} finally {
|
|
97
|
+
setIsProcessing(false)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className="container mx-auto max-w-2xl px-4 py-10">
|
|
103
|
+
<h1 className="text-2xl font-bold mb-6">{t('checkoutTitle')}</h1>
|
|
104
|
+
|
|
105
|
+
<Card className="mb-6">
|
|
106
|
+
<CardHeader>
|
|
107
|
+
<CardTitle>{t('orderSummary')}</CardTitle>
|
|
108
|
+
</CardHeader>
|
|
109
|
+
<CardContent className="space-y-4">
|
|
110
|
+
<div className="flex justify-between">
|
|
111
|
+
<span>{t('plan')}</span>
|
|
112
|
+
<span className="font-medium">{plan?.plan_name}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div className="flex justify-between">
|
|
115
|
+
<span>{t('billingCycle')}</span>
|
|
116
|
+
<span className="font-medium">{cycle === 'monthly' ? t('monthly') : t('yearly')}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="flex justify-between text-lg font-bold">
|
|
119
|
+
<span>{t('total')}</span>
|
|
120
|
+
<span>¥{((amount || 0) / 100).toFixed(2)}</span>
|
|
121
|
+
</div>
|
|
122
|
+
</CardContent>
|
|
123
|
+
</Card>
|
|
124
|
+
|
|
125
|
+
<Card className="mb-6">
|
|
126
|
+
<CardHeader>
|
|
127
|
+
<CardTitle>{t('paymentMethod')}</CardTitle>
|
|
128
|
+
</CardHeader>
|
|
129
|
+
<CardContent>
|
|
130
|
+
<RadioGroup
|
|
131
|
+
value={paymentMethod}
|
|
132
|
+
onValueChange={(v) => setPaymentMethod(v as 'alipay' | 'wechat')}
|
|
133
|
+
className="space-y-3"
|
|
134
|
+
>
|
|
135
|
+
<div className="flex items-center space-x-2 border rounded-lg p-4">
|
|
136
|
+
<RadioGroupItem value="alipay" id="alipay" />
|
|
137
|
+
<Label htmlFor="alipay" className="flex-1 cursor-pointer">
|
|
138
|
+
{t('alipay')}
|
|
139
|
+
</Label>
|
|
140
|
+
</div>
|
|
141
|
+
<div className="flex items-center space-x-2 border rounded-lg p-4">
|
|
142
|
+
<RadioGroupItem value="wechat" id="wechat" />
|
|
143
|
+
<Label htmlFor="wechat" className="flex-1 cursor-pointer">
|
|
144
|
+
{t('wechatPay')}
|
|
145
|
+
</Label>
|
|
146
|
+
</div>
|
|
147
|
+
</RadioGroup>
|
|
148
|
+
</CardContent>
|
|
149
|
+
</Card>
|
|
150
|
+
|
|
151
|
+
<Card className="mb-6">
|
|
152
|
+
<CardHeader>
|
|
153
|
+
<CardTitle>{t('coupon')}</CardTitle>
|
|
154
|
+
</CardHeader>
|
|
155
|
+
<CardContent>
|
|
156
|
+
<Input
|
|
157
|
+
placeholder={t('couponPlaceholder')}
|
|
158
|
+
value={coupon}
|
|
159
|
+
onChange={(e) => setCoupon(e.target.value)}
|
|
160
|
+
/>
|
|
161
|
+
{couponError && <p className="text-sm text-destructive mt-2">{couponError}</p>}
|
|
162
|
+
</CardContent>
|
|
163
|
+
</Card>
|
|
164
|
+
|
|
165
|
+
<Button className="w-full" size="lg" onClick={handleSubmit} disabled={isProcessing}>
|
|
166
|
+
{isProcessing ? t('processing') : t('confirmPayment')}
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
)
|
|
170
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useRouter } from 'next/navigation'
|
|
5
|
+
import { useTranslations } from 'next-intl'
|
|
6
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import {
|
|
9
|
+
Card,
|
|
10
|
+
CardContent,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
CardDescription,
|
|
14
|
+
CardFooter,
|
|
15
|
+
} from '@/components/ui/card'
|
|
16
|
+
import { Badge } from '@/components/ui/badge'
|
|
17
|
+
import { useSubscriptionPlans, useUserSubscription } from '@/features/subscription/hooks'
|
|
18
|
+
import { createBrowserClient } from '@supabase/ssr'
|
|
19
|
+
|
|
20
|
+
export default function PricingPage() {
|
|
21
|
+
const t = useTranslations('pricing')
|
|
22
|
+
const router = useRouter()
|
|
23
|
+
const [billingCycle, setBillingCycle] = useState<'monthly' | 'yearly'>('monthly')
|
|
24
|
+
|
|
25
|
+
const { data: plans, isLoading: plansLoading } = useSubscriptionPlans()
|
|
26
|
+
const [user, setUser] = useState<{ id: string } | null>(null)
|
|
27
|
+
|
|
28
|
+
// Fetch current user
|
|
29
|
+
useState(() => {
|
|
30
|
+
const supabase = createBrowserClient(
|
|
31
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
32
|
+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
|
33
|
+
)
|
|
34
|
+
supabase.auth.getUser().then(({ data }) => {
|
|
35
|
+
if (data.user) {
|
|
36
|
+
setUser({ id: data.user.id })
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const { data: currentSubscription } = useUserSubscription(user?.id || '')
|
|
42
|
+
|
|
43
|
+
const handleSelectPlan = (planCode: string) => {
|
|
44
|
+
router.push(`/payment/checkout?plan=${planCode}&cycle=${billingCycle}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (plansLoading) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="container mx-auto max-w-6xl px-4 py-10">
|
|
50
|
+
<div className="flex justify-center py-20">
|
|
51
|
+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="container mx-auto max-w-6xl px-4 py-10">
|
|
59
|
+
<div className="text-center mb-10">
|
|
60
|
+
<h1 className="text-3xl font-bold mb-4">{t('title')}</h1>
|
|
61
|
+
<p className="text-muted-foreground">{t('description')}</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<Tabs
|
|
65
|
+
value={billingCycle}
|
|
66
|
+
onValueChange={(v) => setBillingCycle(v as 'monthly' | 'yearly')}
|
|
67
|
+
className="w-full max-w-md mx-auto mb-10"
|
|
68
|
+
>
|
|
69
|
+
<TabsList className="grid w-full grid-cols-2">
|
|
70
|
+
<TabsTrigger value="monthly">{t('monthly')}</TabsTrigger>
|
|
71
|
+
<TabsTrigger value="yearly">{t('yearly')}</TabsTrigger>
|
|
72
|
+
</TabsList>
|
|
73
|
+
</Tabs>
|
|
74
|
+
|
|
75
|
+
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
|
76
|
+
{plans?.map((plan) => {
|
|
77
|
+
const price = billingCycle === 'monthly' ? plan.price_monthly : plan.price_yearly
|
|
78
|
+
const isCurrentPlan = currentSubscription?.plan_id === plan.id
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Card
|
|
82
|
+
key={plan.id}
|
|
83
|
+
className={isCurrentPlan ? 'border-primary ring-1 ring-primary' : ''}
|
|
84
|
+
>
|
|
85
|
+
<CardHeader>
|
|
86
|
+
<div className="flex items-center justify-between">
|
|
87
|
+
<CardTitle>{plan.plan_name}</CardTitle>
|
|
88
|
+
{isCurrentPlan && <Badge>{t('currentPlan')}</Badge>}
|
|
89
|
+
{plan.is_featured && <Badge variant="secondary">{t('popular')}</Badge>}
|
|
90
|
+
</div>
|
|
91
|
+
<CardDescription>{plan.description}</CardDescription>
|
|
92
|
+
</CardHeader>
|
|
93
|
+
<CardContent>
|
|
94
|
+
<div className="text-3xl font-bold">
|
|
95
|
+
{price === 0 ? t('free') : `¥${(price / 100).toFixed(2)}`}
|
|
96
|
+
{price > 0 && (
|
|
97
|
+
<span className="text-sm font-normal text-muted-foreground ml-1">
|
|
98
|
+
/{billingCycle === 'monthly' ? t('perMonth') : t('perYear')}
|
|
99
|
+
</span>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</CardContent>
|
|
103
|
+
<CardFooter>
|
|
104
|
+
{isCurrentPlan ? (
|
|
105
|
+
<Button variant="outline" className="w-full" disabled>
|
|
106
|
+
{t('currentPlan')}
|
|
107
|
+
</Button>
|
|
108
|
+
) : (
|
|
109
|
+
<Button className="w-full" onClick={() => handleSelectPlan(plan.plan_code)}>
|
|
110
|
+
{price === 0 ? t('getStarted') : t('upgrade')}
|
|
111
|
+
</Button>
|
|
112
|
+
)}
|
|
113
|
+
</CardFooter>
|
|
114
|
+
</Card>
|
|
115
|
+
)
|
|
116
|
+
})}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -1,5 +1,52 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
1
3
|
import { ReactNode } from 'react'
|
|
4
|
+
import Link from 'next/link'
|
|
5
|
+
import { usePathname } from 'next/navigation'
|
|
6
|
+
import { useTranslations } from 'next-intl'
|
|
7
|
+
import { User, CreditCard } from 'lucide-react'
|
|
8
|
+
import { cn } from '@/lib/utils'
|
|
2
9
|
|
|
3
10
|
export default function SettingsLayout({ children }: { children: ReactNode }) {
|
|
4
|
-
|
|
11
|
+
const t = useTranslations('settings')
|
|
12
|
+
const pathname = usePathname()
|
|
13
|
+
|
|
14
|
+
const navItems = [
|
|
15
|
+
{ href: '/settings/profile', label: t('profileTitle'), icon: User },
|
|
16
|
+
{ href: '/settings/subscription', label: t('subscriptionTitle'), icon: CreditCard },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="container mx-auto max-w-6xl px-4 py-10">
|
|
21
|
+
<div className="flex flex-col gap-8 md:flex-row">
|
|
22
|
+
{/* Sidebar Navigation */}
|
|
23
|
+
<aside className="w-full md:w-64 flex-shrink-0">
|
|
24
|
+
<nav className="space-y-1">
|
|
25
|
+
{navItems.map((item) => {
|
|
26
|
+
const isActive = pathname === item.href || pathname.startsWith(item.href + '/')
|
|
27
|
+
const Icon = item.icon
|
|
28
|
+
return (
|
|
29
|
+
<Link
|
|
30
|
+
key={item.href}
|
|
31
|
+
href={item.href}
|
|
32
|
+
className={cn(
|
|
33
|
+
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
|
34
|
+
isActive
|
|
35
|
+
? 'bg-primary text-primary-foreground'
|
|
36
|
+
: 'text-muted-foreground hover:bg-muted hover:text-foreground',
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
<Icon className="h-4 w-4" />
|
|
40
|
+
{item.label}
|
|
41
|
+
</Link>
|
|
42
|
+
)
|
|
43
|
+
})}
|
|
44
|
+
</nav>
|
|
45
|
+
</aside>
|
|
46
|
+
|
|
47
|
+
{/* Content */}
|
|
48
|
+
<main className="flex-1 min-w-0">{children}</main>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
5
52
|
}
|